001/*
002 *  Copyright 2019 Anyware Services
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 */
016package org.ametys.cms.search.query;
017
018import java.time.LocalDate;
019import java.time.LocalDateTime;
020import java.time.LocalTime;
021import java.time.format.DateTimeFormatter;
022import java.time.format.ResolverStyle;
023
024import org.ametys.core.util.date.AdaptableDate;
025
026/**
027 * Represents a {@link Query} testing a date field.
028 */
029public class DateQuery extends AbstractFieldQuery
030{
031    /** The date formatter */
032    public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
033            .withResolverStyle(ResolverStyle.STRICT);
034    
035    /** The operator. */
036    protected Operator _operator;
037    /** The value to test. */
038    protected AdaptableDate _value;
039    
040    /**
041     * Build a DateQuery testing the existence of the field.
042     * @param fieldPath the field path.
043     */
044    public DateQuery(String fieldPath)
045    {
046        this(fieldPath, Operator.EXISTS, (AdaptableDate) null);
047    }
048    
049    /**
050     * Build a DateQuery.
051     * @param fieldPath the field's path
052     * @param value the value.
053     */
054    public DateQuery(String fieldPath, LocalDate value)
055    {
056        this(fieldPath, AdaptableDate.fromDate(value));
057    }
058    
059    /**
060     * Build a DateQuery.
061     * @param fieldPath the field's path
062     * @param value the value.
063     */
064    public DateQuery(String fieldPath, AdaptableDate value)
065    {
066        this(fieldPath, Operator.EQ, value);
067    }
068    
069    /**
070     * Build a DateQuery.
071     * @param fieldPath the field's path
072     * @param op the operator.
073     * @param value the value.
074     */
075    public DateQuery(String fieldPath, Operator op, LocalDate value)
076    {
077        this(fieldPath, op, AdaptableDate.fromDate(value));
078    }
079    
080    /**
081     * Build a DateQuery.
082     * @param fieldPath the field's path
083     * @param op the operator.
084     * @param value the value.
085     */
086    public DateQuery(String fieldPath, Operator op, AdaptableDate value)
087    {
088        super(fieldPath);
089        _operator = op;
090        _value = value;
091    }
092    
093    /**
094     * Get the operator.
095     * @return the operator.
096     */
097    public Operator getOperator()
098    {
099        return _operator;
100    }
101    
102    /**
103     * Get the value.
104     * @return the value.
105     */
106    public AdaptableDate getValue()
107    {
108        return _value;
109    }
110    
111    @Override
112    public String build() throws QuerySyntaxException
113    {
114        StringBuilder query = new StringBuilder();
115        
116        if (_operator == Operator.NE)
117        {
118            NotQuery.appendNegation(query);
119        }
120        
121        query.append(_fieldPath).append("_dt:");
122        
123        if (_operator == Operator.EXISTS)
124        {
125            query.append(QueryHelper.EXISTS_VALUE);
126        }
127        else
128        {
129            appendDateValue(query, _operator, _value);
130        }
131        
132        return query.toString();
133    }
134    
135    @Override
136    public int hashCode()
137    {
138        final int prime = 31;
139        int result = super.hashCode();
140        result = prime * result + ((_operator == null) ? 0 : _operator.hashCode());
141        result = prime * result + ((_value == null) ? 0 : _value.hashCode());
142        return result;
143    }
144
145    @Override
146    public boolean equals(Object obj)
147    {
148        if (this == obj)
149        {
150            return true;
151        }
152        if (!super.equals(obj))
153        {
154            return false;
155        }
156        if (getClass() != obj.getClass())
157        {
158            return false;
159        }
160        DateQuery other = (DateQuery) obj;
161        if (_operator != other._operator)
162        {
163            return false;
164        }
165        if (_value == null)
166        {
167            if (other._value != null)
168            {
169                return false;
170            }
171        }
172        else if (!_value.equals(other._value))
173        {
174            return false;
175        }
176        return true;
177    }
178
179    /**
180     * Format and append the given date to a StringBuilder.
181     * @param query The string builder containing the query being built.
182     * @param operator The query operator.
183     * @param value The test value.
184     */
185    public static void appendDateValue(StringBuilder query, Operator operator, AdaptableDate value)
186    {
187        if (operator == Operator.EQ || operator == Operator.NE)
188        {
189            LocalDate date = value.resolveDate();
190            query.append('[')
191                .append(date.atStartOfDay().format(DATE_FORMATTER))
192                .append(" TO ")
193                .append(date.atTime(LocalTime.MAX).format(DATE_FORMATTER))
194                .append(']');
195            return;
196        }
197        
198        String strValue = _format(value, operator);
199        if (operator == Operator.GT)
200        {
201            query.append('{').append(strValue).append(" TO *]");
202        }
203        else if (operator == Operator.GE)
204        {
205            query.append('[').append(strValue).append(" TO *]");
206        }
207        else if (operator == Operator.LT)
208        {
209            query.append("[* TO ").append(strValue).append('}');
210        }
211        else if (operator == Operator.LE)
212        {
213            query.append("[* TO ").append(strValue).append(']');
214        }
215    }
216    
217    private static String _format(AdaptableDate value, Operator operator)
218    {
219        LocalDate date = value.resolveDate();
220        // We are doing this because:
221        // In Solr we store datetimes
222        // But (for the moment), we handle querying on dates only
223        // So the AdaptableDate ('value') is resolved as a LocalDate (thus, at the moment of this method call, 'date' has lost any time information)
224        // Then, depending on the operator, we specify the min or max time of the day to handle range
225        // Finally, we format the LocalDateTime to a readable string by Solr parser
226        // 
227        // Note: in the future, if we need to support querying on complete datetimes, we should call value.resolveDateTime() instead of value.resolveDate() (in a DateTimeQuery of course)
228        LocalDateTime dateTime;
229        if (Operator.LE == operator || Operator.GT == operator)
230        {
231            // LE: all datetimes of upper bound no matter their time should be included
232            // GT: all datetimes of lower bound no matter their time should be excluded
233            dateTime = date.atTime(LocalTime.MAX);
234        }
235        else
236        {
237            // GE: all datetimes of lower bound no matter their time should be included
238            // LT: all datetimes of upper bound no matter their time should be excluded
239            dateTime = date.atStartOfDay();
240        }
241        return dateTime.format(DATE_FORMATTER);
242    }
243    
244}