001/* 002 * Copyright 2010 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.filter; 017 018import java.time.LocalDate; 019import java.time.ZoneId; 020import java.time.ZonedDateTime; 021import java.time.format.DateTimeParseException; 022import java.util.ArrayList; 023import java.util.Date; 024import java.util.HashMap; 025import java.util.List; 026import java.util.Map; 027 028import org.apache.avalon.framework.configuration.Configurable; 029import org.apache.avalon.framework.configuration.Configuration; 030import org.apache.avalon.framework.configuration.ConfigurationException; 031import org.apache.avalon.framework.service.ServiceException; 032import org.apache.avalon.framework.service.ServiceManager; 033import org.apache.avalon.framework.service.Serviceable; 034 035import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 036import org.ametys.core.util.DateUtils; 037import org.ametys.plugins.repository.AmetysObjectResolver; 038import org.ametys.plugins.repository.query.SortCriteria; 039import org.ametys.plugins.repository.query.SortCriteria.SortCriterion; 040import org.ametys.plugins.repository.query.expression.AndExpression; 041import org.ametys.plugins.repository.query.expression.DateExpression; 042import org.ametys.plugins.repository.query.expression.Expression; 043import org.ametys.plugins.repository.query.expression.Expression.Operator; 044import org.ametys.plugins.repository.query.expression.MetadataExpression; 045import org.ametys.plugins.repository.query.expression.OrExpression; 046import org.ametys.runtime.plugin.component.PluginAware; 047 048/** 049 * Class representing a content filter. <br> 050 */ 051public class StaticContentFilter extends DefaultContentFilter implements Configurable, PluginAware, Serviceable 052{ 053 /** The plugin name */ 054 protected String _pluginName; 055 /** The feature name */ 056 protected String _featureName; 057 058 /** 059 * Constructor 060 */ 061 public StaticContentFilter () 062 { 063 // Empty construction needed for component 064 super(); 065 } 066 067 /** 068 * Constructor 069 * @param id The filter id 070 * @param resolver The ametys object resolver 071 * @param contentTypeExtensionPoint The extension point for content types 072 */ 073 public StaticContentFilter(String id, AmetysObjectResolver resolver, ContentTypeExtensionPoint contentTypeExtensionPoint) 074 { 075 super(id, resolver, contentTypeExtensionPoint); 076 } 077 078 /** 079 * Creates a new filter from copy of another 080 * @param id The filter unique identifier 081 * @param originalFilter The original filter to be copied 082 * @param resolver The ametys object resolver 083 * @param contentTypeExtensionPoint The extension point for content types 084 */ 085 public StaticContentFilter(String id, StaticContentFilter originalFilter, AmetysObjectResolver resolver, ContentTypeExtensionPoint contentTypeExtensionPoint) 086 { 087 super(id, originalFilter, resolver, contentTypeExtensionPoint); 088 089 _contentTypes = new ArrayList<>(originalFilter._contentTypes); 090 _metadataCondition = originalFilter._metadataCondition; 091 _contextLang = originalFilter._contextLang; 092 _length = originalFilter._length; 093 _viewName = originalFilter._viewName; 094 _metadata = new HashMap<>(originalFilter._metadata); 095 096 SortCriteria originalSC = originalFilter._sortCriteria; 097 if (originalSC != null) 098 { 099 SortCriteria sortCriteria = new SortCriteria(); 100 for (SortCriterion criterion : originalFilter.getSortCriteria().getCriteria()) 101 { 102 if (criterion.getMetadataPath() != null) 103 { 104 sortCriteria.addCriterion(criterion.getMetadataPath(), criterion.isAscending(), criterion.isNormalizedSort()); 105 } 106 else if (criterion.getJcrProperty() != null) 107 { 108 sortCriteria.addCriterion(criterion.getJcrProperty(), criterion.isAscending(), criterion.isNormalizedSort()); 109 } 110 } 111 _sortCriteria = sortCriteria; 112 } 113 114 // FIXME The filter expressions should be copied here but it missing a Expression.clone() or Expression.copy() method to do it. 115 // For now the filter expression is not used when this constructor is used, so we do nothing for now since filters will be refactored soon (for 4.3 ?) 116 } 117 118 @Override 119 public void service(ServiceManager smanager) throws ServiceException 120 { 121 _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE); 122 _contentTypeEP = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE); 123 } 124 125 @Override 126 public void setPluginInfo(String pluginName, String featureName, String id) 127 { 128 _pluginName = pluginName; 129 _featureName = featureName; 130 } 131 132 @Override 133 public void configure(Configuration configuration) throws ConfigurationException 134 { 135 _id = configuration.getAttribute("id"); 136 _contentTypes = _configureContentTypes(configuration.getChild("content-types", true)); 137 _viewName = configuration.getChild("view").getValue("main"); 138 _contextLang = _configureContextLanguage(configuration.getChild("context", true)); 139 _length = configuration.getChild("max-result", true).getValueAsInteger(Integer.MAX_VALUE); 140 _metadata = _configureMetadata(configuration.getChild("metadata", true)); 141 _metadataCondition = Condition.valueOf(configuration.getChild("metadata", true).getAttribute("condition", "AND").toUpperCase()); 142 _additionalFilterExpression = _configureComplexMetadata(configuration.getChild("metadata"), _metadataCondition); 143 _sortCriteria = _configureSortCriteria(configuration.getChild("sort-information")); 144 145 } 146 147 /** 148 * Configure the content type ids 149 * @param configuration The content types configuration 150 * @return The set of content type ids 151 * @throws ConfigurationException If an error occurs 152 */ 153 protected List<String> _configureContentTypes(Configuration configuration) throws ConfigurationException 154 { 155 List<String> cTypes = new ArrayList<>(); 156 for (Configuration cType : configuration.getChildren("type")) 157 { 158 cTypes.add(cType.getAttribute("id")); 159 } 160 return cTypes; 161 } 162 163 /** 164 * Configure simple metadata clauses (fixed string values). 165 * @param configuration The metadata configuration 166 * @return The metadata to filter by, as a Map of metadata name -> value. 167 * @throws ConfigurationException If an error occurs 168 */ 169 protected Map<String, String> _configureMetadata(Configuration configuration) throws ConfigurationException 170 { 171 Map<String, String> metadata = new HashMap<>(); 172 for (Configuration elt : configuration.getChildren("metadata")) 173 { 174 String op = elt.getAttribute("operator", null); 175 String type = elt.getAttribute("type", null); 176 177 // Process only conditions without type and operator. 178 if (op == null && type == null) 179 { 180 metadata.put(elt.getAttribute("id"), elt.getValue("")); 181 } 182 } 183 return metadata; 184 } 185 186 /** 187 * Configure complex metadata conditions (may filter on non-string metadata, 188 * and not limited to equality.) 189 * @param configuration The metadata conditions configuration. 190 * @param metadataOperator the metadata operator. 191 * @return An expression for complex metadata conditions. 192 * @throws ConfigurationException If an error occurs 193 */ 194 protected Expression _configureComplexMetadata(Configuration configuration, Condition metadataOperator) throws ConfigurationException 195 { 196 List<Expression> expressions = new ArrayList<>(); 197 198 for (Configuration elt : configuration.getChildren("metadata")) 199 { 200 String op = elt.getAttribute("operator", null); 201 String type = elt.getAttribute("type", null); 202 203 // Process only metadata with a type and operator. 204 if (op != null && type != null) 205 { 206 String id = elt.getAttribute("id"); 207 String value = elt.getValue(""); 208 209 // Date expression. 210 if (type.equals("date")) 211 { 212 expressions.add(_getComplexDateExpression(id, op, value, elt)); 213 } 214 } 215 else if (op == null && type != null 216 || op != null && type == null) 217 { 218 throw new ConfigurationException("Both type and operator must be specified for complex metadata conditions.", elt); 219 } 220 } 221 222 Expression[] exprArr = expressions.toArray(new Expression[expressions.size()]); 223 224 Expression expr = null; 225 if (metadataOperator == Condition.AND) 226 { 227 expr = new AndExpression(exprArr); 228 } 229 else if (metadataOperator == Condition.OR) 230 { 231 expr = new OrExpression(exprArr); 232 } 233 234 return expr; 235 } 236 237 /** 238 * Get a complex date metadata expression. 239 * @param metadataName the metadata name. 240 * @param operator the comparison operator. 241 * @param value the compared value. 242 * @param configuration the configuration being processed. 243 * @return the date Expression. 244 * @throws ConfigurationException if the configuration is not valid. 245 */ 246 protected Expression _getComplexDateExpression(String metadataName, String operator, String value, Configuration configuration) throws ConfigurationException 247 { 248 int operatorOffset = 0; 249 250 Operator op = Operator.EQ; 251 if ("eq".equalsIgnoreCase(operator)) 252 { 253 op = Operator.EQ; 254 } 255 else if ("gte".equalsIgnoreCase(operator)) 256 { 257 op = Operator.GE; 258 } 259 else if ("gt".equalsIgnoreCase(operator)) 260 { 261 op = Operator.GE; 262 operatorOffset = 1; 263 } 264 else if ("lte".equalsIgnoreCase(operator)) 265 { 266 op = Operator.LT; 267 operatorOffset = 1; 268 } 269 else if ("lt".equalsIgnoreCase(operator)) 270 { 271 op = Operator.LT; 272 } 273 else 274 { 275 throw new ConfigurationException("Invalid date operator: '" + operator + "'", configuration); 276 } 277 278 // Date value can be one of "now"/"today", "2013-09-07", 279 // or "-7" to specify a date relative to the current date. 280 Date date = null; 281 Integer valueOffset = null; 282 if ("now".equalsIgnoreCase(value) || "today".equalsIgnoreCase(value)) 283 { 284 // "now" or "today". 285 valueOffset = 0; 286 } 287 else 288 { 289 try 290 { 291 // Try to parse as a fixed date. 292 date = Date.from(LocalDate.parse(value).atStartOfDay(ZoneId.systemDefault()).plusDays(operatorOffset).toInstant()); 293 } 294 catch (DateTimeParseException | IllegalArgumentException e) 295 { 296 try 297 { 298 // If not a fixed date, try to parse as an offset (signed integer). 299 valueOffset = Integer.parseInt(value); 300 } 301 catch (NumberFormatException nfe) 302 { 303 // Ignore: leave date value null to throw an exception. 304 } 305 } 306 } 307 308 if (date != null) 309 { 310 // Fixed date: standard date expression. 311 return new DateExpression(metadataName, op, date); 312 } 313 else if (valueOffset != null) 314 { 315 // Offset: dynamic date expression. 316 int offset = operatorOffset + valueOffset; 317 return new DynamicDateExpression(metadataName, op, offset); 318 } 319 else 320 { 321 throw new ConfigurationException("Invalid date value: '" + value + "'", configuration); 322 } 323 } 324 325 /** 326 * Configure the sort criteria 327 * @param configuration The sort criteria configuration 328 * @return The sort criteria 329 * @throws ConfigurationException If an error occurs 330 */ 331 protected SortCriteria _configureSortCriteria(Configuration configuration) throws ConfigurationException 332 { 333 SortCriteria sortCriteria = null; 334 if (configuration != null) 335 { 336 sortCriteria = new SortCriteria(); 337 for (Configuration sort : configuration.getChildren("sort")) 338 { 339 sortCriteria.addCriterion(sort.getAttribute("metadataId"), sort.getAttributeAsBoolean("ascending", false), sort.getAttributeAsBoolean("lower-case", false)); 340 } 341 } 342 return sortCriteria; 343 } 344 345 /** 346 * Configure the context language 347 * @param configuration The context configuration 348 * @return The context language 349 * @throws ConfigurationException If an error occurs 350 */ 351 protected ContextLanguage _configureContextLanguage (Configuration configuration) throws ConfigurationException 352 { 353 String context = configuration.getAttribute("lang", "current"); 354 if (context.equals(ContextLanguage.OTHERS.toString())) 355 { 356 return ContextLanguage.OTHERS; 357 } 358 else if (context.equals(ContextLanguage.ALL.toString())) 359 { 360 return ContextLanguage.ALL; 361 } 362 363 return ContextLanguage.CURRENT; 364 } 365 366 /** 367 * Constructs an {@link Expression} representing a comparison between a metadata and the time the build() method is called. 368 */ 369 public static class DynamicDateExpression implements Expression 370 { 371 372 private MetadataExpression _metadataExpr; 373 private Operator _operator; 374 /** 375 * The offset value. 376 */ 377 private int _offsetValue; 378 379 /** 380 * Create a comparison Expression relative to the current date, with an offset, normalizing the 381 * @param metadata the metadata name 382 * @param operator the operator to make the comparison 383 * @param offsetValue the offset value. 384 */ 385 public DynamicDateExpression(String metadata, Operator operator, int offsetValue) 386 { 387 _operator = operator; 388 _metadataExpr = new MetadataExpression(metadata); 389 _offsetValue = offsetValue; 390 } 391 392 @Override 393 public String build() 394 { 395 ZonedDateTime dateTime = LocalDate.now().atStartOfDay(ZoneId.systemDefault()).plusDays(_offsetValue); 396 397 StringBuffer buff = new StringBuffer(); 398 399 buff.append(_metadataExpr.build()).append(' ').append(_operator).append(" xs:dateTime('"); 400 DateUtils.getISODateTimeFormatter().formatTo(dateTime, buff); 401 buff.append("')"); 402 403 return buff.toString(); 404 } 405 406 } 407 408}