/**
* This {@link ILocationInterpreter} is able to create a {@link ICondition} that follows the
* business logic of the bucketing. The condition will be used while performing a drillthrough
* taking into account the location coordinates and subcubes on the Copper bucketing hierarchy.
*
* <p>Only trades whose date belong to the selected buckets (this selection can be deduced from
* locations and subcubes) will appear in the drillthrough result.
*
* <p>Pay attention that this <b>object is shared</b> among queries so it must support concurrency
* calls.
*
* @author ActiveViam
*/
public class TimeBucketLocationInterpreter extends DefaultLocationInterpreter {
/** Time buckets indexed by their name. */
public static final Map<Object, TimeBucket> bucketMap = new LinkedHashMap<>();
static {
bucketMap.put("1D", new TimeBucket(ChronoUnit.DAYS, 1));
bucketMap.put("2D", new TimeBucket(ChronoUnit.DAYS, 2));
bucketMap.put("3D", new TimeBucket(ChronoUnit.DAYS, 3));
bucketMap.put("1W", new TimeBucket(ChronoUnit.WEEKS, 1));
bucketMap.put("2W", new TimeBucket(ChronoUnit.WEEKS, 2));
bucketMap.put("1M", new TimeBucket(ChronoUnit.MONTHS, 1));
bucketMap.put("2M", new TimeBucket(ChronoUnit.MONTHS, 2));
bucketMap.put("3M", new TimeBucket(ChronoUnit.MONTHS, 3));
bucketMap.put("6M", new TimeBucket(ChronoUnit.MONTHS, 6));
bucketMap.put("1Y", new TimeBucket(ChronoUnit.YEARS, 1));
bucketMap.put("2Y", new TimeBucket(ChronoUnit.YEARS, 2));
bucketMap.put("5Y", new TimeBucket(ChronoUnit.YEARS, 5));
bucketMap.put("10Y", new TimeBucket(ChronoUnit.YEARS, 10));
bucketMap.put("20Y", new TimeBucket(ChronoUnit.YEARS, 20));
bucketMap.put("30Y", new TimeBucket(ChronoUnit.YEARS, 30));
bucketMap.put("50Y", new TimeBucket(ChronoUnit.YEARS, 50));
bucketMap.put("<30D", new TimeBucket(ChronoUnit.DAYS, 30));
}
public static NavigableMap<Long, Object> createBucketMap(final long timeReference) {
final NavigableMap<Long, Object> result = new TreeMap<>();
bucketMap.forEach(
(bucketName, timeBucket) -> result.put(timeBucket.getBoundary(timeReference), bucketName));
return result;
}
/**
* Relative time bucket.
*
* @param unit The unit.
* @param count The upper boundary of this bucket, in the given.
*/
public record TimeBucket(ChronoUnit unit, int count) {
/**
* Gives the smaller instant that represent this bucket.
*
* @param reference The reference from which we should start.
* @return The lower boundary of this bucket.
*/
public long getBoundary(final long reference) {
final ZonedDateTime ref = Instant.ofEpochMilli(reference).atZone(ZoneOffset.UTC);
return ref.plus(count, unit).toInstant().toEpochMilli();
}
@Override
public String toString() {
return this.getClass().getSimpleName() + " of " + this.count + " " + this.unit;
}
}
/** Bucket hierarchy property. */
public static final String BUCKET_HIERARCHY_PROPERTY = "bucketHierarchy";
/** Bucketed Level property. */
public static final String BUCKETED_LEVEL_PROPERTY = "bucketLevel";
/** The ordinal of the bucket level. */
public static final int BUCKET_LEVEL_ORDINAL = 1;
/** Name of the analysis hierachy. */
protected final String bucketHiearchyName;
/** Property of the bucketed level. */
protected final String bucketedLevel;
/** The current time in milliseconds. */
protected final long now;
/** The bucket level info. */
protected ILevelInfo bucketLevelInfo;
/** The bucketed level info. */
protected ILevelInfo bucketedLevelInfo;
/** The bucket hierarchy. */
protected final IHierarchy bucketHierarchy;
/**
* Default constructor.
*
* @param properties some additional {@link Properties properties}
*/
public TimeBucketLocationInterpreter(
final IActivePivotVersion pivot,
final ISelection underlyingView,
final Properties properties) {
super(underlyingView, properties);
// It will create the bucket map.
this.bucketedLevel = properties.getProperty(BUCKETED_LEVEL_PROPERTY);
this.bucketHiearchyName = properties.getProperty(BUCKET_HIERARCHY_PROPERTY);
this.now = System.currentTimeMillis();
// Initialization
this.bucketHierarchy = init(pivot);
}
/**
* Initialization of the location interpreter. It is called each time a condition is created (i.e
* per query).
*
* @param pivot the current cube
* @return the analysis hierarchy (TimeBucketDynamic in this case)
*/
protected IHierarchy init(final IActivePivot pivot) {
// Retrieve the bucket hierarchy.
final IHierarchy bucketHierarchy = HierarchiesUtil.getHierarchy(pivot, bucketHiearchyName);
if (bucketHierarchy.getLevels().size() != BUCKET_LEVEL_ORDINAL + 1) {
throw new ActiveViamRuntimeException(
"The bucket hierarchy must have exactly one level: "
+ bucketHierarchy.getName()
+ ", "
+ bucketHierarchy.getLevels());
}
// Extract the level info
bucketLevelInfo = bucketHierarchy.getLevels().get(BUCKET_LEVEL_ORDINAL);
// Retrieve the bucketed level info.
bucketedLevelInfo = HierarchiesUtil.getLevel(pivot, bucketedLevel);
return bucketHierarchy;
}
@Override
protected List<ICondition> locationConditions(
final List<? extends IHierarchy> hierarchies, final ILocation location) {
// First, compute the condition using the default behavior,
// that is to say the one ignoring the analysis hierarchies.
final List<ICondition> originalConditions = super.locationConditions(hierarchies, location);
// Introspect the location
if (LocationUtil.isAtOrBelowLevel(location, bucketLevelInfo)) {
final Object[] values =
LocationUtil.extractValues(location, new ILevelInfo[] {bucketLevelInfo});
if (values != null) {
// The condition deduced from the location.
final var locationCondition = createBucketCondition(values);
if (locationCondition != null) {
originalConditions.add(locationCondition);
}
}
}
return originalConditions;
}
/**
* Create a {@link ICondition} from input buckets and relatively to the current time.
*
* @param buckets the bucket from which a {@link ICondition} will be created.
* @return the deduced {@link ICondition}. It can be returned null.
*/
protected ICondition createBucketCondition(final Object[] buckets) {
final NavigableSet<Object> elements = new TreeSet<>(bucketLevelInfo.getComparator());
for (final Object value : buckets) {
if (value != null) {
elements.add(value);
}
}
final Object lastBucket = elements.last();
if (lastBucket != null) {
final String fieldName = bucketedLevelInfo.getSelectionField();
final FieldPath fieldExpression = underlyingView.findFieldByAlias(fieldName).getFieldPath();
final Long lastDateElement = bucketMap.get(lastBucket).getBoundary(this.now);
final Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(lastDateElement);
return BaseConditions.lessOrEqual(fieldExpression, cal.getTime());
}
return null;
}
}