content: /*
   * Copyright (C) 2008 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.database;
  
  import java.util.Iterator;
  
  /**
   * Does a join on two cursors using the specified columns. The cursors must already
   * be sorted on each of the specified columns in ascending order. This joiner only
   * supports the case where the tuple of key column values is unique.
   * <p>
   * Typical usage:
   *
   * <pre>
   * CursorJoiner joiner = new CursorJoiner(cursorA, keyColumnsofA, cursorB, keyColumnsofB);
   * for (CursorJoiner.Result joinerResult : joiner) {
   *     switch (joinerResult) {
   *         case LEFT:
   *             // handle case where a row in cursorA is unique
   *             break;
   *         case RIGHT:
   *             // handle case where a row in cursorB is unique
   *             break;
   *         case BOTH:
   *             // handle case where a row with the same key is in both cursors
   *             break;
   *     }
   * }
   * </pre>
   */
  public final class CursorJoiner
          implements Iterator<CursorJoiner.Result>, Iterable<CursorJoiner.Result> {
      private Cursor mCursorLeft;
      private Cursor mCursorRight;
      private boolean mCompareResultIsValid;
      private Result mCompareResult;
      private int[] mColumnsLeft;
      private int[] mColumnsRight;
      private String[] mValues;
  
      /**
       * The result of a call to next().
       */
      public enum Result {
          /** The row currently pointed to by the left cursor is unique */
          RIGHT,
          /** The row currently pointed to by the right cursor is unique */
          LEFT,
          /** The rows pointed to by both cursors are the same */
          BOTH
      }
  
      /**
       * Initializes the CursorJoiner and resets the cursors to the first row. The left and right
       * column name arrays must have the same number of columns.
       * @param cursorLeft The left cursor to compare
       * @param columnNamesLeft The column names to compare from the left cursor
       * @param cursorRight The right cursor to compare
       * @param columnNamesRight The column names to compare from the right cursor
       */
      public CursorJoiner(
              Cursor cursorLeft, String[] columnNamesLeft,
              Cursor cursorRight, String[] columnNamesRight) {
          if (columnNamesLeft.length != columnNamesRight.length) {
              throw new IllegalArgumentException(
                      "you must have the same number of columns on the left and right, "
                              + columnNamesLeft.length + " != " + columnNamesRight.length);
          }
  
          mCursorLeft = cursorLeft;
          mCursorRight = cursorRight;
  
          mCursorLeft.moveToFirst();
          mCursorRight.moveToFirst();
  
          mCompareResultIsValid = false;
  
          mColumnsLeft = buildColumnIndiciesArray(cursorLeft, columnNamesLeft);
          mColumnsRight = buildColumnIndiciesArray(cursorRight, columnNamesRight);
  
          mValues = new String[mColumnsLeft.length * 2];
      }
  
      public Iterator<Result> iterator() {
          return this;
      }
  
      /**
       * Lookup the indicies of the each column name and return them in an array.
       * @param cursor the cursor that contains the columns
       * @param columnNames the array of names to lookup
       * @return an array of column indices
       */
      private int[] buildColumnIndiciesArray(Cursor cursor, String[] columnNames) {
          int[] columns = new int[columnNames.length];
          for (int i = 0; i < columnNames.length; i++) {
              columns[i] = cursor.getColumnIndexOrThrow(columnNames[i]);
          }
          return columns;
      }
  
      /**
       * Returns whether or not there are more rows to compare using next().
       * @return true if there are more rows to compare
       */
      public boolean hasNext() {
          if (mCompareResultIsValid) {
              switch (mCompareResult) {
                  case BOTH:
                      return !mCursorLeft.isLast() || !mCursorRight.isLast();
  
                  case LEFT:
                      return !mCursorLeft.isLast() || !mCursorRight.isAfterLast();
  
                  case RIGHT:
                      return !mCursorLeft.isAfterLast() || !mCursorRight.isLast();
  
                  default:
                      throw new IllegalStateException("bad value for mCompareResult, "
                              + mCompareResult);
              }
          } else {
              return !mCursorLeft.isAfterLast() || !mCursorRight.isAfterLast();
          }
      }
  
      /**
       * Returns the comparison result of the next row from each cursor. If one cursor
       * has no more rows but the other does then subsequent calls to this will indicate that
       * the remaining rows are unique.
       * <p>
       * The caller must check that hasNext() returns true before calling this.
       * <p>
       * Once next() has been called the cursors specified in the result of the call to
       * next() are guaranteed to point to the row that was indicated. Reading values
       * from the cursor that was not indicated in the call to next() will result in
       * undefined behavior.
       * @return LEFT, if the row pointed to by the left cursor is unique, RIGHT
       *   if the row pointed to by the right cursor is unique, BOTH if the rows in both
       *   cursors are the same.
       */
      public Result next() {
          if (!hasNext()) {
              throw new IllegalStateException("you must only call next() when hasNext() is true");
          }
          incrementCursors();
          assert hasNext();
  
          boolean hasLeft = !mCursorLeft.isAfterLast();
          boolean hasRight = !mCursorRight.isAfterLast();
  
          if (hasLeft && hasRight) {
              populateValues(mValues, mCursorLeft, mColumnsLeft, 0 /* start filling at index 0 */);
              populateValues(mValues, mCursorRight, mColumnsRight, 1 /* start filling at index 1 */);
              switch (compareStrings(mValues)) {
                  case -1:
                      mCompareResult = Result.LEFT;
                      break;
                  case 0:
                      mCompareResult = Result.BOTH;
                      break;
                  case 1:
                      mCompareResult = Result.RIGHT;
                      break;
              }
          } else if (hasLeft) {
              mCompareResult = Result.LEFT;
          } else  {
              assert hasRight;
              mCompareResult = Result.RIGHT;
          }
          mCompareResultIsValid = true;
          return mCompareResult;
      }
  
      public void remove() {
          throw new UnsupportedOperationException("not implemented");
      }
  
      /**
       * Reads the strings from the cursor that are specifed in the columnIndicies
       * array and saves them in values beginning at startingIndex, skipping a slot
       * for each value. If columnIndicies has length 3 and startingIndex is 1, the
       * values will be stored in slots 1, 3, and 5.
       * @param values the String[] to populate
       * @param cursor the cursor from which to read
       * @param columnIndicies the indicies of the values to read from the cursor
       * @param startingIndex the slot in which to start storing values, and must be either 0 or 1.
       */
      private static void populateValues(String[] values, Cursor cursor, int[] columnIndicies,
              int startingIndex) {
          assert startingIndex == 0 || startingIndex == 1;
          for (int i = 0; i < columnIndicies.length; i++) {
              values[startingIndex + i*2] = cursor.getString(columnIndicies[i]);
          }
      }
  
      /**
       * Increment the cursors past the rows indicated in the most recent call to next().
       * This will only have an affect once per call to next().
       */
      private void incrementCursors() {
          if (mCompareResultIsValid) {
              switch (mCompareResult) {
                  case LEFT:
                      mCursorLeft.moveToNext();
                      break;
                  case RIGHT:
                      mCursorRight.moveToNext();
                      break;
                  case BOTH:
                      mCursorLeft.moveToNext();
                      mCursorRight.moveToNext();
                      break;
              }
              mCompareResultIsValid = false;
          }
      }
  
      /**
       * Compare the values. Values contains n pairs of strings. If all the pairs of strings match
       * then returns 0. Otherwise returns the comparison result of the first non-matching pair
       * of values, -1 if the first of the pair is less than the second of the pair or 1 if it
       * is greater.
       * @param values the n pairs of values to compare
       * @return -1, 0, or 1 as described above.
       */
      private static int compareStrings(String... values) {
          if ((values.length % 2) != 0) {
              throw new IllegalArgumentException("you must specify an even number of values");
          }
  
          for (int index = 0; index < values.length; index+=2) {
              if (values[index] == null) {
                  if (values[index+1] == null) continue;
                  return -1;
              }
  
              if (values[index+1] == null) {
                  return 1;
              }
  
              int comp = values[index].compareTo(values[index+1]);
              if (comp != 0) {
                  return comp < 0 ? -1 : 1;
              }
          }
  
          return 0;
      }
  }
  