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