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