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.cms.filter;
017
018import java.time.LocalDate;
019import java.time.ZoneId;
020import java.time.ZonedDateTime;
021import java.time.format.DateTimeParseException;
022import java.util.ArrayList;
023import java.util.Date;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027
028import org.apache.avalon.framework.configuration.Configurable;
029import org.apache.avalon.framework.configuration.Configuration;
030import org.apache.avalon.framework.configuration.ConfigurationException;
031import org.apache.avalon.framework.service.ServiceException;
032import org.apache.avalon.framework.service.ServiceManager;
033import org.apache.avalon.framework.service.Serviceable;
034
035import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
036import org.ametys.core.util.DateUtils;
037import org.ametys.plugins.repository.AmetysObjectResolver;
038import org.ametys.plugins.repository.query.SortCriteria;
039import org.ametys.plugins.repository.query.SortCriteria.SortCriterion;
040import org.ametys.plugins.repository.query.expression.AndExpression;
041import org.ametys.plugins.repository.query.expression.DateExpression;
042import org.ametys.plugins.repository.query.expression.Expression;
043import org.ametys.plugins.repository.query.expression.Expression.Operator;
044import org.ametys.plugins.repository.query.expression.MetadataExpression;
045import org.ametys.plugins.repository.query.expression.OrExpression;
046import org.ametys.runtime.plugin.component.PluginAware;
047
048/**
049 * Class representing a content filter. <br>
050 */
051public class StaticContentFilter extends DefaultContentFilter implements Configurable, PluginAware, Serviceable
052{
053    /** The plugin name */
054    protected String _pluginName;
055    /** The feature name */
056    protected String _featureName;
057    
058    /**
059     * Constructor
060     */
061    public StaticContentFilter ()
062    {
063        // Empty construction needed for component
064        super();
065    }
066    
067    /**
068     * Constructor
069     * @param id The filter id
070     * @param resolver The ametys object resolver
071     * @param contentTypeExtensionPoint The extension point for content types
072     */
073    public StaticContentFilter(String id, AmetysObjectResolver resolver, ContentTypeExtensionPoint contentTypeExtensionPoint)
074    {
075        super(id, resolver, contentTypeExtensionPoint);
076    }
077    
078    /**
079     * Creates a new filter from copy of another
080     * @param id The filter unique identifier
081     * @param originalFilter The original filter to be copied
082     * @param resolver The ametys object resolver
083     * @param contentTypeExtensionPoint The extension point for content types
084     */
085    public StaticContentFilter(String id, StaticContentFilter originalFilter, AmetysObjectResolver resolver, ContentTypeExtensionPoint contentTypeExtensionPoint)
086    {
087        super(id, originalFilter, resolver, contentTypeExtensionPoint);
088        
089        _contentTypes = new ArrayList<>(originalFilter._contentTypes);
090        _metadataCondition = originalFilter._metadataCondition;
091        _contextLang = originalFilter._contextLang;
092        _length = originalFilter._length;
093        _viewName = originalFilter._viewName;
094        _metadata = new HashMap<>(originalFilter._metadata);
095        
096        SortCriteria originalSC = originalFilter._sortCriteria;
097        if (originalSC != null)
098        {
099            SortCriteria sortCriteria = new SortCriteria();
100            for (SortCriterion criterion : originalFilter.getSortCriteria().getCriteria())
101            {
102                if (criterion.getMetadataPath() != null)
103                {
104                    sortCriteria.addCriterion(criterion.getMetadataPath(), criterion.isAscending(), criterion.isNormalizedSort());
105                }
106                else if (criterion.getJcrProperty() != null)
107                {
108                    sortCriteria.addCriterion(criterion.getJcrProperty(), criterion.isAscending(), criterion.isNormalizedSort());
109                }
110            }
111            _sortCriteria = sortCriteria;
112        }
113        
114        // FIXME The filter expressions should be copied here but it missing a Expression.clone() or Expression.copy() method to do it.
115        // For now the filter expression is not used when this constructor is used, so we do nothing for now since filters will be refactored soon (for 4.3 ?)
116    }
117    
118    @Override
119    public void service(ServiceManager smanager) throws ServiceException
120    {
121        _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
122        _contentTypeEP = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
123    }
124    
125    @Override
126    public void setPluginInfo(String pluginName, String featureName, String id)
127    {
128        _pluginName = pluginName;
129        _featureName = featureName;
130    }
131    
132    @Override
133    public void configure(Configuration configuration) throws ConfigurationException
134    {
135        _id = configuration.getAttribute("id");
136        _contentTypes = _configureContentTypes(configuration.getChild("content-types", true));
137        _viewName = configuration.getChild("view").getValue("main");
138        _contextLang = _configureContextLanguage(configuration.getChild("context", true));
139        _length = configuration.getChild("max-result", true).getValueAsInteger(Integer.MAX_VALUE);
140        _metadata = _configureMetadata(configuration.getChild("metadata", true));
141        _metadataCondition = Condition.valueOf(configuration.getChild("metadata", true).getAttribute("condition", "AND").toUpperCase());
142        _additionalFilterExpression = _configureComplexMetadata(configuration.getChild("metadata"), _metadataCondition);
143        _sortCriteria = _configureSortCriteria(configuration.getChild("sort-information"));
144        
145    }
146    
147    /**
148     * Configure the content type ids
149     * @param configuration The content types configuration
150     * @return The set of content type ids
151     * @throws ConfigurationException If an error occurs
152     */
153    protected List<String> _configureContentTypes(Configuration configuration) throws ConfigurationException
154    {
155        List<String> cTypes = new ArrayList<>();
156        for (Configuration cType : configuration.getChildren("type"))
157        {
158            cTypes.add(cType.getAttribute("id"));
159        }
160        return cTypes;
161    }
162    
163    /**
164     * Configure simple metadata clauses (fixed string values).
165     * @param configuration The metadata configuration
166     * @return The metadata to filter by, as a Map of metadata name -&gt; value.
167     * @throws ConfigurationException If an error occurs
168     */
169    protected Map<String, String> _configureMetadata(Configuration configuration) throws ConfigurationException
170    {
171        Map<String, String> metadata = new HashMap<>();
172        for (Configuration elt : configuration.getChildren("metadata"))
173        {
174            String op = elt.getAttribute("operator", null);
175            String type = elt.getAttribute("type", null);
176            
177            // Process only conditions without type and operator.
178            if (op == null && type == null)
179            {
180                metadata.put(elt.getAttribute("id"), elt.getValue(""));
181            }
182        }
183        return metadata;
184    }
185    
186    /**
187     * Configure complex metadata conditions (may filter on non-string metadata,
188     * and not limited to equality.)
189     * @param configuration The metadata conditions configuration.
190     * @param metadataOperator the metadata operator.
191     * @return An expression for complex metadata conditions.
192     * @throws ConfigurationException If an error occurs
193     */
194    protected Expression _configureComplexMetadata(Configuration configuration, Condition metadataOperator) throws ConfigurationException
195    {
196        List<Expression> expressions = new ArrayList<>();
197        
198        for (Configuration elt : configuration.getChildren("metadata"))
199        {
200            String op = elt.getAttribute("operator", null);
201            String type = elt.getAttribute("type", null);
202            
203            // Process only metadata with a type and operator.
204            if (op != null && type != null)
205            {
206                String id = elt.getAttribute("id");
207                String value = elt.getValue("");
208                
209                // Date expression.
210                if (type.equals("date"))
211                {
212                    expressions.add(_getComplexDateExpression(id, op, value, elt));
213                }
214            }
215            else if ((op == null && type != null) || (op != null && type == null))
216            {
217                throw new ConfigurationException("Both type and operator must be specified for complex metadata conditions.", elt);
218            }
219        }
220        
221        Expression[] exprArr = expressions.toArray(new Expression[expressions.size()]);
222        
223        Expression expr = null;
224        if (metadataOperator == Condition.AND)
225        {
226            expr = new AndExpression(exprArr);
227        }
228        else if (metadataOperator == Condition.OR)
229        {
230            expr = new OrExpression(exprArr); 
231        }
232        
233        return expr;
234    }
235    
236    /**
237     * Get a complex date metadata expression.
238     * @param metadataName the metadata name.
239     * @param operator the comparison operator.
240     * @param value the compared value.
241     * @param configuration the configuration being processed.
242     * @return the date Expression.
243     * @throws ConfigurationException if the configuration is not valid.
244     */
245    protected Expression _getComplexDateExpression(String metadataName, String operator, String value, Configuration configuration) throws ConfigurationException
246    {
247        int operatorOffset = 0;
248        
249        Operator op = Operator.EQ;
250        if ("eq".equalsIgnoreCase(operator))
251        {
252            op = Operator.EQ;
253        }
254        else if ("gte".equalsIgnoreCase(operator))
255        {
256            op = Operator.GE;
257        }
258        else if ("gt".equalsIgnoreCase(operator))
259        {
260            op = Operator.GE;
261            operatorOffset = 1;
262        }
263        else if ("lte".equalsIgnoreCase(operator))
264        {
265            op = Operator.LT;
266            operatorOffset = 1;
267        }
268        else if ("lt".equalsIgnoreCase(operator))
269        {
270            op = Operator.LT;
271        }
272        else
273        {
274            throw new ConfigurationException("Invalid date operator: '" + operator + "'", configuration);
275        }
276        
277        // Date value can be one of "now"/"today", "2013-09-07",
278        // or "-7" to specify a date relative to the current date.
279        Date date = null;
280        Integer valueOffset = null;
281        if ("now".equalsIgnoreCase(value) || "today".equalsIgnoreCase(value))
282        {
283            // "now" or "today".
284            valueOffset = 0;
285        }
286        else
287        {
288            try
289            {
290                // Try to parse as a fixed date.
291                date = Date.from(LocalDate.parse(value).atStartOfDay(ZoneId.systemDefault()).plusDays(operatorOffset).toInstant());
292            }
293            catch (DateTimeParseException | IllegalArgumentException e)
294            {
295                try
296                {
297                    // If not a fixed date, try to parse as an offset (signed integer).
298                    valueOffset = Integer.parseInt(value);
299                }
300                catch (NumberFormatException nfe)
301                {
302                    // Ignore: leave date value null to throw an exception.
303                }
304            }
305        }
306        
307        if (date != null)
308        {
309            // Fixed date: standard date expression.
310            return new DateExpression(metadataName, op, date);
311        }
312        else if (valueOffset != null)
313        {
314            // Offset: dynamic date expression.
315            int offset = operatorOffset + valueOffset;
316            return new DynamicDateExpression(metadataName, op, offset);
317        }
318        else
319        {
320            throw new ConfigurationException("Invalid date value: '" + value + "'", configuration);
321        }
322    }
323    
324    /**
325     * Configure the sort criteria
326     * @param configuration The sort criteria configuration
327     * @return The sort criteria
328     * @throws ConfigurationException If an error occurs
329     */
330    protected SortCriteria _configureSortCriteria(Configuration configuration) throws ConfigurationException
331    {
332        SortCriteria sortCriteria = null;
333        if (configuration != null)
334        {
335            sortCriteria = new SortCriteria();
336            for (Configuration sort : configuration.getChildren("sort"))
337            {
338                sortCriteria.addCriterion(sort.getAttribute("metadataId"), sort.getAttributeAsBoolean("ascending", false), sort.getAttributeAsBoolean("lower-case", false));
339            }
340        }
341        return sortCriteria;
342    }
343    
344    /**
345     * Configure the context language
346     * @param configuration The context configuration
347     * @return The context language
348     * @throws ConfigurationException If an error occurs
349     */
350    protected ContextLanguage _configureContextLanguage (Configuration configuration) throws ConfigurationException
351    {
352        String context = configuration.getAttribute("lang", "current");
353        if (context.equals(ContextLanguage.OTHERS.toString()))
354        {
355            return ContextLanguage.OTHERS;
356        }
357        else if (context.equals(ContextLanguage.ALL.toString()))
358        {
359            return ContextLanguage.ALL;
360        }
361        
362        return ContextLanguage.CURRENT;
363    }
364    
365    /**
366     * Constructs an {@link Expression} representing a comparison between a metadata and the time the build() method is called.
367     */
368    public static class DynamicDateExpression implements Expression
369    {
370        
371        private MetadataExpression _metadataExpr;
372        private Operator _operator;
373        /**
374         * The offset value.
375         */
376        private int _offsetValue;
377        
378        /**
379         * Create a comparison Expression relative to the current date, with an offset, normalizing the 
380         * @param metadata the metadata name
381         * @param operator the operator to make the comparison
382         * @param offsetValue the offset value.
383         */
384        public DynamicDateExpression(String metadata, Operator operator, int offsetValue)
385        {
386            _operator = operator;
387            _metadataExpr = new MetadataExpression(metadata);
388            _offsetValue = offsetValue;
389        }
390        
391        @Override
392        public String build()
393        {
394            ZonedDateTime dateTime = LocalDate.now().atStartOfDay(ZoneId.systemDefault()).plusDays(_offsetValue);
395            
396            StringBuffer buff = new StringBuffer();
397            
398            buff.append(_metadataExpr.build()).append(' ').append(_operator).append(" xs:dateTime('");
399            DateUtils.getISODateTimeFormatter().formatTo(dateTime, buff);
400            buff.append("')");
401            
402            return buff.toString();
403        }
404        
405    }
406    
407}