/*
* Copyright © 2014 Cask Data, Inc.
*
* 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 co.cask.cdap.data2.dataset2;
import co.cask.cdap.api.dataset.Dataset;
import co.cask.cdap.api.dataset.DatasetContext;
import co.cask.cdap.api.dataset.DatasetDefinition;
import co.cask.cdap.api.dataset.DatasetSpecification;
import co.cask.cdap.api.dataset.lib.CompositeDatasetDefinition;
import co.cask.cdap.api.dataset.module.DatasetDefinitionRegistry;
import co.cask.cdap.api.dataset.module.DatasetModule;
import co.cask.cdap.api.dataset.module.DatasetType;
import co.cask.cdap.api.dataset.module.EmbeddedDataset;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Throwables;
import com.google.common.collect.Maps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.util.Map;
/**
* Wraps implementation of {@link Dataset} into a {@link DatasetModule}.
*
* This allows for easier implementation of simple datasets without requiring to implement {@link DatasetDefinition},
* {@link co.cask.cdap.api.dataset.DatasetAdmin}, etc. when the implementation uses existing dataset types.
*
* NOTE: all admin ops of the dataset will be delegated to embedded datasets;
* {@link co.cask.cdap.api.dataset.DatasetProperties} will be propagated to embedded datasets as well
*
* NOTE: must have exactly one constructor with parameter types of
* (DatasetSpecification, [0..n] @EmbeddedDataset Dataset)
*
* <p>
* Example of usage:
*
* <pre>
* {@code
// ...
datasetFramework.addModule("myModule", new SingleTypeModule(SimpleKVTable.class));
// ...
@DatasetType("KVTable")
public class SimpleKVTable extends AbstractDataset implements KeyValueTable {
private static final byte[] COL = new byte[0];
private final Table table;
public SimpleKVTable(DatasetSpecification spec,
@EmbeddedDataset("data") Table table) {
super(spec.getTransactionAwareName(), table);
this.table = table;
}
public void put(String key, String value) throws Exception {
table.put(Bytes.toBytes(key), COL, Bytes.toBytes(value));
}
public String get(String key) throws Exception {
byte[] value = table.get(Bytes.toBytes(key), COL);
return value == null ? null : Bytes.toString(value);
}
}
* }
* </pre>
*
* See {@link DatasetType} and {@link EmbeddedDataset} for more details on their usage.
*
*/
public class SingleTypeModule implements DatasetModule {
private static final Logger LOG = LoggerFactory.getLogger(SingleTypeModule.class);
private final Class<? extends Dataset> dataSetClass;
public SingleTypeModule(Class<? extends Dataset> dataSetClass) {
this.dataSetClass = dataSetClass;
}
public Class<? extends Dataset> getDataSetClass() {
return dataSetClass;
}
@Override
public void register(DatasetDefinitionRegistry registry) {
final Constructor ctor = findSuitableCtorOrFail(dataSetClass);
DatasetType typeAnn = dataSetClass.getAnnotation(DatasetType.class);
// default type name to dataset class name
String typeName = typeAnn != null ? typeAnn.value() : dataSetClass.getName();
final Map<String, DatasetDefinition> defs = Maps.newHashMap();
Class<?>[] paramTypes = ctor.getParameterTypes();
Annotation[][] paramAnns = ctor.getParameterAnnotations();
// computing parameters for dataset constructor:
// if param is of type DatasetSpecification we'll need to set spec as a value
// if param has EmbeddedDataset annotation we need to set instance of embedded dataset as a value
final DatasetCtorParam[] ctorParams = new DatasetCtorParam[paramTypes.length];
for (int i = 0; i < paramTypes.length; i++) {
if (DatasetSpecification.class.isAssignableFrom(paramTypes[i])) {
ctorParams[i] = new DatasetSpecificationParam();
continue;
}
for (Annotation ann : paramAnns[i]) {
if (ann instanceof EmbeddedDataset) {
String type = ((EmbeddedDataset) ann).type();
if (EmbeddedDataset.DEFAULT_TYPE_NAME.equals(type)) {
// default to dataset class name
type = paramTypes[i].getName();
}
DatasetDefinition def = registry.get(type);
if (def == null) {
String msg = String.format("Unknown data set type used with @Dataset: " + type);
LOG.error(msg);
throw new IllegalStateException(msg);
}
defs.put(((EmbeddedDataset) ann).value(), def);
ctorParams[i] = new DatasetParam(((EmbeddedDataset) ann).value());
break;
}
}
}
CompositeDatasetDefinition<Dataset> def = new CompositeDatasetDefinition<Dataset>(typeName, defs) {
@Override
public Dataset getDataset(DatasetContext datasetContext, DatasetSpecification spec,
Map<String, String> arguments, ClassLoader classLoader) throws IOException {
Object[] params = new Object[ctorParams.length];
for (int i = 0; i < ctorParams.length; i++) {
params[i] = ctorParams[i] != null ?
ctorParams[i].getValue(datasetContext, defs, spec, arguments, classLoader) :
null;
}
try {
return (Dataset) ctor.newInstance(params);
} catch (Exception e) {
throw Throwables.propagate(e);
}
}
};
registry.add(def);
}
@VisibleForTesting
static Constructor findSuitableCtorOrFail(Class<? extends Dataset> dataSetClass) {
Constructor[] ctors = dataSetClass.getConstructors();
Constructor suitableCtor = null;
for (Constructor ctor : ctors) {
Class<?>[] paramTypes = ctor.getParameterTypes();
Annotation[][] paramAnns = ctor.getParameterAnnotations();
boolean firstParamIsSpec = paramTypes.length > 0 &&
DatasetSpecification.class.isAssignableFrom(paramTypes[0]);
if (firstParamIsSpec) {
boolean otherParamsAreDatasets = true;
// checking type of the param
for (int i = 1; i < paramTypes.length; i++) {
if (!Dataset.class.isAssignableFrom(paramTypes[i])) {
otherParamsAreDatasets = false;
break;
}
// checking that annotation is there
boolean hasAnnotation = false;
for (Annotation ann : paramAnns[i]) {
if (ann instanceof EmbeddedDataset) {
hasAnnotation = true;
break;
}
}
if (!hasAnnotation) {
otherParamsAreDatasets = false;
}
}
if (otherParamsAreDatasets) {
if (suitableCtor != null) {
throw new IllegalArgumentException(
String.format("Dataset class %s must have single constructor with parameter types of" +
" (DatasetSpecification, [0..n] @EmbeddedDataset Dataset) ", dataSetClass));
}
suitableCtor = ctor;
}
}
}
if (suitableCtor == null) {
throw new IllegalArgumentException(
String.format("Dataset class %s must have single constructor with parameter types of" +
" (DatasetSpecification, [0..n] @EmbeddedDataset Dataset) ", dataSetClass));
}
return suitableCtor;
}
private interface DatasetCtorParam {
Object getValue(DatasetContext datasetContext, Map<String, DatasetDefinition> defs, DatasetSpecification spec,
Map<String, String> arguments, ClassLoader cl) throws IOException;
}
private static final class DatasetSpecificationParam implements DatasetCtorParam {
@Override
public Object getValue(DatasetContext datasetContext, Map<String, DatasetDefinition> defs,
DatasetSpecification spec, Map<String, String> arguments, ClassLoader cl) {
return spec;
}
}
private static final class DatasetParam implements DatasetCtorParam {
private final String name;
private DatasetParam(String name) {
this.name = name;
}
@Override
public Object getValue(DatasetContext datasetContext, Map<String, DatasetDefinition> defs,
DatasetSpecification spec, Map<String, String> arguments,
ClassLoader cl) throws IOException {
return defs.get(name).getDataset(datasetContext, spec.getSpecification(name), arguments, cl);
}
}
}