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 -&gt; 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}