001/*
002 *  Copyright 2022 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 * Base class for all date and operator-based queries.
028 */
029public abstract class AbstractDateOperatorQuery extends AbstractOperatorQuery<AdaptableDate>
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    /**
036     * Build a CreationDateQuery.
037     * @param fieldName the Solr field name
038     * @param op the operator.
039     * @param value the value.
040     */
041    public AbstractDateOperatorQuery(String fieldName, Operator op, AdaptableDate value)
042    {
043        super(fieldName, op, value);
044    }
045    
046    @Override
047    public String build() throws QuerySyntaxException
048    {
049        StringBuilder query = new StringBuilder();
050        
051        if (getOperator() == Operator.EXISTS)
052        {
053            query.append(getFieldName()).append(':').append(QueryHelper.EXISTS_VALUE);
054            return query.toString();
055        }
056        
057        if (getOperator() == Operator.NE)
058        {
059            NotQuery.appendNegation(query);
060        }
061        
062        query.append(getFieldName()).append(':');
063        
064        appendDateValue(query, getOperator(), getValue());
065        
066        return query.toString();
067    }
068    
069    /**
070     * Format and append the given date to a StringBuilder.
071     * @param query The string builder containing the query being built.
072     * @param operator The query operator.
073     * @param value The test value.
074     */
075    protected void appendDateValue(StringBuilder query, Operator operator, AdaptableDate value)
076    {
077        if (operator == Operator.EQ || operator == Operator.NE)
078        {
079            LocalDate date = value.resolveDate();
080            query.append('[')
081                .append(date.atStartOfDay().format(DATE_FORMATTER))
082                .append(" TO ")
083                .append(date.atTime(LocalTime.MAX).format(DATE_FORMATTER))
084                .append(']');
085            return;
086        }
087        
088        String strValue = _format(value, operator);
089        if (operator == Operator.GT)
090        {
091            query.append('{').append(strValue).append(" TO *]");
092        }
093        else if (operator == Operator.GE)
094        {
095            query.append('[').append(strValue).append(" TO *]");
096        }
097        else if (operator == Operator.LT)
098        {
099            query.append("[* TO ").append(strValue).append('}');
100        }
101        else if (operator == Operator.LE)
102        {
103            query.append("[* TO ").append(strValue).append(']');
104        }
105    }
106    
107    private String _format(AdaptableDate value, Operator operator)
108    {
109        LocalDate date = value.resolveDate();
110        // We are doing this because:
111        // In Solr we store datetimes
112        // But (for the moment), we handle querying on dates only
113        // So the AdaptableDate ('value') is resolved as a LocalDate (thus, at the moment of this method call, 'date' has lost any time information)
114        // Then, depending on the operator, we specify the min or max time of the day to handle range
115        // Finally, we format the LocalDateTime to a readable string by Solr parser
116        // 
117        // 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)
118        LocalDateTime dateTime;
119        if (Operator.LE == operator || Operator.GT == operator)
120        {
121            // LE: all datetimes of upper bound no matter their time should be included
122            // GT: all datetimes of lower bound no matter their time should be excluded
123            dateTime = date.atTime(LocalTime.MAX);
124        }
125        else
126        {
127            // GE: all datetimes of lower bound no matter their time should be included
128            // LT: all datetimes of upper bound no matter their time should be excluded
129            dateTime = date.atStartOfDay();
130        }
131        return dateTime.format(DATE_FORMATTER);
132    }
133}