001/* 002 * (C) Copyright 2014-2016 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.time.LocalDate; 035import java.time.ZoneId; 036import java.time.ZonedDateTime; 037import java.time.format.DateTimeFormatter; 038import java.time.temporal.TemporalAccessor; 039import java.util.ArrayList; 040import java.util.Collection; 041import java.util.List; 042import java.util.Map; 043 044import org.elasticsearch.index.query.BoolQueryBuilder; 045import org.elasticsearch.index.query.QueryBuilder; 046import org.elasticsearch.index.query.QueryBuilders; 047import org.elasticsearch.index.query.RangeQueryBuilder; 048import org.elasticsearch.search.aggregations.AggregationBuilders; 049import org.elasticsearch.search.aggregations.BucketOrder; 050import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation; 051import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramAggregationBuilder; 052import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval; 053import org.elasticsearch.search.aggregations.bucket.histogram.ExtendedBounds; 054import org.joda.time.DateTimeZone; 055import org.nuxeo.common.utils.DateUtils; 056import org.nuxeo.ecm.core.api.DocumentModel; 057import org.nuxeo.ecm.platform.query.api.AggregateDefinition; 058import org.nuxeo.ecm.platform.query.core.BucketRangeDate; 059import org.nuxeo.elasticsearch.ElasticSearchConstants; 060 061import com.fasterxml.jackson.annotation.JsonIgnore; 062 063/** 064 * @since 6.0 065 */ 066public class DateHistogramAggregate extends MultiBucketAggregate<BucketRangeDate> { 067 068 public DateHistogramAggregate(AggregateDefinition definition, DocumentModel searchDocument) { 069 super(definition, searchDocument); 070 } 071 072 @JsonIgnore 073 @Override 074 public DateHistogramAggregationBuilder getEsAggregate() { 075 DateHistogramAggregationBuilder ret = AggregationBuilders.dateHistogram(getId()) 076 .field(getField()) 077 .timeZone(DateTimeZone.getDefault() 078 .toTimeZone() 079 .toZoneId()); 080 Map<String, String> props = getProperties(); 081 if (props.containsKey(AGG_INTERVAL_PROP)) { 082 ret.dateHistogramInterval(new DateHistogramInterval(props.get(AGG_INTERVAL_PROP))); 083 } 084 if (props.containsKey(AGG_MIN_DOC_COUNT_PROP)) { 085 ret.minDocCount(Long.parseLong(props.get(AGG_MIN_DOC_COUNT_PROP))); 086 } 087 if (props.containsKey(AGG_ORDER_PROP)) { 088 switch (props.get(AGG_ORDER_PROP).toLowerCase()) { 089 case AGG_ORDER_COUNT_DESC: 090 ret.order(BucketOrder.count(false)); 091 break; 092 case AGG_ORDER_COUNT_ASC: 093 ret.order(BucketOrder.count(true)); 094 break; 095 case AGG_ORDER_KEY_DESC: 096 ret.order(BucketOrder.key(false)); 097 break; 098 case AGG_ORDER_KEY_ASC: 099 ret.order(BucketOrder.key(true)); 100 break; 101 default: 102 throw new IllegalArgumentException("Invalid order: " + props.get(AGG_ORDER_PROP)); 103 } 104 } 105 if (props.containsKey(AGG_EXTENDED_BOUND_MAX_PROP) && props.containsKey(AGG_EXTENDED_BOUND_MIN_PROP)) { 106 ret.extendedBounds( 107 new ExtendedBounds(props.get(AGG_EXTENDED_BOUND_MIN_PROP), props.get(AGG_EXTENDED_BOUND_MAX_PROP))); 108 } 109 if (props.containsKey(AGG_TIME_ZONE_PROP)) { 110 ret.timeZone(DateTimeZone.forID(props.get(AGG_TIME_ZONE_PROP)).toTimeZone().toZoneId()); 111 } 112 if (props.containsKey(AGG_PRE_ZONE_PROP)) { 113 ret.timeZone(DateTimeZone.forID(props.get(AGG_PRE_ZONE_PROP)).toTimeZone().toZoneId()); 114 } 115 if (props.containsKey(AGG_FORMAT_PROP)) { 116 ret.format(props.get(AGG_FORMAT_PROP)); 117 } 118 return ret; 119 } 120 121 @JsonIgnore 122 @Override 123 public QueryBuilder getEsFilter() { 124 if (getSelection().isEmpty()) { 125 return null; 126 } 127 BoolQueryBuilder ret = QueryBuilders.boolQuery(); 128 for (String sel : getSelection()) { 129 RangeQueryBuilder rangeFilter = QueryBuilders.rangeQuery(getField()); 130 ZonedDateTime from = convertStringToDate(sel); 131 ZonedDateTime to = DateHelper.plusDuration(from, getInterval()); 132 rangeFilter.gte(from.toInstant().toEpochMilli()) 133 .lt(to.toInstant().toEpochMilli()) 134 .format(ElasticSearchConstants.EPOCH_MILLIS_FORMAT); 135 ret.should(rangeFilter); 136 } 137 return ret; 138 } 139 140 private ZonedDateTime convertStringToDate(String date) { 141 Map<String, String> props = getProperties(); 142 String timezone = "UTC"; 143 if (props.containsKey(AGG_TIME_ZONE_PROP)) { 144 timezone = props.get(AGG_TIME_ZONE_PROP); 145 } 146 DateTimeFormatter fmt; 147 if (props.containsKey(AGG_FORMAT_PROP)) { 148 fmt = DateUtils.robustOfPattern(props.get(AGG_FORMAT_PROP)).withZone(ZoneId.of(timezone)); 149 } else { 150 throw new IllegalArgumentException("format property must be defined for " + toString()); 151 } 152 TemporalAccessor ta = fmt.parseBest(date, ZonedDateTime::from, LocalDate::from); 153 if (ta instanceof LocalDate) { 154 return ((LocalDate) ta).atStartOfDay(ZoneId.of(timezone)); 155 } else { 156 return (ZonedDateTime) ta; 157 } 158 } 159 160 @JsonIgnore 161 @Override 162 public void parseEsBuckets(Collection<? extends MultiBucketsAggregation.Bucket> buckets) { 163 List<BucketRangeDate> nxBuckets = new ArrayList<>(buckets.size()); 164 for (MultiBucketsAggregation.Bucket bucket : buckets) { 165 ZonedDateTime fromZDT = (ZonedDateTime) bucket.getKey(); 166 ZonedDateTime toZDT = DateHelper.plusDuration(fromZDT, getInterval()); 167 nxBuckets.add(new BucketRangeDate(bucket.getKeyAsString(), fromZDT, toZDT, bucket.getDocCount())); 168 } 169 this.buckets = nxBuckets; 170 } 171 172 private String getInterval() { 173 String ret; 174 Map<String, String> props = getProperties(); 175 if (props.containsKey(AGG_INTERVAL_PROP)) { 176 ret = props.get(AGG_INTERVAL_PROP); 177 } else { 178 throw new IllegalArgumentException("interval property must be defined for " + toString()); 179 } 180 return ret; 181 } 182 183}