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}