001/*
002 * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     bdelbosc
018 */
019package org.nuxeo.elasticsearch.aggregate;
020
021import static org.nuxeo.elasticsearch.ElasticSearchConstants.AGG_EXTENDED_BOUND_MAX_PROP;
022import static org.nuxeo.elasticsearch.ElasticSearchConstants.AGG_EXTENDED_BOUND_MIN_PROP;
023import static org.nuxeo.elasticsearch.ElasticSearchConstants.AGG_FORMAT_PROP;
024import static org.nuxeo.elasticsearch.ElasticSearchConstants.AGG_INTERVAL_PROP;
025import static org.nuxeo.elasticsearch.ElasticSearchConstants.AGG_MIN_DOC_COUNT_PROP;
026import static org.nuxeo.elasticsearch.ElasticSearchConstants.AGG_ORDER_COUNT_ASC;
027import static org.nuxeo.elasticsearch.ElasticSearchConstants.AGG_ORDER_COUNT_DESC;
028import static org.nuxeo.elasticsearch.ElasticSearchConstants.AGG_ORDER_KEY_ASC;
029import static org.nuxeo.elasticsearch.ElasticSearchConstants.AGG_ORDER_KEY_DESC;
030import static org.nuxeo.elasticsearch.ElasticSearchConstants.AGG_ORDER_PROP;
031import static org.nuxeo.elasticsearch.ElasticSearchConstants.AGG_PRE_ZONE_PROP;
032import static org.nuxeo.elasticsearch.ElasticSearchConstants.AGG_TIME_ZONE_PROP;
033
034import java.util.ArrayList;
035import java.util.Collection;
036import java.util.List;
037import java.util.Map;
038
039import org.codehaus.jackson.annotate.JsonIgnore;
040import org.elasticsearch.common.unit.TimeValue;
041import org.elasticsearch.index.query.FilterBuilder;
042import org.elasticsearch.index.query.FilterBuilders;
043import org.elasticsearch.index.query.OrFilterBuilder;
044import org.elasticsearch.index.query.RangeFilterBuilder;
045import org.elasticsearch.search.aggregations.AggregationBuilders;
046import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation;
047import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogram;
048import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramBuilder;
049import org.elasticsearch.search.aggregations.bucket.histogram.Histogram;
050import org.joda.time.DateTime;
051import org.joda.time.format.DateTimeFormat;
052import org.joda.time.format.DateTimeFormatter;
053import org.nuxeo.ecm.core.api.DocumentModel;
054import org.nuxeo.ecm.platform.query.api.AggregateDefinition;
055import org.nuxeo.ecm.platform.query.core.BucketRangeDate;
056
057/**
058 * @since 6.0
059 */
060public class DateHistogramAggregate extends AggregateEsBase<BucketRangeDate> {
061
062    Long intervalMillis;
063
064    public DateHistogramAggregate(AggregateDefinition definition, DocumentModel searchDocument) {
065        super(definition, searchDocument);
066    }
067
068    @JsonIgnore
069    @Override
070    public DateHistogramBuilder getEsAggregate() {
071        DateHistogramBuilder ret = AggregationBuilders.dateHistogram(getId()).field(getField());
072        Map<String, String> props = getProperties();
073        if (props.containsKey(AGG_INTERVAL_PROP)) {
074            ret.interval(new DateHistogram.Interval(props.get(AGG_INTERVAL_PROP)));
075        }
076        if (props.containsKey(AGG_MIN_DOC_COUNT_PROP)) {
077            ret.minDocCount(Long.parseLong(props.get(AGG_MIN_DOC_COUNT_PROP)));
078        }
079        if (props.containsKey(AGG_ORDER_PROP)) {
080            switch (props.get(AGG_ORDER_PROP).toLowerCase()) {
081            case AGG_ORDER_COUNT_DESC:
082                ret.order(Histogram.Order.COUNT_DESC);
083                break;
084            case AGG_ORDER_COUNT_ASC:
085                ret.order(Histogram.Order.COUNT_ASC);
086                break;
087            case AGG_ORDER_KEY_DESC:
088                ret.order(Histogram.Order.KEY_DESC);
089                break;
090            case AGG_ORDER_KEY_ASC:
091                ret.order(Histogram.Order.KEY_ASC);
092                break;
093            default:
094                throw new IllegalArgumentException("Invalid order: " + props.get(AGG_ORDER_PROP));
095            }
096        }
097        if (props.containsKey(AGG_EXTENDED_BOUND_MAX_PROP) && props.containsKey(AGG_EXTENDED_BOUND_MIN_PROP)) {
098            ret.extendedBounds(props.get(AGG_EXTENDED_BOUND_MIN_PROP), props.get(AGG_EXTENDED_BOUND_MAX_PROP));
099        }
100        if (props.containsKey(AGG_TIME_ZONE_PROP)) {
101            ret.timeZone(props.get(AGG_TIME_ZONE_PROP));
102        }
103        if (props.containsKey(AGG_PRE_ZONE_PROP)) {
104            ret.timeZone(props.get(AGG_PRE_ZONE_PROP));
105        }
106        if (props.containsKey(AGG_FORMAT_PROP)) {
107            ret.format(props.get(AGG_FORMAT_PROP));
108        }
109        return ret;
110    }
111
112    @JsonIgnore
113    @Override
114    public FilterBuilder getEsFilter() {
115        if (getSelection().isEmpty()) {
116            return null;
117        }
118        OrFilterBuilder ret = FilterBuilders.orFilter();
119        for (String sel : getSelection()) {
120            RangeFilterBuilder rangeFilter = FilterBuilders.rangeFilter(getField());
121            long from = convertStringToDate(sel);
122            long to = from + getIntervalInMillis();
123            rangeFilter.gte(from).lt(to);
124            ret.add(rangeFilter);
125        }
126        return ret;
127    }
128
129    private long convertStringToDate(String date) {
130        Map<String, String> props = getProperties();
131        DateTimeFormatter fmt;
132        if (props.containsKey(AGG_FORMAT_PROP)) {
133            fmt = DateTimeFormat.forPattern(props.get(AGG_FORMAT_PROP));
134        } else {
135            throw new IllegalArgumentException("format property must be defined for " + toString());
136        }
137        // TODO should take in account all the locale zone stuff ...
138        return fmt.parseDateTime(date).getMillis();
139    }
140
141    @JsonIgnore
142    @Override
143    public void parseEsBuckets(Collection<? extends MultiBucketsAggregation.Bucket> buckets) {
144        List<BucketRangeDate> nxBuckets = new ArrayList<>(buckets.size());
145        for (MultiBucketsAggregation.Bucket bucket : buckets) {
146            DateHistogram.Bucket dateHistoBucket = (DateHistogram.Bucket) bucket;
147            DateTime from = getDateTime(dateHistoBucket.getKeyAsDate());
148            DateTime to = addInterval(from);
149            nxBuckets.add(new BucketRangeDate(bucket.getKey(), from, to, dateHistoBucket.getDocCount()));
150        }
151        this.buckets = nxBuckets;
152    }
153
154    private DateTime addInterval(DateTime from) {
155        return new DateTime(from.getMillis() + getIntervalInMillis());
156    }
157
158    public long getIntervalInMillis() {
159        if (intervalMillis == null) {
160            String interval;
161            Map<String, String> props = getProperties();
162            if (props.containsKey(AGG_INTERVAL_PROP)) {
163                interval = props.get(AGG_INTERVAL_PROP);
164            } else {
165                throw new IllegalArgumentException("interval property must be defined for " + toString());
166            }
167            interval = convertToTimeValueString(interval);
168            intervalMillis = (long) TimeValue.parseTimeValue(interval, null).getMillis();
169        }
170        return intervalMillis;
171    }
172
173    private String convertToTimeValueString(String interval) {
174        switch (interval.toLowerCase()) {
175        case "second":
176            return "1s";
177        case "minute":
178            return "1m";
179        case "hour":
180            return "1h";
181        case "day":
182            return "1d";
183        case "week":
184            return "7d";
185        case "year":
186            return "365d";
187            // may be wrong here ...
188        case "month":
189            return "30d";
190        case "quarter":
191            return "91d";
192        default:
193            // already in ms
194        }
195        return interval;
196    }
197
198}