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    }
101    
102    @Override
103    public void setPluginInfo(String pluginName, String featureName, String id)
104    {
105        _pluginName = pluginName;
106        _featureName = featureName;
107        _id = id;
108    }
109    
110    @Override
111    public void configure(Configuration configuration) throws ConfigurationException
112    {
113        _title = _configureTitle (configuration.getChild("title", true));
114        _description = _configureDescription (configuration.getChild("description", true));
115        _contentTypes = _configureContentTypes(configuration.getChild("content-types", true));
116        _metadataSetName = configuration.getChild("view").getValue("main");
117        _length = configuration.getChild("max-result", true).getValueAsInteger(Integer.MAX_VALUE);
118        _metadata = _configureMetadata(configuration.getChild("metadata", true));
119        _metadataCondition = Condition.valueOf(configuration.getChild("metadata", true).getAttribute("condition", "AND").toUpperCase());
120        _additionalFilterExpression = _configureComplexMetadata(configuration.getChild("metadata"), _metadataCondition);
121        _sortCriteria = _configureSortCriteria(configuration.getChild("sort-information"));
122        _maskOrphan = configuration.getChild("mask-orphan", true).getValueAsBoolean(false);
123        if (!_maskOrphan)
124        {
125            // @Deprecated
126            _maskOrphan = configuration.getAttributeAsBoolean("maskOrphan", false);
127        }
128        boolean handleUserAccess = configuration.getChild("handle-user-access").getValueAsBoolean(false);
129        _accessLimitation = handleUserAccess ? AccessLimitation.USER_ACCESS : AccessLimitation.PAGE_ACCESS;
130        
131        _configureSearchContexts(configuration);
132    }
133    
134    /**
135     * Configure the filter's title
136     * @param configuration The title configuration
137     * @return The filter's title
138     * @throws ConfigurationException If an error occurs
139     */
140    protected I18nizableText _configureTitle (Configuration configuration) throws ConfigurationException
141    {
142        if (configuration.getAttributeAsBoolean("i18n", false))
143        {
144            return new I18nizableText("plugin." + _pluginName, configuration.getValue());
145        }
146        else 
147        {
148            return new I18nizableText(configuration.getValue(""));
149        }
150    }
151    
152    /**
153     * Configure the filter's description
154     * @param configuration The description configuration
155     * @return The filter's description
156     * @throws ConfigurationException If an error occurs
157     */
158    protected I18nizableText _configureDescription (Configuration configuration) throws ConfigurationException
159    {
160        if (configuration.getAttributeAsBoolean("i18n", false))
161        {
162            return new I18nizableText("plugin." + _pluginName, configuration.getValue());
163        }
164        else 
165        {
166            return new I18nizableText(configuration.getValue(""));
167        }
168    }
169    
170    /**
171     * Configure the content type ids
172     * @param configuration The content types configuration
173     * @return The set of content type ids
174     * @throws ConfigurationException If an error occurs
175     */
176    protected List<String> _configureContentTypes(Configuration configuration) throws ConfigurationException
177    {
178        List<String> cTypes = new ArrayList<>();
179        for (Configuration cType : configuration.getChildren("type"))
180        {
181            cTypes.add(cType.getAttribute("id"));
182        }
183        return cTypes;
184    }
185    
186    /**
187     * Configure the search contexts.
188     * @param configuration the filter configuration.
189     * @throws ConfigurationException if an error occurs.
190     */
191    protected void _configureSearchContexts(Configuration configuration) throws ConfigurationException
192    {
193        FilterSearchContext params = addSearchContext();
194        
195        Configuration tagsConf = configuration.getChild("tags", true);
196        for (Configuration tag : tagsConf.getChildren("tag"))
197        {
198            params.addTag(tag.getAttribute("key"));
199        }
200        Condition condition = Condition.valueOf(tagsConf.getAttribute("condition", "AND").toUpperCase());
201        boolean strict = tagsConf.getAttributeAsBoolean("strict", true);
202        Context context = _configureContext(configuration.getChild("context", true));
203        ContextLanguage contextLang = _configureContextLanguage(configuration.getChild("context", true));
204        int depth = _configureDepth(configuration.getChild("context", true));
205        
206        params.setContext(context);
207        params.setContextLanguage(contextLang);
208        params.setDepth(depth);
209        params.setTagsCondition(condition);
210        params.setTagsAutoPosting(!strict);
211    }
212    
213    /**
214     * Configure simple metadata clauses (fixed string values).
215     * @param configuration The metadata configuration
216     * @return The metadata to filter by, as a Map of metadata name -&gt; value.
217     * @throws ConfigurationException If an error occurs
218     */
219    protected Map<String, String> _configureMetadata(Configuration configuration) throws ConfigurationException
220    {
221        Map<String, String> metadata = new HashMap<>();
222        for (Configuration elt : configuration.getChildren("metadata"))
223        {
224            String op = elt.getAttribute("operator", null);
225            String type = elt.getAttribute("type", null);
226            
227            // Process only conditions without type and operator.
228            if (op == null && type == null)
229            {
230                metadata.put(elt.getAttribute("id"), elt.getValue(null));
231            }
232        }
233        return metadata;
234    }
235    
236    /**
237     * Configure complex metadata conditions (may filter on non-string metadata,
238     * and not limited to equality.)
239     * @param configuration The metadata conditions configuration.
240     * @param metadataOperator the metadata operator.
241     * @return An expression for complex metadata conditions.
242     * @throws ConfigurationException If an error occurs
243     */
244    protected Expression _configureComplexMetadata(Configuration configuration, Condition metadataOperator) throws ConfigurationException
245    {
246        List<Expression> expressions = new ArrayList<>();
247        
248        for (Configuration elt : configuration.getChildren("metadata"))
249        {
250            String op = elt.getAttribute("operator", null);
251            String type = elt.getAttribute("type", null);
252            
253            // Process only metadata with a type and operator.
254            if (op != null && type != null)
255            {
256                String id = elt.getAttribute("id");
257                String value = elt.getValue("");
258                
259                // Date expression.
260                if (type.equals("date"))
261                {
262                    expressions.add(_getComplexDateExpression(id, op, value, elt));
263                }
264            }
265            else if ((op == null && type != null) || (op != null && type == null))
266            {
267                throw new ConfigurationException("Both type and operator must be specified for complex metadata conditions.", elt);
268            }
269        }
270        
271        Expression[] exprArr = expressions.toArray(new Expression[expressions.size()]);
272        
273        Expression expr = null;
274        if (metadataOperator == Condition.AND)
275        {
276            expr = new AndExpression(exprArr);
277        }
278        else if (metadataOperator == Condition.OR)
279        {
280            expr = new OrExpression(exprArr); 
281        } 
282        
283        return expr;
284    }
285    
286    /**
287     * Get a complex date metadata expression.
288     * @param metadataName the metadata name.
289     * @param operator the comparison operator.
290     * @param value the compared value.
291     * @param configuration the configuration being processed.
292     * @return the date Expression.
293     * @throws ConfigurationException if the configuration is not valid.
294     */
295    protected Expression _getComplexDateExpression(String metadataName, String operator, String value, Configuration configuration) throws ConfigurationException
296    {
297        int operatorOffset = 0;
298        
299        Operator op = Operator.EQ;
300        if ("eq".equalsIgnoreCase(operator))
301        {
302            op = Operator.EQ;
303        }
304        else if ("gte".equalsIgnoreCase(operator))
305        {
306            op = Operator.GE;
307        }
308        else if ("gt".equalsIgnoreCase(operator))
309        {
310            op = Operator.GE;
311            operatorOffset = 1;
312        }
313        else if ("lte".equalsIgnoreCase(operator))
314        {
315            op = Operator.LT;
316            operatorOffset = 1;
317        }
318        else if ("lt".equalsIgnoreCase(operator))
319        {
320            op = Operator.LT;
321        }
322        else
323        {
324            throw new ConfigurationException("Invalid date operator: '" + operator + "'", configuration);
325        }
326        
327        // Date value can be one of "now"/"today", "2013-09-07",
328        // or "-7" to specify a date relative to the current date.
329        Date date = null;
330        Integer valueOffset = null;
331        if ("now".equalsIgnoreCase(value) || "today".equalsIgnoreCase(value))
332        {
333            // "now" or "today".
334            valueOffset = 0;
335        }
336        else
337        {
338            try
339            {
340                // Try to parse as a fixed date.
341                date = Date.from(LocalDate.parse(value).atStartOfDay(ZoneId.systemDefault()).plusDays(operatorOffset).toInstant());
342            }
343            catch (DateTimeParseException | IllegalArgumentException e)
344            {
345                try
346                {
347                    // If not a fixed date, try to parse as an offset (signed integer).
348                    valueOffset = Integer.parseInt(value);
349                }
350                catch (NumberFormatException nfe)
351                {
352                    // Ignore: leave date value null to throw an exception.
353                }
354            }
355        }
356        
357        if (date != null)
358        {
359            // Fixed date: standard date expression.
360            return new DateExpression(metadataName, op, date);
361        }
362        else if (valueOffset != null)
363        {
364            // Offset: dynamic date expression.
365            int offset = operatorOffset + valueOffset;
366            return new DynamicDateExpression(metadataName, op, offset);
367        }
368        else
369        {
370            throw new ConfigurationException("Invalid date value: '" + value + "'", configuration);
371        }
372    }
373    
374    /**
375     * Configure the sort criteria
376     * @param configuration The sort criteria configuration
377     * @return The sort criteria
378     * @throws ConfigurationException If an error occurs
379     */
380    protected SortCriteria _configureSortCriteria(Configuration configuration) throws ConfigurationException
381    {
382        SortCriteria sortCriteria = null;
383        if (configuration != null)
384        {
385            sortCriteria = new SortCriteria();
386            for (Configuration sort : configuration.getChildren("sort"))
387            {
388                sortCriteria.addCriterion(sort.getAttribute("metadataId"), sort.getAttributeAsBoolean("ascending", false), sort.getAttributeAsBoolean("lower-case", false));
389            }
390        }
391        return sortCriteria;
392    }
393    
394    /**
395     * Configure the context search
396     * @param configuration The context configuration
397     * @return The search context
398     * @throws ConfigurationException If an error occurs
399     */
400    protected Context _configureContext (Configuration configuration) throws ConfigurationException
401    {
402        String context = configuration.getAttribute("type", "current-site");
403        if (context.equals(Context.OTHER_SITES.toString()))
404        {
405            return Context.OTHER_SITES;
406        }
407        else if (context.equals(Context.SITES.toString()))
408        {
409            return Context.SITES;
410        }
411        else if (context.equals(Context.CHILD_PAGES.toString()))
412        {
413            return Context.CHILD_PAGES;
414        }
415        else if (context.equals(Context.SITES_LIST.toString()))
416        {
417            return Context.SITES_LIST;
418        }
419           
420        return Context.CURRENT_SITE;
421    }
422    
423    /**
424     * Configure the context language
425     * @param configuration The context configuration
426     * @return The context language
427     * @throws ConfigurationException If an error occurs
428     */
429    protected ContextLanguage _configureContextLanguage (Configuration configuration) throws ConfigurationException
430    {
431        String context = configuration.getAttribute("lang", "current");
432        if (context.equals(ContextLanguage.OTHERS.toString()))
433        {
434            return ContextLanguage.OTHERS;
435        }
436        else if (context.equals(ContextLanguage.ALL.toString()))
437        {
438            return ContextLanguage.ALL;
439        }
440        
441        return ContextLanguage.CURRENT;
442    }
443    
444    /**
445     * Configure the depth search
446     * @param configuration The depth configuration
447     * @return The depth
448     * @throws ConfigurationException If an error occurs
449     */
450    protected int _configureDepth (Configuration configuration) throws ConfigurationException
451    {
452        return configuration.getAttributeAsInteger("depth", 0);
453    }
454}