Home | History | Annotate | Download | only in database
      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      */
     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 
     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      */
    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      */
    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      */
    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 
    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      */
    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      */
    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      */
    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