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