/**
* (c) Copyright 2014 WibiData, Inc.
*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* 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 org.kiji.schema.impl.cassandra;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.kiji.annotations.ApiAudience;
import org.kiji.schema.EntityId;
import org.kiji.schema.KijiColumnName;
import org.kiji.schema.KijiDataRequest;
import org.kiji.schema.KijiDataRequestValidator;
import org.kiji.schema.KijiResult;
import org.kiji.schema.KijiRowData;
import org.kiji.schema.KijiRowScanner;
import org.kiji.schema.KijiTableReader;
import org.kiji.schema.KijiTableReaderBuilder;
import org.kiji.schema.KijiTableReaderBuilder.OnDecoderCacheMiss;
import org.kiji.schema.NoSuchColumnException;
import org.kiji.schema.SpecificCellDecoderFactory;
import org.kiji.schema.impl.BoundColumnReaderSpec;
import org.kiji.schema.impl.KijiResultRowData;
import org.kiji.schema.impl.KijiResultRowScanner;
import org.kiji.schema.impl.LayoutConsumer;
import org.kiji.schema.layout.CassandraColumnNameTranslator;
import org.kiji.schema.layout.CellSpec;
import org.kiji.schema.layout.ColumnReaderSpec;
import org.kiji.schema.layout.KijiTableLayout;
import org.kiji.schema.layout.impl.CellDecoderProvider;
import org.kiji.schema.util.DebugResourceTracker;
/**
* Reads from a kiji table by sending the requests directly to the C* tables.
*/
@ApiAudience.Private
public final class CassandraKijiTableReader implements KijiTableReader {
private static final Logger LOG = LoggerFactory.getLogger(CassandraKijiTableReader.class);
/** C* KijiTable to read from. */
private final CassandraKijiTable mTable;
/** Behavior when a cell decoder cannot be found. */
private final OnDecoderCacheMiss mOnDecoderCacheMiss;
/** States of a kiji table reader instance. */
private static enum State {
UNINITIALIZED,
OPEN,
CLOSED
}
/** Tracks the state of this KijiTableReader instance. */
private final AtomicReference<State> mState = new AtomicReference<State>(State.UNINITIALIZED);
/** Map of overridden CellSpecs to use when reading. Null when mOverrides is not null. */
private final Map<KijiColumnName, CellSpec> mCellSpecOverrides;
/** Map of overridden column read specifications. Null when mCellSpecOverrides is not null. */
private final Map<KijiColumnName, BoundColumnReaderSpec> mOverrides;
/** Map of backup column read specifications. Null when mCellSpecOverrides is not null. */
private final Collection<BoundColumnReaderSpec> mAlternatives;
/** Layout consumer registration resource. */
private final LayoutConsumer.Registration mLayoutConsumerRegistration;
/** Object which processes layout update from the KijiTable from which this Reader reads. */
private final InnerLayoutUpdater mInnerLayoutUpdater = new InnerLayoutUpdater();
/**
* Encapsulation of all table layout related state necessary for the operation of this reader.
* Can be hot swapped to reflect a table layout update.
*/
private ReaderLayoutCapsule mReaderLayoutCapsule = null;
/**
* Container class encapsulating all reader state which must be updated in response to a table
* layout update.
*/
private static final class ReaderLayoutCapsule {
private final CellDecoderProvider mCellDecoderProvider;
private final KijiTableLayout mLayout;
private final CassandraColumnNameTranslator mTranslator;
/**
* Default constructor.
*
* @param cellDecoderProvider the CellDecoderProvider to cache. This provider should reflect
* all overrides appropriate to this reader.
* @param layout the KijiTableLayout to cache.
* @param translator the ColumnNameTranslator to cache.
*/
private ReaderLayoutCapsule(
final CellDecoderProvider cellDecoderProvider,
final KijiTableLayout layout,
final CassandraColumnNameTranslator translator) {
mCellDecoderProvider = cellDecoderProvider;
mLayout = layout;
mTranslator = translator;
}
/**
* Get the column name translator for the current layout.
* @return the column name translator for the current layout.
*/
private CassandraColumnNameTranslator getColumnNameTranslator() {
return mTranslator;
}
/**
* Get the current table layout for the table to which this reader is associated.
* @return the current table layout for the table to which this reader is associated.
*/
private KijiTableLayout getLayout() {
return mLayout;
}
/**
* Get the CellDecoderProvider including CellSpec overrides for providing cell decoders for the
* current layout.
* @return the CellDecoderProvider including CellSpec overrides for providing cell decoders for
* the current layout.
*/
private CellDecoderProvider getCellDecoderProvider() {
return mCellDecoderProvider;
}
}
/** Provides for the updating of this Reader in response to a table layout update. */
private final class InnerLayoutUpdater implements LayoutConsumer {
/** {@inheritDoc} */
@Override
public void update(final KijiTableLayout layout) throws IOException {
final CellDecoderProvider provider;
if (null != mCellSpecOverrides) {
provider = CellDecoderProvider.create(
layout,
mTable.getKiji().getSchemaTable(),
SpecificCellDecoderFactory.get(),
mCellSpecOverrides);
} else {
provider = CellDecoderProvider.create(
layout,
mOverrides,
mAlternatives,
mOnDecoderCacheMiss);
}
if (mReaderLayoutCapsule != null) {
LOG.debug(
"Updating layout used by KijiTableReader: {} for table: {} from version: {} to: {}",
this,
mTable.getURI(),
mReaderLayoutCapsule.getLayout().getDesc().getLayoutId(),
layout.getDesc().getLayoutId());
} else {
// If the capsule is null this is the initial setup and we need a different log message.
LOG.debug(
"Initializing KijiTableReader: {} for table: {} with table layout version: {}",
this,
mTable.getURI(),
layout.getDesc().getLayoutId());
}
mReaderLayoutCapsule =
new ReaderLayoutCapsule(provider, layout, CassandraColumnNameTranslator.from(layout));
}
}
/**
* Creates a new {@code CassandraKijiTableReader} instance that sends the read requests
* directly to Cassandra.
*
* @param table Kiji table from which to read.
* @throws java.io.IOException on I/O error.
* @return a new CassandraKijiTableReader.
*/
public static CassandraKijiTableReader create(
final CassandraKijiTable table
) throws IOException {
return CassandraKijiTableReaderBuilder.create(table).buildAndOpen();
}
/**
* Creates a new CassandraKijiTableReader instance that sends read requests directly to Cassandra.
*
* @param table Kiji table from which to read.
* @param overrides layout overrides to modify read behavior.
* @return a new CassandraKijiTableReader.
* @throws java.io.IOException in case of an error opening the reader.
*/
public static CassandraKijiTableReader createWithCellSpecOverrides(
final CassandraKijiTable table,
final Map<KijiColumnName, CellSpec> overrides
) throws IOException {
return new CassandraKijiTableReader(table, overrides);
}
/**
* Creates a new CassandraKijiTableReader instance that sends read requests directly to Cassandra.
*
* @param table Kiji table from which to read.
* @param onDecoderCacheMiss behavior to use when a {@link
* org.kiji.schema.layout.ColumnReaderSpec} override specified in a {@link
* org.kiji.schema.KijiDataRequest} cannot be found in the prebuilt cache of cell decoders.
* @param overrides mapping from columns to overriding read behavior for those columns.
* @param alternatives mapping from columns to reader spec alternatives which the
* KijiTableReader will accept as overrides in data requests.
* @return a new CassandraKijiTableReader.
* @throws java.io.IOException in case of an error opening the reader.
*/
public static CassandraKijiTableReader createWithOptions(
final CassandraKijiTable table,
final OnDecoderCacheMiss onDecoderCacheMiss,
final Map<KijiColumnName, ColumnReaderSpec> overrides,
final Multimap<KijiColumnName, ColumnReaderSpec> alternatives
) throws IOException {
return new CassandraKijiTableReader(table, onDecoderCacheMiss, overrides, alternatives);
}
/**
* Open a table reader whose behavior is customized by overriding CellSpecs.
*
* @param table Kiji table from which this reader will read.
* @param cellSpecOverrides specifications of overriding read behaviors.
* @throws java.io.IOException in case of an error opening the reader.
*/
private CassandraKijiTableReader(
final CassandraKijiTable table,
final Map<KijiColumnName, CellSpec> cellSpecOverrides
) throws IOException {
mTable = table;
mCellSpecOverrides = cellSpecOverrides;
mOnDecoderCacheMiss = KijiTableReaderBuilder.DEFAULT_CACHE_MISS;
mOverrides = null;
mAlternatives = null;
mLayoutConsumerRegistration = mTable.registerLayoutConsumer(mInnerLayoutUpdater);
Preconditions.checkState(mReaderLayoutCapsule != null,
"KijiTableReader for table: %s failed to initialize.", mTable.getURI());
// Retain the table only when everything succeeds.
mTable.retain();
final State oldState = mState.getAndSet(State.OPEN);
Preconditions.checkState(oldState == State.UNINITIALIZED,
"Cannot open KijiTableReader instance in state %s.", oldState);
DebugResourceTracker.get().registerResource(this);
}
/**
* Creates a new CassandraKijiTableReader instance that sends read requests directly to Cassandra.
*
* @param table Kiji table from which to read.
* @param onDecoderCacheMiss behavior to use when a {@link
* org.kiji.schema.layout.ColumnReaderSpec} override specified in a {@link
* org.kiji.schema.KijiDataRequest} cannot be found in the prebuilt cache of cell decoders.
* @param overrides mapping from columns to overriding read behavior for those columns.
* @param alternatives mapping from columns to reader spec alternatives which the
* KijiTableReader will accept as overrides in data requests.
* @throws java.io.IOException on I/O error.
*/
private CassandraKijiTableReader(
final CassandraKijiTable table,
final OnDecoderCacheMiss onDecoderCacheMiss,
final Map<KijiColumnName, ColumnReaderSpec> overrides,
final Multimap<KijiColumnName, ColumnReaderSpec> alternatives
) throws IOException {
mTable = table;
mOnDecoderCacheMiss = onDecoderCacheMiss;
final KijiTableLayout layout = mTable.getLayout();
final Set<KijiColumnName> layoutColumns = layout.getColumnNames();
final Map<KijiColumnName, BoundColumnReaderSpec> boundOverrides = Maps.newHashMap();
for (Map.Entry<KijiColumnName, ColumnReaderSpec> override
: overrides.entrySet()) {
final KijiColumnName column = override.getKey();
if (!layoutColumns.contains(column)
&& !layoutColumns.contains(new KijiColumnName(column.getFamily()))) {
throw new NoSuchColumnException(String.format(
"KijiTableLayout: %s does not contain column: %s", layout, column));
} else {
boundOverrides.put(column,
BoundColumnReaderSpec.create(override.getValue(), column));
}
}
mOverrides = boundOverrides;
final Collection<BoundColumnReaderSpec> boundAlternatives = Sets.newHashSet();
for (Map.Entry<KijiColumnName, ColumnReaderSpec> altsEntry
: alternatives.entries()) {
final KijiColumnName column = altsEntry.getKey();
if (!layoutColumns.contains(column)
&& !layoutColumns.contains(KijiColumnName.create(column.getFamily()))) {
throw new NoSuchColumnException(String.format(
"KijiTableLayout: %s does not contain column: %s", layout, column));
} else {
boundAlternatives.add(
BoundColumnReaderSpec.create(altsEntry.getValue(), altsEntry.getKey()));
}
}
mAlternatives = boundAlternatives;
mCellSpecOverrides = null;
mLayoutConsumerRegistration = mTable.registerLayoutConsumer(mInnerLayoutUpdater);
Preconditions.checkState(mReaderLayoutCapsule != null,
"KijiTableReader for table: %s failed to initialize.", mTable.getURI());
// Retain the table only when everything succeeds.
mTable.retain();
final State oldState = mState.getAndSet(State.OPEN);
Preconditions.checkState(oldState == State.UNINITIALIZED,
"Cannot open KijiTableReader instance in state %s.", oldState);
DebugResourceTracker.get().registerResource(this);
}
/** {@inheritDoc} */
@Override
public KijiRowData get(
final EntityId entityId,
final KijiDataRequest dataRequest
) throws IOException {
return new KijiResultRowData(
mReaderLayoutCapsule.getLayout(),
getResult(entityId, dataRequest));
}
/**
* Get a KijiResult for the given EntityId and data request.
*
* <p>
* This method allows the caller to specify a type-bound on the values of the {@code KijiCell}s
* of the returned {@code KijiResult}. The caller should be careful to only specify an
* appropriate type. If the type is too specific (or wrong), a runtime
* {@link java.lang.ClassCastException} will be thrown when the returned {@code KijiResult} is
* used. See the 'Type Safety' section of {@link KijiResult}'s documentation for more details.
* </p>
*
* @param entityId EntityId of the row from which to get data.
* @param dataRequest Specification of the data to get from the given row.
* @param <T> type {@code KijiCell} value returned by the {@code KijiResult}.
* @return a new KijiResult for the given EntityId and data request.
* @throws IOException in case of an error getting the data.
*/
public <T> KijiResult<T> getResult(
final EntityId entityId,
final KijiDataRequest dataRequest
) throws IOException {
final State state = mState.get();
Preconditions.checkState(state == State.OPEN,
"Cannot get row from KijiTableReader instance %s in state %s.", this, state);
final ReaderLayoutCapsule capsule = mReaderLayoutCapsule;
// Make sure the request validates against the layout of the table.
final KijiTableLayout tableLayout = capsule.getLayout();
validateRequestAgainstLayout(dataRequest, tableLayout);
return CassandraKijiResult.create(
entityId,
dataRequest,
mTable,
tableLayout,
capsule.getColumnNameTranslator(),
capsule.getCellDecoderProvider());
}
/** {@inheritDoc} */
@Override
public List<KijiRowData> bulkGet(
final List<EntityId> entityIds,
final KijiDataRequest dataRequest
) throws IOException {
// TODO(SCHEMA-981): make this use async requests
final State state = mState.get();
Preconditions.checkState(state == State.OPEN,
"Cannot get rows from KijiTableReader instance %s in state %s.", this, state);
List<KijiRowData> data = Lists.newArrayList();
for (EntityId eid : entityIds) {
data.add(get(eid, dataRequest));
}
return data;
}
/** {@inheritDoc} */
@Override
public KijiRowScanner getScanner(final KijiDataRequest dataRequest) throws IOException {
return getScannerWithOptions(dataRequest, CassandraKijiScannerOptions.withoutBounds());
}
/** {@inheritDoc} */
@Override
public KijiRowScanner getScanner(
final KijiDataRequest dataRequest,
final KijiScannerOptions kijiScannerOptions
) throws IOException {
throw new UnsupportedOperationException(
"Cassandra Kiji cannot use KijiScannerOptions"
);
}
/**
* Returns a new RowScanner configured with Cassandra-specific scanner options.
*
* @param request for the scan.
* @param kijiScannerOptions Cassandra-specific scan options.
* @return A new row scanner.
* @throws IOException if there is a problem creating the row scanner.
*/
public KijiRowScanner getScannerWithOptions(
final KijiDataRequest request,
final CassandraKijiScannerOptions kijiScannerOptions
) throws IOException {
return new KijiResultRowScanner(
mReaderLayoutCapsule.getLayout(),
getKijiResultScanner(request, kijiScannerOptions));
}
/**
* Get a KijiResultScanner for the given data request and scan options.
*
* <p>
* This method allows the caller to specify a type-bound on the values of the {@code KijiCell}s
* of the returned {@code KijiResult}s. The caller should be careful to only specify an
* appropriate type. If the type is too specific (or wrong), a runtime
* {@link java.lang.ClassCastException} will be thrown when the returned {@code KijiResult} is
* used. See the 'Type Safety' section of {@code KijiResult}'s documentation for more details.
* </p>
*
* @param request Data request defining the data to retrieve from each row.
* @param scannerOptions Options to control the operation of the scanner.
* @param <T> type {@code KijiCell} value returned by the {@code KijiResult}.
* @return A new KijiResultScanner.
* @throws IOException in case of an error creating the scanner.
*/
public <T> CassandraKijiResultScanner<T> getKijiResultScanner(
final KijiDataRequest request,
final CassandraKijiScannerOptions scannerOptions
) throws IOException {
final State state = mState.get();
Preconditions.checkState(state == State.OPEN,
"Cannot get scanner from KijiTableReader instance %s in state %s.", this, state);
final ReaderLayoutCapsule capsule = mReaderLayoutCapsule;
// Make sure the request validates against the layout of the table.
final KijiTableLayout layout = capsule.getLayout();
validateRequestAgainstLayout(request, layout);
return new CassandraKijiResultScanner<T>(
request,
scannerOptions,
mTable,
layout,
capsule.getCellDecoderProvider(),
capsule.getColumnNameTranslator());
}
/** {@inheritDoc} */
@Override
public String toString() {
return Objects.toStringHelper(CassandraKijiTableReader.class)
.add("id", System.identityHashCode(this))
.add("table", mTable.getURI())
.add("layout-version", mReaderLayoutCapsule.getLayout().getDesc().getLayoutId())
.add("state", mState.get())
.toString();
}
/**
* Validate a data request against a table layout.
*
* @param dataRequest A KijiDataRequest.
* @param layout the KijiTableLayout of the table against which to validate the data request.
*/
private void validateRequestAgainstLayout(KijiDataRequest dataRequest, KijiTableLayout layout) {
// TODO(SCHEMA-263): This could be made more efficient if the layout and/or validator were
// cached.
KijiDataRequestValidator.validatorForLayout(layout).validate(dataRequest);
}
/** {@inheritDoc} */
@Override
public void close() throws IOException {
final State oldState = mState.getAndSet(State.CLOSED);
Preconditions.checkState(oldState == State.OPEN,
"Cannot close KijiTableReader instance %s in state %s.", this, oldState);
mLayoutConsumerRegistration.close();
mTable.release();
DebugResourceTracker.get().unregisterResource(this);
}
}