001/*
002 *
003 * Licensed to the Apache Software Foundation (ASF) under one
004 * or more contributor license agreements.  See the NOTICE file
005 * distributed with this work for additional information
006 * regarding copyright ownership.  The ASF licenses this file
007 * to you under the Apache License, Version 2.0 (the
008 * "License"); you may not use this file except in compliance
009 * with the License.  You may obtain a copy of the License at
010 *
011 *     http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 */
019package org.apache.hadoop.hbase.filter;
020
021import java.io.IOException;
022import java.util.ArrayList;
023
024import org.apache.hadoop.hbase.Cell;
025import org.apache.hadoop.hbase.CellUtil;
026import org.apache.hadoop.hbase.PrivateCellUtil;
027import org.apache.yetus.audience.InterfaceAudience;
028import org.apache.hadoop.hbase.exceptions.DeserializationException;
029import org.apache.hadoop.hbase.shaded.protobuf.generated.FilterProtos;
030import org.apache.hadoop.hbase.util.Bytes;
031
032import org.apache.hbase.thirdparty.com.google.common.base.Preconditions;
033import org.apache.hbase.thirdparty.com.google.protobuf.InvalidProtocolBufferException;
034import org.apache.hbase.thirdparty.com.google.protobuf.UnsafeByteOperations;
035
036/**
037 * A filter, based on the ColumnCountGetFilter, takes two arguments: limit and offset.
038 * This filter can be used for row-based indexing, where references to other tables are stored across many columns,
039 * in order to efficient lookups and paginated results for end users. Only most recent versions are considered
040 * for pagination.
041 */
042@InterfaceAudience.Public
043public class ColumnPaginationFilter extends FilterBase {
044
045  private int limit = 0;
046  private int offset = -1;
047  private byte[] columnOffset = null;
048  private int count = 0;
049
050  /**
051   * Initializes filter with an integer offset and limit. The offset is arrived at
052   * scanning sequentially and skipping entries. @limit number of columns are
053   * then retrieved. If multiple column families are involved, the columns may be spread
054   * across them.
055   *
056   * @param limit Max number of columns to return.
057   * @param offset The integer offset where to start pagination.
058   */
059  public ColumnPaginationFilter(final int limit, final int offset)
060  {
061    Preconditions.checkArgument(limit >= 0, "limit must be positive %s", limit);
062    Preconditions.checkArgument(offset >= 0, "offset must be positive %s", offset);
063    this.limit = limit;
064    this.offset = offset;
065  }
066
067  /**
068   * Initializes filter with a string/bookmark based offset and limit. The offset is arrived
069   * at, by seeking to it using scanner hints. If multiple column families are involved,
070   * pagination starts at the first column family which contains @columnOffset. Columns are
071   * then retrieved sequentially upto @limit number of columns which maybe spread across
072   * multiple column families, depending on how the scan is setup.
073   *
074   * @param limit Max number of columns to return.
075   * @param columnOffset The string/bookmark offset on where to start pagination.
076   */
077  public ColumnPaginationFilter(final int limit, final byte[] columnOffset) {
078    Preconditions.checkArgument(limit >= 0, "limit must be positive %s", limit);
079    Preconditions.checkArgument(columnOffset != null,
080                                "columnOffset must be non-null %s",
081                                columnOffset);
082    this.limit = limit;
083    this.columnOffset = columnOffset;
084  }
085
086  /**
087   * @return limit
088   */
089  public int getLimit() {
090    return limit;
091  }
092
093  /**
094   * @return offset
095   */
096  public int getOffset() {
097    return offset;
098  }
099
100  /**
101   * @return columnOffset
102   */
103  public byte[] getColumnOffset() {
104    return columnOffset;
105  }
106
107  @Override
108  public boolean filterRowKey(Cell cell) throws IOException {
109    // Impl in FilterBase might do unnecessary copy for Off heap backed Cells.
110    return false;
111  }
112
113  @Override
114  @Deprecated
115  public ReturnCode filterKeyValue(final Cell c) {
116    return filterCell(c);
117  }
118
119  @Override
120  public ReturnCode filterCell(final Cell c)
121  {
122    if (columnOffset != null) {
123      if (count >= limit) {
124        return ReturnCode.NEXT_ROW;
125      }
126      int cmp = 0;
127      // Only compare if no KV's have been seen so far.
128      if (count == 0) {
129        cmp = CellUtil.compareQualifiers(c, this.columnOffset, 0, this.columnOffset.length);
130      }
131      if (cmp < 0) {
132        return ReturnCode.SEEK_NEXT_USING_HINT;
133      } else {
134        count++;
135        return ReturnCode.INCLUDE_AND_NEXT_COL;
136      }
137    } else {
138      if (count >= offset + limit) {
139        return ReturnCode.NEXT_ROW;
140      }
141
142      ReturnCode code = count < offset ? ReturnCode.NEXT_COL :
143                                         ReturnCode.INCLUDE_AND_NEXT_COL;
144      count++;
145      return code;
146    }
147  }
148
149  @Override
150  public Cell getNextCellHint(Cell cell) {
151    return PrivateCellUtil.createFirstOnRowCol(cell, columnOffset, 0, columnOffset.length);
152  }
153
154  @Override
155  public void reset()
156  {
157    this.count = 0;
158  }
159
160  public static Filter createFilterFromArguments(ArrayList<byte []> filterArguments) {
161    Preconditions.checkArgument(filterArguments.size() == 2,
162                                "Expected 2 but got: %s", filterArguments.size());
163    int limit = ParseFilter.convertByteArrayToInt(filterArguments.get(0));
164    int offset = ParseFilter.convertByteArrayToInt(filterArguments.get(1));
165    return new ColumnPaginationFilter(limit, offset);
166  }
167
168  /**
169   * @return The filter serialized using pb
170   */
171  @Override
172  public byte [] toByteArray() {
173    FilterProtos.ColumnPaginationFilter.Builder builder =
174      FilterProtos.ColumnPaginationFilter.newBuilder();
175    builder.setLimit(this.limit);
176    if (this.offset >= 0) {
177      builder.setOffset(this.offset);
178    }
179    if (this.columnOffset != null) {
180      builder.setColumnOffset(UnsafeByteOperations.unsafeWrap(this.columnOffset));
181    }
182    return builder.build().toByteArray();
183  }
184
185  /**
186   * @param pbBytes A pb serialized {@link ColumnPaginationFilter} instance
187   * @return An instance of {@link ColumnPaginationFilter} made from <code>bytes</code>
188   * @throws DeserializationException
189   * @see #toByteArray
190   */
191  public static ColumnPaginationFilter parseFrom(final byte [] pbBytes)
192  throws DeserializationException {
193    FilterProtos.ColumnPaginationFilter proto;
194    try {
195      proto = FilterProtos.ColumnPaginationFilter.parseFrom(pbBytes);
196    } catch (InvalidProtocolBufferException e) {
197      throw new DeserializationException(e);
198    }
199    if (proto.hasColumnOffset()) {
200      return new ColumnPaginationFilter(proto.getLimit(),
201                                        proto.getColumnOffset().toByteArray());
202    }
203    return new ColumnPaginationFilter(proto.getLimit(),proto.getOffset());
204  }
205
206  /**
207   * @param o the other filter to compare with
208   * @return true if and only if the fields of the filter that are serialized
209   * are equal to the corresponding fields in other.  Used for testing.
210   */
211  @Override
212  boolean areSerializedFieldsEqual(Filter o) {
213    if (o == this) return true;
214    if (!(o instanceof ColumnPaginationFilter)) return false;
215
216    ColumnPaginationFilter other = (ColumnPaginationFilter)o;
217    if (this.columnOffset != null) {
218      return this.getLimit() == other.getLimit() &&
219          Bytes.equals(this.getColumnOffset(), other.getColumnOffset());
220    }
221    return this.getLimit() == other.getLimit() && this.getOffset() == other.getOffset();
222  }
223
224  @Override
225  public String toString() {
226    if (this.columnOffset != null) {
227      return (this.getClass().getSimpleName() + "(" + this.limit + ", " +
228          Bytes.toStringBinary(this.columnOffset) + ")");
229    }
230    return String.format("%s (%d, %d)", this.getClass().getSimpleName(),
231        this.limit, this.offset);
232  }
233}