blob: 5d74d219d2fb48d9b03d97e907ab457787886643 [file] [log] [blame]
/**
* 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.apex.malhar.lib.dimensions;
import java.io.Serializable;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.apex.malhar.lib.appdata.schemas.CustomTimeBucket;
import org.apache.apex.malhar.lib.appdata.schemas.Fields;
import org.apache.apex.malhar.lib.appdata.schemas.FieldsDescriptor;
import org.apache.apex.malhar.lib.appdata.schemas.TimeBucket;
import org.apache.apex.malhar.lib.appdata.schemas.Type;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
/**
* <p>
* This class defines a dimensions combination which is used by dimensions
* computation operators and stores. A dimension combination is composed of the
* names of the fields that constitute the key, as well as the TimeBucket under
* which data is stored.
* </p>
* <p>
* This class supports the creation of a dimensions combination from a
* {@link TimeBucket} object and a set of fields. It also supports the creation
* of a dimensions combination an aggregation string. An aggregation string
* looks like the following: <br/>
* <br/>
* {@code
* "time=MINUTES:publisher:advertiser"
* } <br/>
* <br/>
* In the example above <b>"time=MINUTES"</b> represents a time bucket, and the
* other colon separated strings represent the name of fields which comprise the
* key for this dimension combination. When specifiying a time bucket in an
* aggregation string you must use the name of one of the TimeUnit enums.
* </p>
* <p>
* One of the primary uses of a {@link DimensionsDescriptor} is for querying a
* dimensional data store. When a query is received for a dimensional data
* store, the query must be mapped to many things including a
* dimensionDescriptorID. The dimensionDescriptorID is an id assigned to a class
* of dimension combinations which share the same keys. This mapping is
* performed by creating a {@link DimensionsDescriptor} object from the query,
* and then using the {@link DimensionsDescriptor} object to look up the correct
* dimensionsDescriptorID. This lookup to retrieve a dimensionsDescriptorID is
* necessary because a dimensionsDescriptorID is used for storage in order to
* prevent key conflicts.
* </p>
*
*
* @since 3.3.0
*/
public class DimensionsDescriptor implements Serializable, Comparable<DimensionsDescriptor>
{
private static final long serialVersionUID = 201506251237L;
/**
* Name of the reserved time field.
*/
public static final String DIMENSION_TIME = "time";
/**
* Type of the reserved time field.
*/
public static final Type DIMENSION_TIME_TYPE = Type.LONG;
/**
* Name of the reserved time bucket field.
*/
public static final String DIMENSION_TIME_BUCKET = "timeBucket";
/**
* Type of the reserved time bucket field.
*/
public static final Type DIMENSION_TIME_BUCKET_TYPE = Type.INTEGER;
/**
* The set of fields used for time, which are intended to be queried. Not that
* the timeBucket field is not included here because its not intended to be
* queried.
*/
public static final Fields TIME_FIELDS = new Fields(Sets.newHashSet(DIMENSION_TIME));
/**
* This set represents the field names which cannot be part of the user
* defined field names in a schema for dimensions computation.
*/
public static final Set<String> RESERVED_DIMENSION_NAMES = ImmutableSet.of(DIMENSION_TIME, DIMENSION_TIME_BUCKET);
/**
* This is the equals string separator used when defining a time bucket for a
* dimensions combination.
*/
public static final String DELIMETER_EQUALS = "=";
/**
* This separates dimensions in the dimensions combination.
*/
public static final String DELIMETER_SEPERATOR = ":";
/**
* A map from a key field to its type.
*/
public static final Map<String, Type> DIMENSION_FIELD_TO_TYPE;
/**
* The time bucket used for this dimension combination.
*/
private TimeBucket timeBucket;
/**
* The custom time bucket used for this dimension combination.
*/
private CustomTimeBucket customTimeBucket;
/**
* The set of key fields which compose this dimension combination.
*/
private Fields fields;
static {
Map<String, Type> dimensionFieldToType = Maps.newHashMap();
dimensionFieldToType.put(DIMENSION_TIME, DIMENSION_TIME_TYPE);
dimensionFieldToType.put(DIMENSION_TIME_BUCKET, DIMENSION_TIME_BUCKET_TYPE);
DIMENSION_FIELD_TO_TYPE = Collections.unmodifiableMap(dimensionFieldToType);
}
/**
* Constructor for kryo serialization.
*/
private DimensionsDescriptor()
{
//for kryo
}
/**
* Creates a dimensions descriptor (dimensions combination) with the given
* {@link TimeBucket} and key fields.
*
* @param timeBucket
* The {@link TimeBucket} that this dimensions combination
* represents.
* @param fields
* The key fields included in this dimensions combination.
* @deprecated use
* {@link #DimensionsDescriptor(org.apache.apex.malhar.lib.appdata.schemas.CustomTimeBucket, org.apache.apex.malhar.lib.appdata.schemas.Fields)}
* instead.
*/
@Deprecated
public DimensionsDescriptor(TimeBucket timeBucket, Fields fields)
{
setTimeBucket(timeBucket);
setFields(fields);
}
/**
* Creates a dimensions descriptor (dimensions combination) with the given
* {@link CustomTimeBucket} and key fields.
*
* @param timeBucket
* The {@link CustomTimeBucket} that this dimensions combination
* represents.
* @param fields
* The key fields included in this dimensions combination.
*/
public DimensionsDescriptor(CustomTimeBucket timeBucket, Fields fields)
{
setCustomTimeBucket(timeBucket);
setFields(fields);
}
/**
* Creates a dimensions descriptor (dimensions combination) with the given key
* fields.
*
* @param fields
* The key fields included in this dimensions combination.
*/
public DimensionsDescriptor(Fields fields)
{
setFields(fields);
}
/**
* This construction creates a dimensions descriptor (dimensions combination)
* from the given aggregation string.
*
* @param aggregationString
* The aggregation string to use when initializing this dimensions
* combination.
*/
public DimensionsDescriptor(String aggregationString)
{
initialize(aggregationString);
}
/**
* Initializes the dimensions combination with the given aggregation string.
*
* @param aggregationString
* The aggregation string with which to initialize this dimensions
* combination.
*/
private void initialize(String aggregationString)
{
String[] fieldArray = aggregationString.split(DELIMETER_SEPERATOR);
Set<String> fieldSet = Sets.newHashSet();
for (String field : fieldArray) {
String[] fieldAndValue = field.split(DELIMETER_EQUALS);
String fieldName = fieldAndValue[0];
if (fieldName.equals(DIMENSION_TIME_BUCKET)) {
throw new IllegalArgumentException(DIMENSION_TIME_BUCKET + " is an invalid time.");
}
if (!fieldName.equals(DIMENSION_TIME)) {
fieldSet.add(fieldName);
}
if (fieldName.equals(DIMENSION_TIME)) {
if (timeBucket != null) {
throw new IllegalArgumentException(
"Cannot specify time in a dimensions " + "descriptor when a timebucket is also " + "specified.");
}
if (fieldAndValue.length == 2) {
timeBucket = TimeBucket.TIME_UNIT_TO_TIME_BUCKET.get(TimeUnit.valueOf(fieldAndValue[1]));
}
}
}
fields = new Fields(fieldSet);
}
/**
* This is a helper method which sets and validates the {@link TimeBucket}.
*
* @param timeBucket
* The {@link TimeBucket} to set and validate.
*/
private void setTimeBucket(TimeBucket timeBucket)
{
Preconditions.checkNotNull(timeBucket);
this.timeBucket = timeBucket;
this.customTimeBucket = new CustomTimeBucket(timeBucket);
}
/**
* This is a helper method which sets and validates the
* {@link CustomTimeBucket}.
*
* @param customTimeBucket
* The {@link CustomTimeBucket} to set and validate.
*/
private void setCustomTimeBucket(CustomTimeBucket customTimeBucket)
{
Preconditions.checkNotNull(customTimeBucket);
this.customTimeBucket = customTimeBucket;
this.timeBucket = customTimeBucket.getTimeBucket();
}
/**
* Gets the {@link TimeBucket} for this {@link DimensionsDescriptor} object.
*
* @return The {@link TimeBucket} for this {@link DimensionsDescriptor}
* object.
* @deprecated use {@link #getCustomTimeBucket()} instead.
*/
@Deprecated
public TimeBucket getTimeBucket()
{
return timeBucket;
}
/**
* Gets the {@link CustomTimeBucket} for this {@link DimensionsDescriptor}
* object.
*
* @return The {@link CustomTimeBucket} for this {@link DimensionsDescriptor}
* object.
*/
public CustomTimeBucket getCustomTimeBucket()
{
return customTimeBucket;
}
/**
* This is a helper method which sets and validates the set of key fields for
* this {@link DimensionsDescriptor} object.
*
* @param fields
* The set of key fields for this {@link DimensionsDescriptor}
* object.
*/
private void setFields(Fields fields)
{
Preconditions.checkNotNull(fields);
this.fields = fields;
}
/**
* Returns the set of key fields for this {@link DimensionsDescriptor} object.
*
* @return The set of key fields for this {@link DimensionsDescriptor} object.
*/
public Fields getFields()
{
return fields;
}
/**
* This method is used to create a new {@link FieldsDescriptor} object
* representing this {@link DimensionsDescriptor} object from another
* {@link FieldsDescriptor} object which defines the names and types of all
* the available key fields.
*
* @param parentDescriptor
* The {@link FieldsDescriptor} object which defines the name and
* type of all the available key fields.
* @return A {@link FieldsDescriptor} object which represents this
* {@link DimensionsDescriptor} (dimensions combination) derived from
* the given {@link FieldsDescriptor} object.
*/
public FieldsDescriptor createFieldsDescriptor(FieldsDescriptor parentDescriptor)
{
Map<String, Type> fieldToType = Maps.newHashMap();
Map<String, Type> parentFieldToType = parentDescriptor.getFieldToType();
for (String field : this.fields.getFields()) {
if (RESERVED_DIMENSION_NAMES.contains(field)) {
continue;
}
fieldToType.put(field, parentFieldToType.get(field));
}
if (timeBucket != null && timeBucket != TimeBucket.ALL) {
fieldToType.put(DIMENSION_TIME_BUCKET, DIMENSION_TIME_BUCKET_TYPE);
fieldToType.put(DIMENSION_TIME, Type.LONG);
}
return new FieldsDescriptor(fieldToType);
}
@Override
public int hashCode()
{
int hash = 7;
hash = 83 * hash + (this.customTimeBucket != null ? this.customTimeBucket.hashCode() : 0);
hash = 83 * hash + (this.fields != null ? this.fields.hashCode() : 0);
return hash;
}
@Override
public boolean equals(Object obj)
{
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final DimensionsDescriptor other = (DimensionsDescriptor)obj;
if (!this.customTimeBucket.equals(other.customTimeBucket)) {
return false;
}
if (this.fields != other.fields && (this.fields == null || !this.fields.equals(other.fields))) {
return false;
}
return true;
}
@Override
public String toString()
{
return "DimensionsDescriptor{" + "timeBucket=" + customTimeBucket + ", fields=" + fields + '}';
}
@Override
public int compareTo(DimensionsDescriptor other)
{
if (this == other) {
return 0;
}
List<String> thisFieldList = this.getFields().getFieldsList();
List<String> otherFieldList = other.getFields().getFieldsList();
if (thisFieldList != otherFieldList) {
int compare = thisFieldList.size() - otherFieldList.size();
if (compare != 0) {
return compare;
}
Collections.sort(thisFieldList);
Collections.sort(otherFieldList);
for (int index = 0; index < thisFieldList.size(); index++) {
String thisField = thisFieldList.get(index);
String otherField = otherFieldList.get(index);
int fieldCompare = thisField.compareTo(otherField);
if (fieldCompare != 0) {
return fieldCompare;
}
}
}
CustomTimeBucket thisBucket = this.getCustomTimeBucket();
CustomTimeBucket otherBucket = other.getCustomTimeBucket();
if (thisBucket == null && otherBucket == null) {
return 0;
} else if (thisBucket != null && otherBucket == null) {
return 1;
} else if (thisBucket == null && otherBucket != null) {
return -1;
} else {
return thisBucket.compareTo(otherBucket);
}
}
private static final Logger LOG = LoggerFactory.getLogger(DimensionsDescriptor.class);
}