1 /*
2  * Copyright (C) 2008 The Android Open Source Project
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 android.database;
18 
19 import java.util.Iterator;
20 
21 /**
22  * Does a join on two cursors using the specified columns. The cursors must already
23  * be sorted on each of the specified columns in ascending order. This joiner only
24  * supports the case where the tuple of key column values is unique.
25  * <p>
26  * Typical usage:
27  *
28  * <pre>
29  * CursorJoiner joiner = new CursorJoiner(cursorA, keyColumnsofA, cursorB, keyColumnsofB);
30  * for (CursorJointer.Result joinerResult : joiner) {
31  *     switch (joinerResult) {
32  *         case LEFT:
33  *             // handle case where a row in cursorA is unique
34  *             break;
35  *         case RIGHT:
36  *             // handle case where a row in cursorB is unique
37  *             break;
38  *         case BOTH:
39  *             // handle case where a row with the same key is in both cursors
40  *             break;
41  *     }
42  * }
43  * </pre>
44  */
45 public final class CursorJoiner
46         implements Iterator<CursorJoiner.Result>, Iterable<CursorJoiner.Result> {
47     private Cursor mCursorLeft;
48     private Cursor mCursorRight;
49     private boolean mCompareResultIsValid;
50     private Result mCompareResult;
51     private int[] mColumnsLeft;
52     private int[] mColumnsRight;
53     private String[] mValues;
54 
55     /**
56      * The result of a call to next().
57      */
58     public enum Result {
59         /** The row currently pointed to by the left cursor is unique */
60         RIGHT,
61         /** The row currently pointed to by the right cursor is unique */
62         LEFT,
63         /** The rows pointed to by both cursors are the same */
64         BOTH
65     }
66 
67     /**
68      * Initializes the CursorJoiner and resets the cursors to the first row. The left and right
69      * column name arrays must have the same number of columns.
70      * @param cursorLeft The left cursor to compare
71      * @param columnNamesLeft The column names to compare from the left cursor
72      * @param cursorRight The right cursor to compare
73      * @param columnNamesRight The column names to compare from the right cursor
74      */
CursorJoiner( Cursor cursorLeft, String[] columnNamesLeft, Cursor cursorRight, String[] columnNamesRight)75     public CursorJoiner(
76             Cursor cursorLeft, String[] columnNamesLeft,
77             Cursor cursorRight, String[] columnNamesRight) {
78         if (columnNamesLeft.length != columnNamesRight.length) {
79             throw new IllegalArgumentException(
80                     "you must have the same number of columns on the left and right, "
81                             + columnNamesLeft.length + " != " + columnNamesRight.length);
82         }
83 
84         mCursorLeft = cursorLeft;
85         mCursorRight = cursorRight;
86 
87         mCursorLeft.moveToFirst();
88         mCursorRight.moveToFirst();
89 
90         mCompareResultIsValid = false;
91 
92         mColumnsLeft = buildColumnIndiciesArray(cursorLeft, columnNamesLeft);
93         mColumnsRight = buildColumnIndiciesArray(cursorRight, columnNamesRight);
94 
95         mValues = new String[mColumnsLeft.length * 2];
96     }
97 
iterator()98     public Iterator<Result> iterator() {
99         return this;
100     }
101 
102     /**
103      * Lookup the indicies of the each column name and return them in an array.
104      * @param cursor the cursor that contains the columns
105      * @param columnNames the array of names to lookup
106      * @return an array of column indices
107      */
buildColumnIndiciesArray(Cursor cursor, String[] columnNames)108     private int[] buildColumnIndiciesArray(Cursor cursor, String[] columnNames) {
109         int[] columns = new int[columnNames.length];
110         for (int i = 0; i < columnNames.length; i++) {
111             columns[i] = cursor.getColumnIndexOrThrow(columnNames[i]);
112         }
113         return columns;
114     }
115 
116     /**
117      * Returns whether or not there are more rows to compare using next().
118      * @return true if there are more rows to compare
119      */
hasNext()120     public boolean hasNext() {
121         if (mCompareResultIsValid) {
122             switch (mCompareResult) {
123                 case BOTH:
124                     return !mCursorLeft.isLast() || !mCursorRight.isLast();
125 
126                 case LEFT:
127                     return !mCursorLeft.isLast() || !mCursorRight.isAfterLast();
128 
129                 case RIGHT:
130                     return !mCursorLeft.isAfterLast() || !mCursorRight.isLast();
131 
132                 default:
133                     throw new IllegalStateException("bad value for mCompareResult, "
134                             + mCompareResult);
135             }
136         } else {
137             return !mCursorLeft.isAfterLast() || !mCursorRight.isAfterLast();
138         }
139     }
140 
141     /**
142      * Returns the comparison result of the next row from each cursor. If one cursor
143      * has no more rows but the other does then subsequent calls to this will indicate that
144      * the remaining rows are unique.
145      * <p>
146      * The caller must check that hasNext() returns true before calling this.
147      * <p>
148      * Once next() has been called the cursors specified in the result of the call to
149      * next() are guaranteed to point to the row that was indicated. Reading values
150      * from the cursor that was not indicated in the call to next() will result in
151      * undefined behavior.
152      * @return LEFT, if the row pointed to by the left cursor is unique, RIGHT
153      *   if the row pointed to by the right cursor is unique, BOTH if the rows in both
154      *   cursors are the same.
155      */
next()156     public Result next() {
157         if (!hasNext()) {
158             throw new IllegalStateException("you must only call next() when hasNext() is true");
159         }
160         incrementCursors();
161         assert hasNext();
162 
163         boolean hasLeft = !mCursorLeft.isAfterLast();
164         boolean hasRight = !mCursorRight.isAfterLast();
165 
166         if (hasLeft && hasRight) {
167             populateValues(mValues, mCursorLeft, mColumnsLeft, 0 /* start filling at index 0 */);
168             populateValues(mValues, mCursorRight, mColumnsRight, 1 /* start filling at index 1 */);
169             switch (compareStrings(mValues)) {
170                 case -1:
171                     mCompareResult = Result.LEFT;
172                     break;
173                 case 0:
174                     mCompareResult = Result.BOTH;
175                     break;
176                 case 1:
177                     mCompareResult = Result.RIGHT;
178                     break;
179             }
180         } else if (hasLeft) {
181             mCompareResult = Result.LEFT;
182         } else  {
183             assert hasRight;
184             mCompareResult = Result.RIGHT;
185         }
186         mCompareResultIsValid = true;
187         return mCompareResult;
188     }
189 
remove()190     public void remove() {
191         throw new UnsupportedOperationException("not implemented");
192     }
193 
194     /**
195      * Reads the strings from the cursor that are specifed in the columnIndicies
196      * array and saves them in values beginning at startingIndex, skipping a slot
197      * for each value. If columnIndicies has length 3 and startingIndex is 1, the
198      * values will be stored in slots 1, 3, and 5.
199      * @param values the String[] to populate
200      * @param cursor the cursor from which to read
201      * @param columnIndicies the indicies of the values to read from the cursor
202      * @param startingIndex the slot in which to start storing values, and must be either 0 or 1.
203      */
populateValues(String[] values, Cursor cursor, int[] columnIndicies, int startingIndex)204     private static void populateValues(String[] values, Cursor cursor, int[] columnIndicies,
205             int startingIndex) {
206         assert startingIndex == 0 || startingIndex == 1;
207         for (int i = 0; i < columnIndicies.length; i++) {
208             values[startingIndex + i*2] = cursor.getString(columnIndicies[i]);
209         }
210     }
211 
212     /**
213      * Increment the cursors past the rows indicated in the most recent call to next().
214      * This will only have an affect once per call to next().
215      */
incrementCursors()216     private void incrementCursors() {
217         if (mCompareResultIsValid) {
218             switch (mCompareResult) {
219                 case LEFT:
220                     mCursorLeft.moveToNext();
221                     break;
222                 case RIGHT:
223                     mCursorRight.moveToNext();
224                     break;
225                 case BOTH:
226                     mCursorLeft.moveToNext();
227                     mCursorRight.moveToNext();
228                     break;
229             }
230             mCompareResultIsValid = false;
231         }
232     }
233 
234     /**
235      * Compare the values. Values contains n pairs of strings. If all the pairs of strings match
236      * then returns 0. Otherwise returns the comparison result of the first non-matching pair
237      * of values, -1 if the first of the pair is less than the second of the pair or 1 if it
238      * is greater.
239      * @param values the n pairs of values to compare
240      * @return -1, 0, or 1 as described above.
241      */
compareStrings(String... values)242     private static int compareStrings(String... values) {
243         if ((values.length % 2) != 0) {
244             throw new IllegalArgumentException("you must specify an even number of values");
245         }
246 
247         for (int index = 0; index < values.length; index+=2) {
248             if (values[index] == null) {
249                 if (values[index+1] == null) continue;
250                 return -1;
251             }
252 
253             if (values[index+1] == null) {
254                 return 1;
255             }
256 
257             int comp = values[index].compareTo(values[index+1]);
258             if (comp != 0) {
259                 return comp < 0 ? -1 : 1;
260             }
261         }
262 
263         return 0;
264     }
265 }
266