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