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) || (op != null && type == null)) 268 { 269 throw new ConfigurationException("Both type and operator must be specified for complex metadata conditions.", elt); 270 } 271 } 272 273 Expression[] exprArr = expressions.toArray(new Expression[expressions.size()]); 274 275 Expression expr = null; 276 if (metadataOperator == Condition.AND) 277 { 278 expr = new AndExpression(exprArr); 279 } 280 else if (metadataOperator == Condition.OR) 281 { 282 expr = new OrExpression(exprArr); 283 } 284 285 return expr; 286 } 287 288 /** 289 * Get a complex date metadata expression. 290 * @param metadataName the metadata name. 291 * @param operator the comparison operator. 292 * @param value the compared value. 293 * @param configuration the configuration being processed. 294 * @return the date Expression. 295 * @throws ConfigurationException if the configuration is not valid. 296 */ 297 protected Expression _getComplexDateExpression(String metadataName, String operator, String value, Configuration configuration) throws ConfigurationException 298 { 299 int operatorOffset = 0; 300 301 Operator op = Operator.EQ; 302 if ("eq".equalsIgnoreCase(operator)) 303 { 304 op = Operator.EQ; 305 } 306 else if ("gte".equalsIgnoreCase(operator)) 307 { 308 op = Operator.GE; 309 } 310 else if ("gt".equalsIgnoreCase(operator)) 311 { 312 op = Operator.GE; 313 operatorOffset = 1; 314 } 315 else if ("lte".equalsIgnoreCase(operator)) 316 { 317 op = Operator.LT; 318 operatorOffset = 1; 319 } 320 else if ("lt".equalsIgnoreCase(operator)) 321 { 322 op = Operator.LT; 323 } 324 else 325 { 326 throw new ConfigurationException("Invalid date operator: '" + operator + "'", configuration); 327 } 328 329 // Date value can be one of "now"/"today", "2013-09-07", 330 // or "-7" to specify a date relative to the current date. 331 Date date = null; 332 Integer valueOffset = null; 333 if ("now".equalsIgnoreCase(value) || "today".equalsIgnoreCase(value)) 334 { 335 // "now" or "today". 336 valueOffset = 0; 337 } 338 else 339 { 340 try 341 { 342 // Try to parse as a fixed date. 343 date = Date.from(LocalDate.parse(value).atStartOfDay(ZoneId.systemDefault()).plusDays(operatorOffset).toInstant()); 344 } 345 catch (DateTimeParseException | IllegalArgumentException e) 346 { 347 try 348 { 349 // If not a fixed date, try to parse as an offset (signed integer). 350 valueOffset = Integer.parseInt(value); 351 } 352 catch (NumberFormatException nfe) 353 { 354 // Ignore: leave date value null to throw an exception. 355 } 356 } 357 } 358 359 if (date != null) 360 { 361 // Fixed date: standard date expression. 362 return new DateExpression(metadataName, op, date); 363 } 364 else if (valueOffset != null) 365 { 366 // Offset: dynamic date expression. 367 int offset = operatorOffset + valueOffset; 368 return new DynamicDateExpression(metadataName, op, offset); 369 } 370 else 371 { 372 throw new ConfigurationException("Invalid date value: '" + value + "'", configuration); 373 } 374 } 375 376 /** 377 * Configure the sort criteria 378 * @param configuration The sort criteria configuration 379 * @return The sort criteria 380 * @throws ConfigurationException If an error occurs 381 */ 382 protected SortCriteria _configureSortCriteria(Configuration configuration) throws ConfigurationException 383 { 384 SortCriteria sortCriteria = null; 385 if (configuration != null) 386 { 387 sortCriteria = new SortCriteria(); 388 for (Configuration sort : configuration.getChildren("sort")) 389 { 390 sortCriteria.addCriterion(sort.getAttribute("metadataId"), sort.getAttributeAsBoolean("ascending", false), sort.getAttributeAsBoolean("lower-case", false)); 391 } 392 } 393 return sortCriteria; 394 } 395 396 /** 397 * Configure the context search 398 * @param configuration The context configuration 399 * @return The search context 400 * @throws ConfigurationException If an error occurs 401 */ 402 protected Context _configureContext (Configuration configuration) throws ConfigurationException 403 { 404 String context = configuration.getAttribute("type", "current-site"); 405 if (context.equals(Context.OTHER_SITES.toString())) 406 { 407 return Context.OTHER_SITES; 408 } 409 else if (context.equals(Context.SITES.toString())) 410 { 411 return Context.SITES; 412 } 413 else if (context.equals(Context.CHILD_PAGES.toString())) 414 { 415 return Context.CHILD_PAGES; 416 } 417 else if (context.equals(Context.SITES_LIST.toString())) 418 { 419 return Context.SITES_LIST; 420 } 421 else if (context.equals(Context.NO_SITE.toString())) 422 { 423 return Context.NO_SITE; 424 } 425 426 return Context.CURRENT_SITE; 427 } 428 429 /** 430 * Configure the context language 431 * @param configuration The context configuration 432 * @return The context language 433 * @throws ConfigurationException If an error occurs 434 */ 435 protected ContextLanguage _configureContextLanguage (Configuration configuration) throws ConfigurationException 436 { 437 String context = configuration.getAttribute("lang", "current"); 438 if (context.equals(ContextLanguage.OTHERS.toString())) 439 { 440 return ContextLanguage.OTHERS; 441 } 442 else if (context.equals(ContextLanguage.ALL.toString())) 443 { 444 return ContextLanguage.ALL; 445 } 446 447 return ContextLanguage.CURRENT; 448 } 449 450 /** 451 * Configure the depth search 452 * @param configuration The depth configuration 453 * @return The depth 454 * @throws ConfigurationException If an error occurs 455 */ 456 protected int _configureDepth (Configuration configuration) throws ConfigurationException 457 { 458 return configuration.getAttributeAsInteger("depth", 0); 459 } 460}