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) || (op != null && type == null)) 216 { 217 throw new ConfigurationException("Both type and operator must be specified for complex metadata conditions.", elt); 218 } 219 } 220 221 Expression[] exprArr = expressions.toArray(new Expression[expressions.size()]); 222 223 Expression expr = null; 224 if (metadataOperator == Condition.AND) 225 { 226 expr = new AndExpression(exprArr); 227 } 228 else if (metadataOperator == Condition.OR) 229 { 230 expr = new OrExpression(exprArr); 231 } 232 233 return expr; 234 } 235 236 /** 237 * Get a complex date metadata expression. 238 * @param metadataName the metadata name. 239 * @param operator the comparison operator. 240 * @param value the compared value. 241 * @param configuration the configuration being processed. 242 * @return the date Expression. 243 * @throws ConfigurationException if the configuration is not valid. 244 */ 245 protected Expression _getComplexDateExpression(String metadataName, String operator, String value, Configuration configuration) throws ConfigurationException 246 { 247 int operatorOffset = 0; 248 249 Operator op = Operator.EQ; 250 if ("eq".equalsIgnoreCase(operator)) 251 { 252 op = Operator.EQ; 253 } 254 else if ("gte".equalsIgnoreCase(operator)) 255 { 256 op = Operator.GE; 257 } 258 else if ("gt".equalsIgnoreCase(operator)) 259 { 260 op = Operator.GE; 261 operatorOffset = 1; 262 } 263 else if ("lte".equalsIgnoreCase(operator)) 264 { 265 op = Operator.LT; 266 operatorOffset = 1; 267 } 268 else if ("lt".equalsIgnoreCase(operator)) 269 { 270 op = Operator.LT; 271 } 272 else 273 { 274 throw new ConfigurationException("Invalid date operator: '" + operator + "'", configuration); 275 } 276 277 // Date value can be one of "now"/"today", "2013-09-07", 278 // or "-7" to specify a date relative to the current date. 279 Date date = null; 280 Integer valueOffset = null; 281 if ("now".equalsIgnoreCase(value) || "today".equalsIgnoreCase(value)) 282 { 283 // "now" or "today". 284 valueOffset = 0; 285 } 286 else 287 { 288 try 289 { 290 // Try to parse as a fixed date. 291 date = Date.from(LocalDate.parse(value).atStartOfDay(ZoneId.systemDefault()).plusDays(operatorOffset).toInstant()); 292 } 293 catch (DateTimeParseException | IllegalArgumentException e) 294 { 295 try 296 { 297 // If not a fixed date, try to parse as an offset (signed integer). 298 valueOffset = Integer.parseInt(value); 299 } 300 catch (NumberFormatException nfe) 301 { 302 // Ignore: leave date value null to throw an exception. 303 } 304 } 305 } 306 307 if (date != null) 308 { 309 // Fixed date: standard date expression. 310 return new DateExpression(metadataName, op, date); 311 } 312 else if (valueOffset != null) 313 { 314 // Offset: dynamic date expression. 315 int offset = operatorOffset + valueOffset; 316 return new DynamicDateExpression(metadataName, op, offset); 317 } 318 else 319 { 320 throw new ConfigurationException("Invalid date value: '" + value + "'", configuration); 321 } 322 } 323 324 /** 325 * Configure the sort criteria 326 * @param configuration The sort criteria configuration 327 * @return The sort criteria 328 * @throws ConfigurationException If an error occurs 329 */ 330 protected SortCriteria _configureSortCriteria(Configuration configuration) throws ConfigurationException 331 { 332 SortCriteria sortCriteria = null; 333 if (configuration != null) 334 { 335 sortCriteria = new SortCriteria(); 336 for (Configuration sort : configuration.getChildren("sort")) 337 { 338 sortCriteria.addCriterion(sort.getAttribute("metadataId"), sort.getAttributeAsBoolean("ascending", false), sort.getAttributeAsBoolean("lower-case", false)); 339 } 340 } 341 return sortCriteria; 342 } 343 344 /** 345 * Configure the context language 346 * @param configuration The context configuration 347 * @return The context language 348 * @throws ConfigurationException If an error occurs 349 */ 350 protected ContextLanguage _configureContextLanguage (Configuration configuration) throws ConfigurationException 351 { 352 String context = configuration.getAttribute("lang", "current"); 353 if (context.equals(ContextLanguage.OTHERS.toString())) 354 { 355 return ContextLanguage.OTHERS; 356 } 357 else if (context.equals(ContextLanguage.ALL.toString())) 358 { 359 return ContextLanguage.ALL; 360 } 361 362 return ContextLanguage.CURRENT; 363 } 364 365 /** 366 * Constructs an {@link Expression} representing a comparison between a metadata and the time the build() method is called. 367 */ 368 public static class DynamicDateExpression implements Expression 369 { 370 371 private MetadataExpression _metadataExpr; 372 private Operator _operator; 373 /** 374 * The offset value. 375 */ 376 private int _offsetValue; 377 378 /** 379 * Create a comparison Expression relative to the current date, with an offset, normalizing the 380 * @param metadata the metadata name 381 * @param operator the operator to make the comparison 382 * @param offsetValue the offset value. 383 */ 384 public DynamicDateExpression(String metadata, Operator operator, int offsetValue) 385 { 386 _operator = operator; 387 _metadataExpr = new MetadataExpression(metadata); 388 _offsetValue = offsetValue; 389 } 390 391 @Override 392 public String build() 393 { 394 ZonedDateTime dateTime = LocalDate.now().atStartOfDay(ZoneId.systemDefault()).plusDays(_offsetValue); 395 396 StringBuffer buff = new StringBuffer(); 397 398 buff.append(_metadataExpr.build()).append(' ').append(_operator).append(" xs:dateTime('"); 399 DateUtils.getISODateTimeFormatter().formatTo(dateTime, buff); 400 buff.append("')"); 401 402 return buff.toString(); 403 } 404 405 } 406 407}