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}