/*
* Copyright 2009 The Apache Software Foundation
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.hadoop.hbase.regionserver;
import java.util.ArrayList;
import java.util.List;
import java.util.NavigableSet;
import org.apache.hadoop.hbase.HConstants;
import org.apache.hadoop.hbase.util.Bytes;
/**
* This class is used for the tracking and enforcement of columns and numbers
* of versions during the course of a Get or Scan operation, when explicit
* column qualifiers have been asked for in the query.
*
* With a little magic (see {@link ScanQueryMatcher}), we can use this matcher
* for both scans and gets. The main difference is 'next' and 'done' collapse
* for the scan case (since we see all columns in order), and we only reset
* between rows.
*
* <p>
* This class is utilized by {@link ScanQueryMatcher} through two methods:
* <ul><li>{@link #checkColumn} is called when a Put satisfies all other
* conditions of the query. This method returns a {@link org.apache.hadoop.hbase.regionserver.ScanQueryMatcher.MatchCode} to define
* what action should be taken.
* <li>{@link #update} is called at the end of every StoreFile or memstore.
* <p>
* This class is NOT thread-safe as queries are never multi-threaded
*/
public class ExplicitColumnTracker implements ColumnTracker {
private final int maxVersions;
private final List<ColumnCount> columns;
private final List<ColumnCount> columnsToReuse;
private int index;
private ColumnCount column;
/** Keeps track of the latest timestamp included for current column.
* Used to eliminate duplicates. */
private long latestTSOfCurrentColumn;
/**
* Default constructor.
* @param columns columns specified user in query
* @param maxVersions maximum versions to return per column
*/
public ExplicitColumnTracker(NavigableSet<byte[]> columns, int maxVersions) {
this.maxVersions = maxVersions;
this.columns = new ArrayList<ColumnCount>(columns.size());
this.columnsToReuse = new ArrayList<ColumnCount>(columns.size());
for(byte [] column : columns) {
this.columnsToReuse.add(new ColumnCount(column,maxVersions));
}
reset();
}
/**
* Done when there are no more columns to match against.
*/
public boolean done() {
return this.columns.size() == 0;
}
public ColumnCount getColumnHint() {
return this.column;
}
/**
* Checks against the parameters of the query and the columns which have
* already been processed by this query.
* @param bytes KeyValue buffer
* @param offset offset to the start of the qualifier
* @param length length of the qualifier
* @param timestamp timestamp of the key being checked
* @return MatchCode telling ScanQueryMatcher what action to take
*/
public ScanQueryMatcher.MatchCode checkColumn(byte [] bytes, int offset,
int length, long timestamp) {
do {
// No more columns left, we are done with this query
if(this.columns.size() == 0) {
return ScanQueryMatcher.MatchCode.SEEK_NEXT_ROW; // done_row
}
// No more columns to match against, done with storefile
if(this.column == null) {
return ScanQueryMatcher.MatchCode.SEEK_NEXT_ROW; // done_row
}
// Compare specific column to current column
int ret = Bytes.compareTo(column.getBuffer(), column.getOffset(),
column.getLength(), bytes, offset, length);
// Column Matches. If it is not a duplicate key, decrement versions left
// and include.
if(ret == 0) {
//If column matches, check if it is a duplicate timestamp
if (sameAsPreviousTS(timestamp)) {
//If duplicate, skip this Key
return ScanQueryMatcher.MatchCode.SKIP;
}
if(this.column.decrement() == 0) {
// Done with versions for this column
this.columns.remove(this.index);
resetTS();
if(this.columns.size() == this.index) {
// Will not hit any more columns in this storefile
this.column = null;
} else {
this.column = this.columns.get(this.index);
}
} else {
setTS(timestamp);
}
return ScanQueryMatcher.MatchCode.INCLUDE;
}
resetTS();
if (ret > 0) {
// Specified column is smaller than the current, skip to next column.
return ScanQueryMatcher.MatchCode.SEEK_NEXT_COL;
}
// Specified column is bigger than current column
// Move down current column and check again
if(ret <= -1) {
if(++this.index >= this.columns.size()) {
// No more to match, do not include, done with storefile
return ScanQueryMatcher.MatchCode.SEEK_NEXT_ROW; // done_row
}
// This is the recursive case.
this.column = this.columns.get(this.index);
}
} while(true);
}
/**
* Called at the end of every StoreFile or memstore.
*/
public void update() {
if(this.columns.size() != 0) {
this.index = 0;
this.column = this.columns.get(this.index);
} else {
this.index = -1;
this.column = null;
}
}
// Called between every row.
public void reset() {
buildColumnList();
this.index = 0;
this.column = this.columns.get(this.index);
resetTS();
}
private void resetTS() {
latestTSOfCurrentColumn = HConstants.LATEST_TIMESTAMP;
}
private void setTS(long timestamp) {
latestTSOfCurrentColumn = timestamp;
}
private boolean sameAsPreviousTS(long timestamp) {
return timestamp == latestTSOfCurrentColumn;
}
private void buildColumnList() {
this.columns.clear();
this.columns.addAll(this.columnsToReuse);
for(ColumnCount col : this.columns) {
col.setCount(this.maxVersions);
}
}
/**
* This method is used to inform the column tracker that we are done with
* this column. We may get this information from external filters or
* timestamp range and we then need to indicate this information to
* tracker. It is required only in case of ExplicitColumnTracker.
* @param bytes
* @param offset
* @param length
*/
public void doneWithColumn(byte [] bytes, int offset, int length) {
while (this.column != null) {
int compare = Bytes.compareTo(column.getBuffer(), column.getOffset(),
column.getLength(), bytes, offset, length);
if (compare == 0) {
this.columns.remove(this.index);
if (this.columns.size() == this.index) {
// Will not hit any more columns in this storefile
this.column = null;
} else {
this.column = this.columns.get(this.index);
}
return;
} else if ( compare <= -1) {
if(++this.index != this.columns.size()) {
this.column = this.columns.get(this.index);
} else {
this.column = null;
}
} else {
return;
}
}
}
}