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 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) throws IOException {
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) throws IOException {
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     throws IOException 
233   {
234     boolean removed = false;
235     while(iter.hasNext()) {
236       iter.next();
237       iter.remove();
238       removed = true;
239     }
240     return removed;    
241   }
242 
243   /**
244    * Fills in the _entryValues with the relevant info from the given "from"
245    * table row.
246    */
247   private void toEntryValues(Map<String,?> fromRow) {
248     for(int i = 0; i < _entryValues.length; ++i) {
249       _entryValues[i] = _fromCols.get(i).getColumn().getRowValue(fromRow);
250     }
251   }
252 
253   /**
254    * Fills in the _entryValues with the relevant info from the given "from"
255    * table row.
256    */
257   private void toEntryValues(Object[] fromRow) {
258     for(int i = 0; i < _entryValues.length; ++i) {
259       _entryValues[i] = _fromCols.get(i).getColumn().getRowValue(fromRow);
260     }
261   }
262 
263   /**
264    * Returns a pretty string describing the foreign key relationship backing
265    * this Joiner.
266    */
267   public String toFKString() {
268     StringBuilder sb = new StringBuilder();
269     sb.append("Foreign Key from ");
270 
271     String fromType = "] (primary)";
272     String toType = "] (secondary)";
273     if(!((IndexImpl)_fromIndex).getReference().isPrimaryTable()) {
274       fromType = "] (secondary)";
275       toType = "] (primary)";
276     }
277 
278     sb.append(getFromTable().getName()).append("[");
279 
280     sb.append(_fromCols.get(0).getName());
281     for(int i = 1; i < _fromCols.size(); ++i) {
282       sb.append(",").append(_fromCols.get(i).getName());
283     }
284     sb.append(fromType);
285     
286     sb.append(" to ").append(getToTable().getName()).append("[");
287     List<? extends Index.Column> toCols = _toCursor.getIndex().getColumns();
288     sb.append(toCols.get(0).getName());
289     for(int i = 1; i < toCols.size(); ++i) {
290       sb.append(",").append(toCols.get(i).getName());
291     }
292     sb.append(toType)
293       .append(" (Db=")
294       .append(((DatabaseImpl)getFromTable().getDatabase()).getName())
295       .append(")");
296     
297     return sb.toString();
298   }
299 }