/* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.elasticsearch.search.aggregations.bucket.histogram; import org.elasticsearch.common.CheckedFunction; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.rounding.Rounding; import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.ObjectParser.ValueType; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentParser.Token; import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.search.SearchParseException; import org.elasticsearch.search.internal.SearchContext; import java.io.IOException; import java.util.Objects; import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; public class ExtendedBounds implements ToXContent, Writeable { static final ParseField EXTENDED_BOUNDS_FIELD = Histogram.EXTENDED_BOUNDS_FIELD; static final ParseField MIN_FIELD = new ParseField("min"); static final ParseField MAX_FIELD = new ParseField("max"); public static final ConstructingObjectParser<ExtendedBounds, Void> PARSER = new ConstructingObjectParser<>( "extended_bounds", a -> { assert a.length == 2; Long min = null; Long max = null; String minAsStr = null; String maxAsStr = null; if (a[0] == null) { // nothing to do with it } else if (a[0] instanceof Long) { min = (Long) a[0]; } else if (a[0] instanceof String) { minAsStr = (String) a[0]; } else { throw new IllegalArgumentException("Unknown field type [" + a[0].getClass() + "]"); } if (a[1] == null) { // nothing to do with it } else if (a[1] instanceof Long) { max = (Long) a[1]; } else if (a[1] instanceof String) { maxAsStr = (String) a[1]; } else { throw new IllegalArgumentException("Unknown field type [" + a[1].getClass() + "]"); } return new ExtendedBounds(min, max, minAsStr, maxAsStr); }); static { CheckedFunction<XContentParser, Object, IOException> longOrString = p -> { if (p.currentToken() == Token.VALUE_NUMBER) { return p.longValue(false); } if (p.currentToken() == Token.VALUE_STRING) { return p.text(); } if (p.currentToken() == Token.VALUE_NULL) { return null; } throw new IllegalArgumentException("Unsupported token [" + p.currentToken() + "]"); }; PARSER.declareField(optionalConstructorArg(), longOrString, MIN_FIELD, ValueType.LONG_OR_NULL); PARSER.declareField(optionalConstructorArg(), longOrString, MAX_FIELD, ValueType.LONG_OR_NULL); } /** * Parsed min value. If this is null and {@linkplain #minAsStr} isn't then this must be parsed from {@linkplain #minAsStr}. If this is * null and {@linkplain #minAsStr} is also null then there is no lower bound. */ private final Long min; /** * Parsed min value. If this is null and {@linkplain #maxAsStr} isn't then this must be parsed from {@linkplain #maxAsStr}. If this is * null and {@linkplain #maxAsStr} is also null then there is no lower bound. */ private final Long max; private final String minAsStr; private final String maxAsStr; /** * Construct with parsed bounds. */ public ExtendedBounds(Long min, Long max) { this(min, max, null, null); } /** * Construct with unparsed bounds. */ public ExtendedBounds(String minAsStr, String maxAsStr) { this(null, null, minAsStr, maxAsStr); } /** * Construct with all possible information. */ private ExtendedBounds(Long min, Long max, String minAsStr, String maxAsStr) { this.min = min; this.max = max; this.minAsStr = minAsStr; this.maxAsStr = maxAsStr; } /** * Read from a stream. */ public ExtendedBounds(StreamInput in) throws IOException { min = in.readOptionalLong(); max = in.readOptionalLong(); minAsStr = in.readOptionalString(); maxAsStr = in.readOptionalString(); } @Override public void writeTo(StreamOutput out) throws IOException { out.writeOptionalLong(min); out.writeOptionalLong(max); out.writeOptionalString(minAsStr); out.writeOptionalString(maxAsStr); } /** * Parse the bounds and perform any delayed validation. Returns the result of the parsing. */ ExtendedBounds parseAndValidate(String aggName, SearchContext context, DocValueFormat format) { Long min = this.min; Long max = this.max; assert format != null; if (minAsStr != null) { min = format.parseLong(minAsStr, false, context.getQueryShardContext()::nowInMillis); } if (maxAsStr != null) { // TODO: Should we rather pass roundUp=true? max = format.parseLong(maxAsStr, false, context.getQueryShardContext()::nowInMillis); } if (min != null && max != null && min.compareTo(max) > 0) { throw new SearchParseException(context, "[extended_bounds.min][" + min + "] cannot be greater than " + "[extended_bounds.max][" + max + "] for histogram aggregation [" + aggName + "]", null); } return new ExtendedBounds(min, max, minAsStr, maxAsStr); } ExtendedBounds round(Rounding rounding) { return new ExtendedBounds(min != null ? rounding.round(min) : null, max != null ? rounding.round(max) : null); } @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(EXTENDED_BOUNDS_FIELD.getPreferredName()); if (min != null) { builder.field(MIN_FIELD.getPreferredName(), min); } else { builder.field(MIN_FIELD.getPreferredName(), minAsStr); } if (max != null) { builder.field(MAX_FIELD.getPreferredName(), max); } else { builder.field(MAX_FIELD.getPreferredName(), maxAsStr); } builder.endObject(); return builder; } @Override public int hashCode() { return Objects.hash(min, max, minAsStr, maxAsStr); } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } ExtendedBounds other = (ExtendedBounds) obj; return Objects.equals(min, other.min) && Objects.equals(max, other.max) && Objects.equals(minAsStr, other.minAsStr) && Objects.equals(maxAsStr, other.maxAsStr); } public Long getMin() { return min; } public Long getMax() { return max; } @Override public String toString() { StringBuilder b = new StringBuilder(); if (min != null) { b.append(min); if (minAsStr != null) { b.append('(').append(minAsStr).append(')'); } } else { if (minAsStr != null) { b.append(minAsStr); } } b.append("--"); if (max != null) { b.append(min); if (maxAsStr != null) { b.append('(').append(maxAsStr).append(')'); } } else { if (maxAsStr != null) { b.append(maxAsStr); } } return b.toString(); } }