/*
* 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.api.dataset;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.SortedMap;
import java.util.TreeMap;
import javax.annotation.Nullable;
/**
* A {@link DatasetSpecification} is a hierarchical meta data object that contains all
* meta data needed to instantiate a dataset at runtime. It is hierarchical
* because it also contains the specification for any underlying datasets that
* are used in the implementation of the dataset. {@link DatasetSpecification}
* consists of:
* <li>fixed fields such as the dataset instance name and the dataset type name</li>
* <li>custom string properties that vary from dataset to dataset,
* and that the dataset implementation depends on</li>
* <li>a {@link DatasetSpecification} for each underlying dataset. For instance,
* if a dataset implements an indexed table using two base Tables,
* one for the data and one for the index, then these two tables have
* their own spec, which must be carried along with the spec for the
* indexed table.</li>
* {@link DatasetSpecification} uses a builder pattern for construction.
*/
public final class DatasetSpecification {
// the name of the dataset
private final String name;
// the name of the type of the dataset
private final String type;
// description of dataset
private final String description;
// the properties of the dataset as passed in when the dataset was created or reconfigured
private final Map<String, String> originalProperties;
// the custom properties of the dataset.
// NOTE: we need the map to be ordered because we compare serialized to JSON form as Strings during deploy validation
private final SortedMap<String, String> properties;
// the meta data for embedded datasets
// NOTE: we need the map to be ordered because we compare serialized to JSON form as Strings during deploy validation
private final SortedMap<String, DatasetSpecification> datasetSpecs;
public static Builder builder(String name, String typeName) {
return new Builder(name, typeName);
}
/**
* Private constructor, only to be used by the builder.
* @param name the name of the dataset
* @param type the type of the dataset
* @param properties the custom properties
* @param datasetSpecs the specs of embedded datasets
*/
private DatasetSpecification(String name,
String type,
@Nullable String description,
SortedMap<String, String> properties,
SortedMap<String, DatasetSpecification> datasetSpecs) {
this(name, type, description, null, properties, datasetSpecs);
}
/**
* Private constructor, only to be used by static method setOriginalProperties.
* @param name the name of the dataset
* @param type the type of the dataset
* @param description the description of dataset
* @param properties the custom properties
* @param datasetSpecs the specs of embedded datasets
*/
private DatasetSpecification(String name,
String type,
@Nullable String description,
@Nullable Map<String, String> originalProperties,
SortedMap<String, String> properties,
SortedMap<String, DatasetSpecification> datasetSpecs) {
this.name = name;
this.type = type;
this.description = description;
this.properties = Collections.unmodifiableSortedMap(new TreeMap<>(properties));
this.originalProperties = originalProperties == null ? null :
Collections.unmodifiableMap(new TreeMap<String, String>(originalProperties));
this.datasetSpecs = Collections.unmodifiableSortedMap(new TreeMap<>(datasetSpecs));
}
/**
* Returns the name of the dataset.
* @return the name of the dataset
*/
public String getName() {
return this.name;
}
/**
* Returns the type of the dataset.
* @return the type of the dataset
*/
public String getType() {
return this.type;
}
/**
* Returns the description of dataset.
* @return the description of dataset
*/
@Nullable
public String getDescription() {
return description;
}
/**
* Lookup a custom property of the dataset.
* @param key the name of the property
* @return the value of the property or null if the property does not exist
*/
public String getProperty(String key) {
return properties.get(key);
}
/**
* Lookup a custom property of the dataset.
* @param key the name of the property
* @param defaultValue the value to return if property does not exist
* @return the value of the property or defaultValue if the property does not exist
*/
public String getProperty(String key, String defaultValue) {
return properties.containsKey(key) ? getProperty(key) : defaultValue;
}
/**
* Lookup a custom property of the dataset.
* @param key the name of the property
* @param defaultValue the value to return if property does not exist
* @return the value of the property or defaultValue if the property does not exist
*/
public long getLongProperty(String key, long defaultValue) {
return properties.containsKey(key) ? Long.parseLong(getProperty(key)) : defaultValue;
}
/**
* Lookup a custom property of the dataset.
* @param key the name of the property
* @param defaultValue the value to return if property does not exist
* @return the value of the property or defaultValue if the property does not exist
*/
public int getIntProperty(String key, int defaultValue) {
return properties.containsKey(key) ? Integer.parseInt(getProperty(key)) : defaultValue;
}
/**
* Return the original properties with which the dataset was created/reconfigured.
* @return an immutable map. For embedded datasets, this will always return null.
*/
@Nullable
public Map<String, String> getOriginalProperties() {
return originalProperties;
}
/**
* Return map of all properties set in this specification.
* @return an immutable map.
*/
public Map<String, String> getProperties() {
return Collections.unmodifiableMap(properties);
}
/**
* Get the specification for an embedded dataset.
* @param dsName the name of the embedded dataset
* @return the specification for the named embedded dataset,
* or null if not found.
*/
public DatasetSpecification getSpecification(String dsName) {
return datasetSpecs.get(dsName);
}
/**
* Get the map of embedded dataset name to {@link co.cask.cdap.api.dataset.DatasetSpecification}
* @return the map of dataset name to {@link co.cask.cdap.api.dataset.DatasetSpecification}
*/
public SortedMap<String, DatasetSpecification> getSpecifications() {
return datasetSpecs;
}
/**
* @return a new spec that is the same as this one, with the original properties set to the provided properties.
*/
public DatasetSpecification setOriginalProperties(DatasetProperties originalProps) {
return setOriginalProperties(originalProps.getProperties());
}
/**
* @return a new spec that is the same as this one, with the original properties set to the provided properties.
*/
public DatasetSpecification setOriginalProperties(Map<String, String> originalProps) {
return new DatasetSpecification(name, type, description, originalProps, properties, datasetSpecs);
}
public DatasetSpecification setDescription(String description) {
return new DatasetSpecification(name, type, description, originalProperties, properties, datasetSpecs);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
DatasetSpecification that = (DatasetSpecification) o;
return Objects.equals(name, that.name) &&
Objects.equals(type, that.type) &&
Objects.equals(description, that.description) &&
Objects.equals(originalProperties, that.originalProperties) &&
Objects.equals(properties, that.properties) &&
Objects.equals(datasetSpecs, that.datasetSpecs);
}
@Override
public int hashCode() {
return Objects.hash(name, type, description, originalProperties, properties, datasetSpecs);
}
/**
* Returns true if the datasetName is the name of a non-composite dataset represented by this spec.
* That is, it is represented by one of the leaf-nodes of this dataset spec.
* @param datasetName the name of a dataset
* @return <code>true</code> if the datasetName is represented by the dataset spec;
* <code>false</code> otherwise
*/
public boolean isParent(String datasetName) {
return isParent(datasetName, this);
}
private boolean isParent(String datasetName, DatasetSpecification specification) {
if (datasetName == null) {
return false;
}
if (specification.getSpecifications().size() == 0 && specification.getName().equals(datasetName)) {
return true;
}
if (datasetName.startsWith(specification.getName())) {
for (DatasetSpecification spec : specification.getSpecifications().values()) {
if (isParent(datasetName, spec)) {
return true;
}
}
}
return false;
}
@Override
public String toString() {
return "DatasetSpecification{" +
"name='" + name + '\'' +
", type='" + type + '\'' +
", description='" + description + '\'' +
", originalProperties=" + originalProperties +
", properties=" + properties +
", datasetSpecs=" + datasetSpecs +
'}';
}
/**
* A Builder to construct DatasetSpecification instances.
*/
public static final class Builder {
// private fields
private final String name;
private final String type;
private String description;
private final TreeMap<String, String> properties;
private final TreeMap<String, DatasetSpecification> dataSetSpecs;
private Builder(String name, String typeName) {
this.name = name;
this.type = typeName;
this.properties = new TreeMap<>();
this.dataSetSpecs = new TreeMap<>();
}
/**
* Set the dataset description.
* This description will be overridden by the description set during dataset instance creation.
*
* @param description dataset description
* @return this builder object to allow chaining
*/
public Builder setDescription(String description) {
this.description = description;
return this;
}
/**
* Add underlying dataset specs.
* @param specs specs to add
* @return this builder object to allow chaining
*/
public Builder datasets(DatasetSpecification... specs) {
return datasets(Arrays.asList(specs));
}
/**
* Add underlying dataset specs.
* @param specs specs to add
* @return this builder object to allow chaining
*/
public Builder datasets(Collection<? extends DatasetSpecification> specs) {
for (DatasetSpecification spec : specs) {
this.dataSetSpecs.put(spec.getName(), spec);
}
return this;
}
/**
* Add a custom property.
* @param key the name of the custom property
* @param value the value of the custom property
* @return this builder object to allow chaining
*/
public Builder property(String key, String value) {
this.properties.put(key, value);
return this;
}
/**
* Add properties.
* @param props properties to add
* @return this builder object to allow chaining
*/
public Builder properties(Map<String, String> props) {
this.properties.putAll(props);
return this;
}
/**
* Create a DataSetSpecification from this builder, using the private DataSetSpecification
* constructor.
* @return a complete DataSetSpecification
*/
public DatasetSpecification build() {
return namespace(new DatasetSpecification(name, type, description, properties, dataSetSpecs));
}
/**
* Prefixes all DataSets embedded inside the given {@link DatasetSpecification} with the name of the enclosing
* Dataset.
*/
private DatasetSpecification namespace(DatasetSpecification spec) {
return namespace(null, spec);
}
/*
* Prefixes all DataSets embedded inside the given {@link DataSetSpecification} with the given namespace.
*/
private DatasetSpecification namespace(String namespace, DatasetSpecification spec) {
// Name of the DataSetSpecification is prefixed with namespace if namespace is present.
String name;
if (namespace == null) {
name = spec.getName();
} else {
name = namespace;
if (!spec.getName().isEmpty()) {
name += '.' + spec.getName();
}
}
// If no namespace is given, starts with using the DataSet name.
namespace = (namespace == null) ? spec.getName() : namespace;
TreeMap<String, DatasetSpecification> specifications = new TreeMap<>();
for (Map.Entry<String, DatasetSpecification> entry : spec.datasetSpecs.entrySet()) {
specifications.put(entry.getKey(), namespace(namespace, entry.getValue()));
}
return new DatasetSpecification(name, spec.type, spec.description, spec.properties, specifications);
}
}
}