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
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}