View Javadoc
1   /*
2   Copyright (c) 2011 James Ahlborn
3   
4   Licensed under the Apache License, Version 2.0 (the "License");
5   you may not use this file except in compliance with the License.
6   You may obtain a copy of the License at
7   
8       http://www.apache.org/licenses/LICENSE-2.0
9   
10  Unless required by applicable law or agreed to in writing, software
11  distributed under the License is distributed on an "AS IS" BASIS,
12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  See the License for the specific language governing permissions and
14  limitations under the License.
15  */
16  
17  package com.healthmarketscience.jackcess.util;
18  
19  import java.io.IOException;
20  import java.util.Collection;
21  import java.util.Collections;
22  import java.util.Iterator;
23  import java.util.List;
24  import java.util.Map;
25  
26  import com.healthmarketscience.jackcess.CursorBuilder;
27  import com.healthmarketscience.jackcess.Index;
28  import com.healthmarketscience.jackcess.IndexCursor;
29  import com.healthmarketscience.jackcess.Row;
30  import com.healthmarketscience.jackcess.Table;
31  import com.healthmarketscience.jackcess.impl.DatabaseImpl;
32  import com.healthmarketscience.jackcess.impl.IndexImpl;
33  
34  /**
35   * Utility for finding rows based on pre-defined, foreign-key table
36   * relationships.
37   *
38   * @author James Ahlborn
39   * @usage _general_class_
40   */
41  public class Joiner
42  {
43    private final Index _fromIndex;
44    private final List<? extends Index.Column> _fromCols;
45    private final IndexCursor _toCursor;
46    private final Object[] _entryValues;
47  
48    private Joiner(Index fromIndex, IndexCursor toCursor)
49    {
50      _fromIndex = fromIndex;
51      _fromCols = _fromIndex.getColumns();
52      _entryValues = new Object[_fromCols.size()];
53      _toCursor = toCursor;
54    }
55  
56    /**
57     * Creates a new Joiner based on the foreign-key relationship between the
58     * given "from"" table and the given "to"" table.
59     *
60     * @param fromTable the "from" side of the relationship
61     * @param toTable the "to" side of the relationship
62     * @throws IllegalArgumentException if there is no relationship between the
63     *         given tables
64     */
65    public static Joiner create(Table../../../../com/healthmarketscience/jackcess/Table.html#Table">Table fromTable, Table toTable)
66      throws IOException
67    {
68      return create(fromTable.getForeignKeyIndex(toTable));
69    }
70  
71    /**
72     * Creates a new Joiner based on the given index which backs a foreign-key
73     * relationship.  The table of the given index will be the "from" table and
74     * the table on the other end of the relationship will be the "to" table.
75     *
76     * @param fromIndex the index backing one side of a foreign-key relationship
77     */
78    public static Joiner create(Index fromIndex)
79      throws IOException
80    {
81      Index toIndex = fromIndex.getReferencedIndex();
82      IndexCursor toCursor = CursorBuilder.createCursor(toIndex);
83      // text lookups are always case-insensitive
84      toCursor.setColumnMatcher(CaseInsensitiveColumnMatcher.INSTANCE);
85      return new Joiner(fromIndex, toCursor);
86    }
87  
88    /**
89     * Creates a new Joiner that is the reverse of this Joiner (the "from" and
90     * "to" tables are swapped).
91     */
92    public Joiner createReverse()
93      throws IOException
94    {
95      return create(getToTable(), getFromTable());
96    }
97  
98    public Table getFromTable() {
99      return getFromIndex().getTable();
100   }
101 
102   public Index getFromIndex() {
103     return _fromIndex;
104   }
105 
106   public Table getToTable() {
107     return getToCursor().getTable();
108   }
109 
110   public Index getToIndex() {
111     return getToCursor().getIndex();
112   }
113 
114   public IndexCursor getToCursor() {
115     return _toCursor;
116   }
117 
118   public List<? extends Index.Column> getColumns() {
119     // note, this list is already unmodifiable, no need to re-wrap
120     return _fromCols;
121   }
122 
123   /**
124    * Returns {@code true} if the "to" table has any rows based on the given
125    * columns in the "from" table, {@code false} otherwise.
126    */
127   public boolean hasRows(Map<String,?> fromRow) throws IOException {
128     toEntryValues(fromRow);
129     return _toCursor.findFirstRowByEntry(_entryValues);
130   }
131 
132   /**
133    * Returns {@code true} if the "to" table has any rows based on the given
134    * columns in the "from" table, {@code false} otherwise.
135    * @usage _intermediate_method_
136    */
137   public boolean hasRows(Object[] fromRow) throws IOException {
138     toEntryValues(fromRow);
139     return _toCursor.findFirstRowByEntry(_entryValues);
140   }
141 
142   /**
143    * Returns the first row in the "to" table based on the given columns in the
144    * "from" table if any, {@code null} if there is no matching row.
145    *
146    * @param fromRow row from the "from" table (which must include the relevant
147    *                columns for this join relationship)
148    */
149   public Row findFirstRow(Map<String,?> fromRow)
150     throws IOException
151   {
152     return findFirstRow(fromRow, null);
153   }
154 
155   /**
156    * Returns selected columns from the first row in the "to" table based on
157    * the given columns in the "from" table if any, {@code null} if there is no
158    * matching row.
159    *
160    * @param fromRow row from the "from" table (which must include the relevant
161    *                columns for this join relationship)
162    * @param columnNames desired columns in the from table row
163    */
164   public Row findFirstRow(Map<String,?> fromRow, Collection<String> columnNames)
165     throws IOException
166   {
167     return (hasRows(fromRow) ? _toCursor.getCurrentRow(columnNames) : null);
168   }
169 
170   /**
171    * Returns an Iterator over all the rows in the "to" table based on the
172    * given columns in the "from" table.
173    *
174    * @param fromRow row from the "from" table (which must include the relevant
175    *                columns for this join relationship)
176    */
177   public EntryIterableBuilder findRows(Map<String,?> fromRow)
178   {
179     toEntryValues(fromRow);
180     return _toCursor.newEntryIterable(_entryValues);
181   }
182 
183   /**
184    * Returns an Iterator with the selected columns over all the rows in the
185    * "to" table based on the given columns in the "from" table.
186    *
187    * @param fromRow row from the "from" table (which must include the relevant
188    *                columns for this join relationship)
189    */
190   public EntryIterableBuilder findRows(Object[] fromRow)
191   {
192     toEntryValues(fromRow);
193     return _toCursor.newEntryIterable(_entryValues);
194   }
195 
196   /**
197    * Deletes any rows in the "to" table based on the given columns in the
198    * "from" table.
199    *
200    * @param fromRow row from the "from" table (which must include the relevant
201    *                columns for this join relationship)
202    * @return {@code true} if any "to" rows were deleted, {@code false}
203    *         otherwise
204    */
205   public boolean deleteRows(Map<String,?> fromRow) {
206     return deleteRowsImpl(findRows(fromRow)
207                           .setColumnNames(Collections.<String>emptySet())
208                           .iterator());
209   }
210 
211   /**
212    * Deletes any rows in the "to" table based on the given columns in the
213    * "from" table.
214    *
215    * @param fromRow row from the "from" table (which must include the relevant
216    *                columns for this join relationship)
217    * @return {@code true} if any "to" rows were deleted, {@code false}
218    *         otherwise
219    * @usage _intermediate_method_
220    */
221   public boolean deleteRows(Object[] fromRow) {
222     return deleteRowsImpl(findRows(fromRow)
223                           .setColumnNames(Collections.<String>emptySet())
224                           .iterator());
225   }
226 
227   /**
228    * Deletes all the rows and returns whether or not any "to"" rows were
229    * deleted.
230    */
231   private static boolean deleteRowsImpl(Iterator<Row> iter)
232   {
233     boolean removed = false;
234     while(iter.hasNext()) {
235       iter.next();
236       iter.remove();
237       removed = true;
238     }
239     return removed;
240   }
241 
242   /**
243    * Fills in the _entryValues with the relevant info from the given "from"
244    * table row.
245    */
246   private void toEntryValues(Map<String,?> fromRow) {
247     for(int i = 0; i < _entryValues.length; ++i) {
248       _entryValues[i] = _fromCols.get(i).getColumn().getRowValue(fromRow);
249     }
250   }
251 
252   /**
253    * Fills in the _entryValues with the relevant info from the given "from"
254    * table row.
255    */
256   private void toEntryValues(Object[] fromRow) {
257     for(int i = 0; i < _entryValues.length; ++i) {
258       _entryValues[i] = _fromCols.get(i).getColumn().getRowValue(fromRow);
259     }
260   }
261 
262   /**
263    * Returns a pretty string describing the foreign key relationship backing
264    * this Joiner.
265    */
266   public String toFKString() {
267     StringBuilder sb = new StringBuilder();
268     sb.append("Foreign Key from ");
269 
270     String fromType = "] (primary)";
271     String toType = "] (secondary)";
272     if(!((IndexImpl)_fromIndex).getReference().isPrimaryTable()) {
273       fromType = "] (secondary)";
274       toType = "] (primary)";
275     }
276 
277     sb.append(getFromTable().getName()).append("[");
278 
279     sb.append(_fromCols.get(0).getName());
280     for(int i = 1; i < _fromCols.size(); ++i) {
281       sb.append(",").append(_fromCols.get(i).getName());
282     }
283     sb.append(fromType);
284 
285     sb.append(" to ").append(getToTable().getName()).append("[");
286     List<? extends Index.Column> toCols = _toCursor.getIndex().getColumns();
287     sb.append(toCols.get(0).getName());
288     for(int i = 1; i < toCols.size(); ++i) {
289       sb.append(",").append(toCols.get(i).getName());
290     }
291     sb.append(toType)
292       .append(" (Db=")
293       .append(((DatabaseImpl)getFromTable().getDatabase()).getName())
294       .append(")");
295 
296     return sb.toString();
297   }
298 }