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.web.filter; 017 018import java.time.LocalDate; 019import java.time.ZoneId; 020import java.time.format.DateTimeParseException; 021import java.util.ArrayList; 022import java.util.Date; 023import java.util.HashMap; 024import java.util.List; 025import java.util.Map; 026 027import org.apache.avalon.framework.configuration.Configurable; 028import org.apache.avalon.framework.configuration.Configuration; 029import org.apache.avalon.framework.configuration.ConfigurationException; 030import org.apache.avalon.framework.service.ServiceException; 031import org.apache.avalon.framework.service.ServiceManager; 032import org.apache.avalon.framework.service.Serviceable; 033 034import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 035import org.ametys.cms.filter.StaticContentFilter.DynamicDateExpression; 036import org.ametys.cms.tag.TagProviderExtensionPoint; 037import org.ametys.plugins.repository.AmetysObjectResolver; 038import org.ametys.plugins.repository.query.SortCriteria; 039import org.ametys.plugins.repository.query.expression.AndExpression; 040import org.ametys.plugins.repository.query.expression.DateExpression; 041import org.ametys.plugins.repository.query.expression.Expression; 042import org.ametys.plugins.repository.query.expression.Expression.Operator; 043import org.ametys.plugins.repository.query.expression.OrExpression; 044import org.ametys.runtime.i18n.I18nizableText; 045import org.ametys.runtime.plugin.component.PluginAware; 046import org.ametys.web.repository.site.SiteManager; 047 048/** 049 * This class represents a static filter for contents 050 * 051 */ 052public class StaticWebContentFilter extends DefaultWebContentFilter implements Configurable, PluginAware, Serviceable 053{ 054 /** The plugin name */ 055 protected String _pluginName; 056 /** The feature name */ 057 protected String _featureName; 058 059 /** 060 * Constructor 061 */ 062 public StaticWebContentFilter () 063 { 064 // Empty construction needed for component 065 super(); 066 } 067 068 /** 069 * Constructor 070 * @param id The filter id 071 * @param resolver The ametys object resolver 072 * @param contentTypeExtensionPoint The extension point for content types 073 * @param siteManager The site manager 074 * @param tagProviderEP The tag provider 075 */ 076 public StaticWebContentFilter(String id, AmetysObjectResolver resolver, ContentTypeExtensionPoint contentTypeExtensionPoint, SiteManager siteManager, TagProviderExtensionPoint tagProviderEP) 077 { 078 super(id, resolver, contentTypeExtensionPoint, siteManager, tagProviderEP); 079 } 080 081 /** 082 * Creates a new filter from copy of another 083 * @param id The filter unique identifier 084 * @param originalFilter The original filter to be copied 085 * @param resolver The ametys object resolver 086 * @param contentTypeExtensionPoint The extension point for content types 087 * @param siteManager The site manager 088 * @param tagProviderEP The tag provider 089 */ 090 public StaticWebContentFilter(String id, StaticWebContentFilter originalFilter, AmetysObjectResolver resolver, ContentTypeExtensionPoint contentTypeExtensionPoint, SiteManager siteManager, TagProviderExtensionPoint tagProviderEP) 091 { 092 super(id, originalFilter, resolver, contentTypeExtensionPoint, siteManager, tagProviderEP); 093 } 094 095 @Override 096 public void service(ServiceManager smanager) throws ServiceException 097 { 098 _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE); 099 _contentTypeEP = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE); 100 _siteManager = (SiteManager) smanager.lookup(SiteManager.ROLE); 101 _tagProviderEP = (TagProviderExtensionPoint) smanager.lookup(TagProviderExtensionPoint.ROLE); 102 } 103 104 @Override 105 public void setPluginInfo(String pluginName, String featureName, String id) 106 { 107 _pluginName = pluginName; 108 _featureName = featureName; 109 _id = id; 110 } 111 112 @Override 113 public void configure(Configuration configuration) throws ConfigurationException 114 { 115 _title = _configureTitle (configuration.getChild("title", true)); 116 _description = _configureDescription (configuration.getChild("description", true)); 117 _contentTypes = _configureContentTypes(configuration.getChild("content-types", true)); 118 _viewName = configuration.getChild("view").getValue("main"); 119 _length = configuration.getChild("max-result", true).getValueAsInteger(Integer.MAX_VALUE); 120 _metadata = _configureMetadata(configuration.getChild("metadata", true)); 121 _metadataCondition = Condition.valueOf(configuration.getChild("metadata", true).getAttribute("condition", "AND").toUpperCase()); 122 _additionalFilterExpression = _configureComplexMetadata(configuration.getChild("metadata"), _metadataCondition); 123 _sortCriteria = _configureSortCriteria(configuration.getChild("sort-information")); 124 _maskOrphan = configuration.getChild("mask-orphan", true).getValueAsBoolean(false); 125 if (!_maskOrphan) 126 { 127 // @Deprecated 128 _maskOrphan = configuration.getAttributeAsBoolean("maskOrphan", false); 129 } 130 boolean handleUserAccess = configuration.getChild("handle-user-access").getValueAsBoolean(false); 131 _accessLimitation = handleUserAccess ? AccessLimitation.USER_ACCESS : AccessLimitation.PAGE_ACCESS; 132 133 _configureSearchContexts(configuration); 134 } 135 136 /** 137 * Configure the filter's title 138 * @param configuration The title configuration 139 * @return The filter's title 140 * @throws ConfigurationException If an error occurs 141 */ 142 protected I18nizableText _configureTitle (Configuration configuration) throws ConfigurationException 143 { 144 if (configuration.getAttributeAsBoolean("i18n", false)) 145 { 146 return new I18nizableText("plugin." + _pluginName, configuration.getValue()); 147 } 148 else 149 { 150 return new I18nizableText(configuration.getValue("")); 151 } 152 } 153 154 /** 155 * Configure the filter's description 156 * @param configuration The description configuration 157 * @return The filter's description 158 * @throws ConfigurationException If an error occurs 159 */ 160 protected I18nizableText _configureDescription (Configuration configuration) throws ConfigurationException 161 { 162 if (configuration.getAttributeAsBoolean("i18n", false)) 163 { 164 return new I18nizableText("plugin." + _pluginName, configuration.getValue()); 165 } 166 else 167 { 168 return new I18nizableText(configuration.getValue("")); 169 } 170 } 171 172 /** 173 * Configure the content type ids 174 * @param configuration The content types configuration 175 * @return The set of content type ids 176 * @throws ConfigurationException If an error occurs 177 */ 178 protected List<String> _configureContentTypes(Configuration configuration) throws ConfigurationException 179 { 180 List<String> cTypes = new ArrayList<>(); 181 for (Configuration cType : configuration.getChildren("type")) 182 { 183 cTypes.add(cType.getAttribute("id")); 184 } 185 return cTypes; 186 } 187 188 /** 189 * Configure the search contexts. 190 * @param configuration the filter configuration. 191 * @throws ConfigurationException if an error occurs. 192 */ 193 protected void _configureSearchContexts(Configuration configuration) throws ConfigurationException 194 { 195 FilterSearchContext params = addSearchContext(); 196 197 Configuration tagsConf = configuration.getChild("tags", true); 198 for (Configuration tag : tagsConf.getChildren("tag")) 199 { 200 params.addTag(tag.getAttribute("key")); 201 } 202 Condition condition = Condition.valueOf(tagsConf.getAttribute("condition", "AND").toUpperCase()); 203 boolean strict = tagsConf.getAttributeAsBoolean("strict", true); 204 Context context = _configureContext(configuration.getChild("context", true)); 205 ContextLanguage contextLang = _configureContextLanguage(configuration.getChild("context", true)); 206 int depth = _configureDepth(configuration.getChild("context", true)); 207 208 params.setContext(context); 209 params.setContextLanguage(contextLang); 210 params.setDepth(depth); 211 params.setTagsCondition(condition); 212 params.setTagsAutoPosting(!strict); 213 } 214 215 /** 216 * Configure simple metadata clauses (fixed string values). 217 * @param configuration The metadata configuration 218 * @return The metadata to filter by, as a Map of metadata name -> value. 219 * @throws ConfigurationException If an error occurs 220 */ 221 protected Map<String, String> _configureMetadata(Configuration configuration) throws ConfigurationException 222 { 223 Map<String, String> metadata = new HashMap<>(); 224 for (Configuration elt : configuration.getChildren("metadata")) 225 { 226 String op = elt.getAttribute("operator", null); 227 String type = elt.getAttribute("type", null); 228 229 // Process only conditions without type and operator. 230 if (op == null && type == null) 231 { 232 metadata.put(elt.getAttribute("id"), elt.getValue(null)); 233 } 234 } 235 return metadata; 236 } 237 238 /** 239 * Configure complex metadata conditions (may filter on non-string metadata, 240 * and not limited to equality.) 241 * @param configuration The metadata conditions configuration. 242 * @param metadataOperator the metadata operator. 243 * @return An expression for complex metadata conditions. 244 * @throws ConfigurationException If an error occurs 245 */ 246 protected Expression _configureComplexMetadata(Configuration configuration, Condition metadataOperator) throws ConfigurationException 247 { 248 List<Expression> expressions = new ArrayList<>(); 249 250 for (Configuration elt : configuration.getChildren("metadata")) 251 { 252 String op = elt.getAttribute("operator", null); 253 String type = elt.getAttribute("type", null); 254 255 // Process only metadata with a type and operator. 256 if (op != null && type != null) 257 { 258 String id = elt.getAttribute("id"); 259 String value = elt.getValue(""); 260 261 // Date expression. 262 if (type.equals("date")) 263 { 264 expressions.add(_getComplexDateExpression(id, op, value, elt)); 265 } 266 } 267 else if (op == null && type != null 268 || op != null && type == null) 269 { 270 throw new ConfigurationException("Both type and operator must be specified for complex metadata conditions.", elt); 271 } 272 } 273 274 Expression[] exprArr = expressions.toArray(new Expression[expressions.size()]); 275 276 Expression expr = null; 277 if (metadataOperator == Condition.AND) 278 { 279 expr = new AndExpression(exprArr); 280 } 281 else if (metadataOperator == Condition.OR) 282 { 283 expr = new OrExpression(exprArr); 284 } 285 286 return expr; 287 } 288 289 /** 290 * Get a complex date metadata expression. 291 * @param metadataName the metadata name. 292 * @param operator the comparison operator. 293 * @param value the compared value. 294 * @param configuration the configuration being processed. 295 * @return the date Expression. 296 * @throws ConfigurationException if the configuration is not valid. 297 */ 298 protected Expression _getComplexDateExpression(String metadataName, String operator, String value, Configuration configuration) throws ConfigurationException 299 { 300 int operatorOffset = 0; 301 302 Operator op = Operator.EQ; 303 if ("eq".equalsIgnoreCase(operator)) 304 { 305 op = Operator.EQ; 306 } 307 else if ("gte".equalsIgnoreCase(operator)) 308 { 309 op = Operator.GE; 310 } 311 else if ("gt".equalsIgnoreCase(operator)) 312 { 313 op = Operator.GE; 314 operatorOffset = 1; 315 } 316 else if ("lte".equalsIgnoreCase(operator)) 317 { 318 op = Operator.LT; 319 operatorOffset = 1; 320 } 321 else if ("lt".equalsIgnoreCase(operator)) 322 { 323 op = Operator.LT; 324 } 325 else 326 { 327 throw new ConfigurationException("Invalid date operator: '" + operator + "'", configuration); 328 } 329 330 // Date value can be one of "now"/"today", "2013-09-07", 331 // or "-7" to specify a date relative to the current date. 332 Date date = null; 333 Integer valueOffset = null; 334 if ("now".equalsIgnoreCase(value) || "today".equalsIgnoreCase(value)) 335 { 336 // "now" or "today". 337 valueOffset = 0; 338 } 339 else 340 { 341 try 342 { 343 // Try to parse as a fixed date. 344 date = Date.from(LocalDate.parse(value).atStartOfDay(ZoneId.systemDefault()).plusDays(operatorOffset).toInstant()); 345 } 346 catch (DateTimeParseException | IllegalArgumentException e) 347 { 348 try 349 { 350 // If not a fixed date, try to parse as an offset (signed integer). 351 valueOffset = Integer.parseInt(value); 352 } 353 catch (NumberFormatException nfe) 354 { 355 // Ignore: leave date value null to throw an exception. 356 } 357 } 358 } 359 360 if (date != null) 361 { 362 // Fixed date: standard date expression. 363 return new DateExpression(metadataName, op, date); 364 } 365 else if (valueOffset != null) 366 { 367 // Offset: dynamic date expression. 368 int offset = operatorOffset + valueOffset; 369 return new DynamicDateExpression(metadataName, op, offset); 370 } 371 else 372 { 373 throw new ConfigurationException("Invalid date value: '" + value + "'", configuration); 374 } 375 } 376 377 /** 378 * Configure the sort criteria 379 * @param configuration The sort criteria configuration 380 * @return The sort criteria 381 * @throws ConfigurationException If an error occurs 382 */ 383 protected SortCriteria _configureSortCriteria(Configuration configuration) throws ConfigurationException 384 { 385 SortCriteria sortCriteria = null; 386 if (configuration != null) 387 { 388 sortCriteria = new SortCriteria(); 389 for (Configuration sort : configuration.getChildren("sort")) 390 { 391 sortCriteria.addCriterion(sort.getAttribute("metadataId"), sort.getAttributeAsBoolean("ascending", false), sort.getAttributeAsBoolean("lower-case", false)); 392 } 393 } 394 return sortCriteria; 395 } 396 397 /** 398 * Configure the context search 399 * @param configuration The context configuration 400 * @return The search context 401 * @throws ConfigurationException If an error occurs 402 */ 403 protected Context _configureContext (Configuration configuration) throws ConfigurationException 404 { 405 String context = configuration.getAttribute("type", "current-site"); 406 if (context.equals(Context.OTHER_SITES.toString())) 407 { 408 return Context.OTHER_SITES; 409 } 410 else if (context.equals(Context.SITES.toString())) 411 { 412 return Context.SITES; 413 } 414 else if (context.equals(Context.CHILD_PAGES.toString())) 415 { 416 return Context.CHILD_PAGES; 417 } 418 else if (context.equals(Context.SITES_LIST.toString())) 419 { 420 return Context.SITES_LIST; 421 } 422 else if (context.equals(Context.NO_SITE.toString())) 423 { 424 return Context.NO_SITE; 425 } 426 427 return Context.CURRENT_SITE; 428 } 429 430 /** 431 * Configure the context language 432 * @param configuration The context configuration 433 * @return The context language 434 * @throws ConfigurationException If an error occurs 435 */ 436 protected ContextLanguage _configureContextLanguage (Configuration configuration) throws ConfigurationException 437 { 438 String context = configuration.getAttribute("lang", "current"); 439 if (context.equals(ContextLanguage.OTHERS.toString())) 440 { 441 return ContextLanguage.OTHERS; 442 } 443 else if (context.equals(ContextLanguage.ALL.toString())) 444 { 445 return ContextLanguage.ALL; 446 } 447 448 return ContextLanguage.CURRENT; 449 } 450 451 /** 452 * Configure the depth search 453 * @param configuration The depth configuration 454 * @return The depth 455 * @throws ConfigurationException If an error occurs 456 */ 457 protected int _configureDepth (Configuration configuration) throws ConfigurationException 458 { 459 return configuration.getAttributeAsInteger("depth", 0); 460 } 461}