View Javadoc

1   /*
2   Copyright (c) 2011 James Ahlborn
3   
4   This library is free software; you can redistribute it and/or
5   modify it under the terms of the GNU Lesser General Public
6   License as published by the Free Software Foundation; either
7   version 2.1 of the License, or (at your option) any later version.
8   
9   This library is distributed in the hope that it will be useful,
10  but WITHOUT ANY WARRANTY; without even the implied warranty of
11  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  Lesser General Public License for more details.
13  
14  You should have received a copy of the GNU Lesser General Public
15  License along with this library; if not, write to the Free Software
16  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
17  USA
18  */
19  
20  package com.healthmarketscience.jackcess;
21  
22  import java.io.IOException;
23  import java.util.Collection;
24  import java.util.Collections;
25  import java.util.Iterator;
26  import java.util.List;
27  import java.util.Map;
28  
29  /**
30   * Utility for finding rows based on pre-defined, foreign-key table
31   * relationships.
32   *
33   * @author James Ahlborn
34   */
35  public class Joiner 
36  {
37    private final Index _fromIndex;
38    private final List<IndexData.ColumnDescriptor> _fromCols;
39    private final IndexCursor _toCursor;
40    private final Object[] _entryValues;
41    
42    private Joiner(Index fromIndex, IndexCursor toCursor)
43    {
44      _fromIndex = fromIndex;
45      _fromCols = _fromIndex.getColumns();
46      _entryValues = new Object[_fromCols.size()];
47      _toCursor = toCursor;
48    }
49  
50    /**
51     * Creates a new Joiner based on the foreign-key relationship between the
52     * given "from"" table and the given "to"" table.
53     *
54     * @param fromTable the "from" side of the relationship
55     * @param toTable the "to" side of the relationship
56     * @throws IllegalArgumentException if there is no relationship between the
57     *         given tables
58     */
59    public static Joiner create(Table fromTable, Table toTable)
60      throws IOException
61    {
62      return create(fromTable.getForeignKeyIndex(toTable));
63    }
64    
65    /**
66     * Creates a new Joiner based on the given index which backs a foreign-key
67     * relationship.  The table of the given index will be the "from" table and
68     * the table on the other end of the relationship will be the "to" table.
69     *
70     * @param fromIndex the index backing one side of a foreign-key relationship
71     */
72    public static Joiner create(Index fromIndex)
73      throws IOException
74    {
75      Index toIndex = fromIndex.getReferencedIndex();
76      IndexCursor toCursor = IndexCursor.createCursor(
77          toIndex.getTable(), toIndex);
78      // text lookups are always case-insensitive
79      toCursor.setColumnMatcher(CaseInsensitiveColumnMatcher.INSTANCE);
80      return new Joiner(fromIndex, toCursor);
81    }
82  
83    /**
84     * Creates a new Joiner that is the reverse of this Joiner (the "from" and
85     * "to" tables are swapped).
86     */ 
87    public Joiner createReverse()
88      throws IOException
89    {
90      return create(getToTable(), getFromTable());
91    }
92    
93    public Table getFromTable() {
94      return getFromIndex().getTable();
95    }
96    
97    public Index getFromIndex() {
98      return _fromIndex;
99    }
100   
101   public Table getToTable() {
102     return getToCursor().getTable();
103   }
104   
105   public Index getToIndex() {
106     return getToCursor().getIndex();
107   }
108   
109   public IndexCursor getToCursor() {
110     return _toCursor;
111   }
112 
113   public List<IndexData.ColumnDescriptor> getColumns() {
114     // note, this list is already unmodifiable, no need to re-wrap
115     return _fromCols;
116   }
117 
118   /**
119    * Returns {@code true} if the "to" table has any rows based on the given
120    * columns in the "from" table, {@code false} otherwise.
121    */
122   public boolean hasRows(Map<String,?> fromRow) throws IOException {
123     toEntryValues(fromRow);
124     return _toCursor.findFirstRowByEntry(_entryValues);
125   }
126 
127   /**
128    * Returns {@code true} if the "to" table has any rows based on the given
129    * columns in the "from" table, {@code false} otherwise.
130    */
131   boolean hasRows(Object[] fromRow) throws IOException {
132     toEntryValues(fromRow);
133     return _toCursor.findFirstRowByEntry(_entryValues);
134   }
135 
136   /**
137    * Returns the first row in the "to" table based on the given columns in the
138    * "from" table if any, {@code null} if there is no matching row.
139    *
140    * @param fromRow row from the "from" table (which must include the relevant
141    *                columns for this join relationship)
142    */
143   public Map<String,Object> findFirstRow(Map<String,?> fromRow)
144     throws IOException
145   {
146     return findFirstRow(fromRow, null);
147   }
148   
149   /**
150    * Returns selected columns from the first row in the "to" table based on
151    * the given columns in the "from" table if any, {@code null} if there is no
152    * matching row.
153    *
154    * @param fromRow row from the "from" table (which must include the relevant
155    *                columns for this join relationship)
156    * @param columnNames desired columns in the from table row
157    */
158   public Map<String,Object> findFirstRow(Map<String,?> fromRow,
159                                          Collection<String> columnNames)
160     throws IOException
161   {
162     return (hasRows(fromRow) ? _toCursor.getCurrentRow(columnNames) : null);
163   }
164 
165   /**
166    * Returns an Iterator over all the rows in the "to" table based on the
167    * given columns in the "from" table.
168    *
169    * @param fromRow row from the "from" table (which must include the relevant
170    *                columns for this join relationship)
171    */
172   public Iterator<Map<String,Object>> findRows(Map<String,?> fromRow)
173   {
174     return findRows(fromRow, null);
175   }
176   
177   /**
178    * Returns an Iterator with the selected columns over all the rows in the
179    * "to" table based on the given columns in the "from" table.
180    *
181    * @param fromRow row from the "from" table (which must include the relevant
182    *                columns for this join relationship)
183    * @param columnNames desired columns in the from table row
184    */
185   public Iterator<Map<String,Object>> findRows(Map<String,?> fromRow,
186                                                Collection<String> columnNames)
187   {
188     toEntryValues(fromRow);
189     return _toCursor.entryIterator(columnNames, _entryValues);
190   }
191 
192   /**
193    * Returns an Iterator with the selected columns over all the rows in the
194    * "to" table based on the given columns in the "from" table.
195    *
196    * @param fromRow row from the "from" table (which must include the relevant
197    *                columns for this join relationship)
198    * @param columnNames desired columns in the from table row
199    */
200   Iterator<Map<String,Object>> findRows(Object[] fromRow,
201                                         Collection<String> columnNames)
202   {
203     toEntryValues(fromRow);
204     return _toCursor.entryIterator(columnNames, _entryValues);
205   }
206 
207   /**
208    * Returns an Iterable whose iterator() method returns the result of a call
209    * to {@link #findRows(Map)}
210    * 
211    * @param fromRow row from the "from" table (which must include the relevant
212    *                columns for this join relationship)
213    * @throws IllegalStateException if an IOException is thrown by one of the
214    *         operations, the actual exception will be contained within
215    */
216   public Iterable<Map<String,Object>> findRowsIterable(Map<String,?> fromRow)
217   {
218     return findRowsIterable(fromRow, null);
219   }
220   
221   /**
222    * Returns an Iterable whose iterator() method returns the result of a call
223    * to {@link #findRows(Map,Collection)}
224    * 
225    * @param fromRow row from the "from" table (which must include the relevant
226    *                columns for this join relationship)
227    * @param columnNames desired columns in the from table row
228    * @throws IllegalStateException if an IOException is thrown by one of the
229    *         operations, the actual exception will be contained within
230    */
231   public Iterable<Map<String,Object>> findRowsIterable(
232       final Map<String,?> fromRow, final Collection<String> columnNames)
233   {
234     return new Iterable<Map<String, Object>>() {
235       public Iterator<Map<String, Object>> iterator() {
236         return findRows(fromRow, columnNames);
237       }
238     };
239   }
240 
241   /**
242    * Deletes any rows in the "to" table based on the given columns in the
243    * "from" table.
244    * 
245    * @param fromRow row from the "from" table (which must include the relevant
246    *                columns for this join relationship)
247    * @return {@code true} if any "to" rows were deleted, {@code false}
248    *         otherwise
249    */
250   public boolean deleteRows(Map<String,?> fromRow) throws IOException {
251     return deleteRowsImpl(findRows(fromRow, Collections.<String>emptySet()));
252   }
253 
254   /**
255    * Deletes any rows in the "to" table based on the given columns in the
256    * "from" table.
257    * 
258    * @param fromRow row from the "from" table (which must include the relevant
259    *                columns for this join relationship)
260    * @return {@code true} if any "to" rows were deleted, {@code false}
261    *         otherwise
262    */
263   boolean deleteRows(Object[] fromRow) throws IOException {
264     return deleteRowsImpl(findRows(fromRow, Collections.<String>emptySet()));
265   }
266 
267   /**
268    * Deletes all the rows and returns whether or not any "to"" rows were
269    * deleted.
270    */
271   private static boolean deleteRowsImpl(Iterator<Map<String,Object>> iter)
272     throws IOException 
273   {
274     boolean removed = false;
275     while(iter.hasNext()) {
276       iter.next();
277       iter.remove();
278       removed = true;
279     }
280     return removed;    
281   }
282 
283   /**
284    * Fills in the _entryValues with the relevant info from the given "from"
285    * table row.
286    */
287   private void toEntryValues(Map<String,?> fromRow) {
288     for(int i = 0; i < _entryValues.length; ++i) {
289       _entryValues[i] = _fromCols.get(i).getColumn().getRowValue(fromRow);
290     }
291   }
292 
293   /**
294    * Fills in the _entryValues with the relevant info from the given "from"
295    * table row.
296    */
297   private void toEntryValues(Object[] fromRow) {
298     for(int i = 0; i < _entryValues.length; ++i) {
299       _entryValues[i] = _fromCols.get(i).getColumn().getRowValue(fromRow);
300     }
301   }
302 
303   /**
304    * Returns a pretty string describing the foreign key relationship backing
305    * this Joiner.
306    */
307   public String toFKString() {
308     StringBuilder sb = new StringBuilder();
309     sb.append("Foreign Key from ");
310 
311     String fromType = "] (primary)";
312     String toType = "] (secondary)";
313     if(!_fromIndex.getReference().isPrimaryTable()) {
314       fromType = "] (secondary)";
315       toType = "] (primary)";
316     }
317 
318     sb.append(getFromTable().getName()).append("[");
319 
320     sb.append(_fromCols.get(0).getName());
321     for(int i = 1; i < _fromCols.size(); ++i) {
322       sb.append(",").append(_fromCols.get(i).getName());
323     }
324     sb.append(fromType);
325     
326     sb.append(" to ").append(getToTable().getName()).append("[");
327     List<IndexData.ColumnDescriptor> toCols = _toCursor.getIndex().getColumns();
328     sb.append(toCols.get(0).getName());
329     for(int i = 1; i < toCols.size(); ++i) {
330       sb.append(",").append(toCols.get(i).getName());
331     }
332     sb.append(toType);
333     
334     return sb.toString();
335   }
336 }