001/*
002 *  Copyright 2011 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.sitemap;
017
018import java.io.File;
019import java.io.FileInputStream;
020import java.io.InputStream;
021import java.lang.reflect.Array;
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027
028import javax.jcr.Node;
029import javax.jcr.Property;
030import javax.jcr.RepositoryException;
031import javax.jcr.Value;
032
033import org.apache.avalon.framework.component.Component;
034import org.apache.avalon.framework.configuration.Configuration;
035import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
036import org.apache.avalon.framework.context.Context;
037import org.apache.avalon.framework.context.ContextException;
038import org.apache.avalon.framework.context.Contextualizable;
039import org.apache.avalon.framework.service.ServiceException;
040import org.apache.avalon.framework.service.ServiceManager;
041import org.apache.avalon.framework.service.Serviceable;
042import org.apache.cocoon.Constants;
043import org.apache.cocoon.components.ContextHelper;
044import org.apache.cocoon.environment.Request;
045import org.slf4j.Logger;
046import org.slf4j.LoggerFactory;
047
048import org.ametys.core.right.RightManager;
049import org.ametys.plugins.repository.AmetysObjectResolver;
050import org.ametys.plugins.repository.UnknownAmetysObjectException;
051import org.ametys.plugins.repository.data.holder.ModelLessDataHolder;
052import org.ametys.plugins.repository.data.holder.group.Composite;
053import org.ametys.plugins.repository.data.holder.group.Repeater;
054import org.ametys.plugins.repository.jcr.JCRAmetysObject;
055import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector;
056import org.ametys.runtime.model.type.ElementType;
057import org.ametys.web.repository.page.MetadataAwarePagesContainer;
058import org.ametys.web.repository.page.Page;
059import org.ametys.web.repository.page.Page.PageType;
060import org.ametys.web.repository.page.PagesContainer;
061import org.ametys.web.repository.site.Site;
062import org.ametys.web.repository.sitemap.Sitemap;
063import org.ametys.web.sitemap.SitemapIcon.Condition;
064
065/**
066 * Default implementation of {@link SitemapDecoratorsHandler}
067 *
068 */
069public class DefaultSitemapDecoratorsHandler implements SitemapDecoratorsHandler, Serviceable, Contextualizable, Component
070{
071    private static Logger _logger = LoggerFactory.getLogger(DefaultSitemapDecoratorsHandler.class);
072    
073    /** The last time the file was loaded */
074    protected Map<String, Long> _lastConfUpdate = new HashMap<>();
075    
076    private Map<String, List<SitemapIcon>> _icons = new HashMap<>();
077    private Map<String, List<SitemapDecorator>> _decorators = new HashMap<>();
078    
079    private org.apache.cocoon.environment.Context _cocoonContext;
080    private Context _context;
081
082    private AmetysObjectResolver _resolver;
083
084    private RightManager _rightManager; 
085    
086    @Override
087    public void service(ServiceManager manager) throws ServiceException
088    {
089        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
090        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
091    }
092    
093    @Override
094    public List<SitemapIcon> getIcons(Site site)
095    {
096        String skinId = site.getSkinId();
097        refreshValues(skinId);
098        return _icons.get(skinId);
099    }
100    
101    @Override
102    public SitemapIcon getIcon(Page page)
103    {
104        String skinId = page.getSite().getSkinId();
105        refreshValues(skinId);
106        
107        List<SitemapIcon> icons = _icons.get(skinId);
108        for (SitemapIcon icon : icons)
109        {
110            if (_matches(icon, page))
111            {
112                return icon;
113            }
114        }
115        
116        return null;
117    }
118    
119    @Override
120    public List<SitemapDecorator> getDecorators (Site site)
121    {
122        String skinId = site.getSkinId();
123        refreshValues(skinId);
124        return _decorators.get(skinId);
125    }
126    
127    @Override
128    public List<SitemapDecorator> getDecorators(Sitemap sitemap)
129    {
130        String skinId = sitemap.getSite().getSkinId();
131        refreshValues(skinId);
132        
133        List<SitemapDecorator> sitemapDecorators = new ArrayList<>();
134        for (SitemapDecorator decorator : _decorators.get(skinId))
135        {
136            if (_matches(decorator, sitemap))
137            {
138                sitemapDecorators.add(decorator);
139            }
140        }
141        return sitemapDecorators;
142    }
143    
144    @Override
145    public List<SitemapDecorator> getDecorators(Page page)
146    {
147        String skinId = page.getSite().getSkinId();
148        refreshValues(skinId);
149        
150        List<SitemapDecorator> pageDecorators = new ArrayList<>();
151        for (SitemapDecorator decorator : _decorators.get(skinId))
152        {
153            if (_matches(decorator, page))
154            {
155                pageDecorators.add(decorator);
156            }
157        }
158        return pageDecorators;
159    }
160    
161    /**
162     * Determines if page matches the decorator
163     * @param icon the icon
164     * @param sitemap the sitemap
165     * @return true if the page matches the decorator
166     */
167    protected boolean _matches (SitemapIcon icon, Sitemap sitemap)
168    {
169        if (!_matchesMetadata(icon, sitemap))
170        {
171            return false;
172        }
173        
174        if (!_matchesProperties (icon, sitemap))
175        {
176            return false;
177        }
178        
179        if (!icon.getTags().isEmpty())
180        {
181            return false;
182        }
183        
184        if (icon.isLive())
185        {
186            return false;
187        }
188        
189        if (icon.getPageType() != null)
190        {
191            return false;
192        }
193        
194        if (icon.isRestricted() && _rightManager.hasAnonymousReadAccess(sitemap))
195        {
196            return false;
197        }
198        
199        return true;
200    }
201    
202    /**
203     * Determines if page matches the decorator
204     * @param icon the icon
205     * @param page the page to test
206     * @return true if the page matches the decorator
207     */
208    protected boolean _matches (SitemapIcon icon, Page page)
209    {
210        PageType pageType = icon.getPageType();
211        if (pageType != null && !page.getType().equals(pageType))
212        {
213            return false;
214        }
215        
216        if (!_matchesTags (icon, page))
217        {
218            return false;
219        }
220        
221        if (!_matchesMetadata(icon, page))
222        {
223            return false;
224        }
225        
226        if (!_matchesProperties (icon, page))
227        {
228            return false;
229        }
230        
231        if (icon.isLive())
232        {
233            Request request = ContextHelper.getRequest(_context);
234            String currentWp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
235            
236            try
237            {
238                RequestAttributeWorkspaceSelector.setForcedWorkspace(request, "live");
239                _resolver.resolveById(page.getId());
240            }
241            catch (UnknownAmetysObjectException e)
242            {
243                return false;
244            }
245            finally
246            {
247                RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWp);
248            }
249        }
250        
251        if (icon.isRestricted() && _rightManager.hasAnonymousReadAccess(page))
252        {
253            return false;
254        }
255        
256        return true;
257    }
258    
259    private boolean _matchesTags (SitemapIcon icon, Page page)
260    {
261        List<String> tags = icon.getTags();
262        
263        if (!tags.isEmpty())
264        {
265            Condition condition = icon.getTagsCondition();
266            if (condition == Condition.AND)
267            {
268                return page.getTags().containsAll(tags);
269            }
270            else if (condition == Condition.OR)
271            {
272                for (String tagName : tags)
273                {
274                    if (page.getTags().contains(tagName))
275                    {
276                        return true;
277                    }
278                }
279                return false;
280            }
281        }
282        return true;
283    }
284    
285    private boolean _matchesMetadata (SitemapIcon icon, MetadataAwarePagesContainer page)
286    {
287        Map<String, String> metadata = icon.getMetadata();
288        if (!metadata.isEmpty())
289        {
290            Condition condition = icon.getMetadataCondition();
291            if (condition == Condition.AND)
292            {
293                return _matchesAndData (metadata, page);
294            }
295            else if (condition == Condition.OR)
296            {
297                return _matchesOrData (metadata, page);
298            }
299        }
300        return true;
301    }
302    
303    private boolean _matchesProperties (SitemapIcon icon, PagesContainer page)
304    {
305        Map<String, String> properties = icon.getProperties();
306        if (!properties.isEmpty())
307        {
308            Condition condition = icon.getMetadataCondition();
309            if (condition == Condition.AND)
310            {
311                return _matchesAndProperties(properties, page);
312            }
313            else if (condition == Condition.OR)
314            {
315                return _matchesOrProperties(properties, page);
316            }
317        }
318        return true;
319    }
320    
321    private boolean _matchesAndProperties (Map<String, String> properties, PagesContainer page)
322    {
323        if (!(page instanceof JCRAmetysObject))
324        {
325            return properties.size() == 0;
326        }
327        
328        JCRAmetysObject jcrPage = (JCRAmetysObject) page;
329        Node node = jcrPage.getNode();
330        
331        try
332        {
333            for (String propertyName : properties.keySet())
334            {
335                String valueToTest = properties.get(propertyName);
336                
337                if (!node.hasProperty(propertyName))
338                {
339                    // property does not exits
340                    return false;
341                }
342                
343                Property property = node.getProperty(propertyName);
344                if (property.getDefinition().isMultiple())
345                {
346                    Value[] values = property.getValues();
347                    if (valueToTest.length() > 0 && values.length == 0)
348                    {
349                        // metadata exits but is empty
350                        return false;
351                    }
352                    else if (valueToTest.length() > 0)
353                    {
354                        String[] results = new String[values.length];
355                        for (int i = 0; i < values.length; i++)
356                        {
357                            Value value = values[i];
358                            results[i] = value.getString();
359                        }
360                        
361                        List<String> asList = Arrays.asList(results);
362                        String[] valuesToTest = valueToTest.split(",");
363                        for (String val : valuesToTest)
364                        {
365                            if (!asList.contains(val))
366                            {
367                                // values do not contain all test values
368                                return false;
369                            }
370                        }
371                    }
372                }
373                else
374                {
375                    String value = property.getValue().getString();
376                    if (valueToTest.length() > 0 && !valueToTest.equals(value))
377                    {
378                        // value is not equals to test value
379                        return false;
380                    }
381                }
382            }
383        }
384        catch (RepositoryException e)
385        {
386            _logger.error("An error occurred while testing properties", e);
387            return false;
388        }
389        return true;
390    }
391    
392    private boolean _matchesOrProperties (Map<String, String> properties, PagesContainer page)
393    {
394        if (!(page instanceof JCRAmetysObject))
395        {
396            return properties.size() == 0;
397        }
398        
399        JCRAmetysObject jcrPage = (JCRAmetysObject) page;
400        Node node = jcrPage.getNode();
401        
402        try
403        {
404            for (String propertyName : properties.keySet())
405            {
406                String valueToTest = properties.get(propertyName);
407                
408                if (node.hasProperty(propertyName))
409                {
410                    Property property = node.getProperty(propertyName);
411                    if (property.getDefinition().isMultiple())
412                    {
413                        Value[] values = property.getValues();
414                        if (valueToTest.length() == 0 && values.length > 0)
415                        {
416                            // multiple metadata exists and is not empty
417                            return true; 
418                        }
419                        else if (valueToTest.length() != 0)
420                        {
421                            String[] results = new String[values.length];
422                            for (int i = 0; i < values.length; i++)
423                            {
424                                Value value = values[i];
425                                results[i] = value.getString();
426                            }
427                            
428                            List<String> asList = Arrays.asList(results);
429                            String[] valuesToTest = valueToTest.split(",");
430                            boolean findAll = true;
431                            for (String val : valuesToTest)
432                            {
433                                if (!asList.contains(val))
434                                {
435                                    findAll = false;
436                                }
437                            }
438                            if (findAll)
439                            {
440                                // values contain all test values
441                                return true;
442                            }
443                        }
444                    }
445                    else
446                    {
447                        String value = property.getString();
448                        if (valueToTest.length() == 0 || valueToTest.equals(value))
449                        {
450                            // value is equals to test value
451                            return true;
452                        }
453                    }
454                }
455            }
456        }
457        catch (RepositoryException e)
458        {
459            _logger.error("An error occurred while testing properties", e);
460            return false;
461        }
462        
463        return false;
464    }
465    
466    @SuppressWarnings("unchecked")
467    boolean _matchesAndData (Map<String, String> data, ModelLessDataHolder dataHolder)
468    {
469        for (String dataPath : data.keySet())
470        {
471            if (!dataHolder.hasValue(dataPath))
472            {
473                // data does not exits
474                return false;
475            }
476            
477            Object value = dataHolder.getValue(dataPath);
478            
479            if (value instanceof Composite)
480            {
481                if (((Composite) value).getDataNames().isEmpty())
482                {
483                    // the composite data is empty
484                    return false;
485                }
486            }
487            else if (value instanceof Repeater)
488            {
489                if (((Repeater) value).getSize() <= 0)
490                {
491                    // the repeater is empty
492                    return false;
493                }
494            }
495            else if (dataHolder.isMultiple(dataPath))
496            {
497                String valueToTest = data.get(dataPath);
498                if (!valueToTest.isEmpty() && Array.getLength(value) == 0)
499                {
500                    // data exits but is empty
501                    return false;
502                }
503                else if (!valueToTest.isEmpty())
504                {
505                    ElementType type = (ElementType) dataHolder.getType(dataPath);
506                    if (!_findAllValuesInMultipleData(valueToTest, value, type))
507                    {
508                        // value does not contain all test values
509                        return false;
510                    }
511                }
512            }
513            else
514            {
515                String valueToTest = data.get(dataPath);
516                ElementType type = (ElementType) dataHolder.getType(dataPath);
517                if (!valueToTest.isEmpty() && !valueToTest.equals(type.toString(value)))
518                {
519                    // value is not equals to test value
520                    return false;
521                }
522            }
523        }
524        
525        // All data have matched
526        return true;
527    }
528    
529    boolean _matchesOrData (Map<String, String> data, ModelLessDataHolder dataHolder)
530    {
531        for (String dataPath : data.keySet())
532        {
533            if (_matchesOrData(dataPath, data, dataHolder))
534            {
535                return true;
536            }
537        }
538        
539        return false;
540    }
541    
542    @SuppressWarnings("unchecked")
543    private boolean _matchesOrData (String dataPath, Map<String, String> data, ModelLessDataHolder dataHolder)
544    {
545        if (!dataHolder.hasValue(dataPath))
546        {
547            // data does not exists
548            return false;
549        }
550        
551        Object value = dataHolder.getValue(dataPath);
552        
553        if (value instanceof Composite)
554        {
555            if (!((Composite) value).getDataNames().isEmpty())
556            {
557                // composite data is not empty, the condition matched
558                return true;
559            }
560        }
561        else if (value instanceof Repeater)
562        {
563            if (((Repeater) value).getSize() > 0)
564            {
565                // repeater data is not empty, the condition matched
566                return true;
567            }
568        }
569        else if (dataHolder.isMultiple(dataPath))
570        {
571            String valueToTest = data.get(dataPath);
572            if (valueToTest.isEmpty() && Array.getLength(value) > 0)
573            {
574                // multiple metadata exists and is not empty
575                return true;
576            }
577            else if (!valueToTest.isEmpty())
578            {
579                ElementType type = (ElementType) dataHolder.getType(dataPath);
580                if (_findAllValuesInMultipleData(valueToTest, value, type))
581                {
582                    // values contain all test values
583                    return true;
584                }
585            }
586        }
587        else
588        {
589            String valueToTest = data.get(dataPath);
590            ElementType type = (ElementType) dataHolder.getType(dataPath);
591            if (valueToTest.isEmpty() || valueToTest.equals(type.toString(value)))
592            {
593                // value is equals to test value
594                return true;
595            }
596        }
597        
598        return false;
599    }
600    
601    private boolean _findAllValuesInMultipleData(String valueToTest, Object valueFromDataHolder, ElementType type)
602    {
603        List<String> valuesFromDataHolderAsString = new ArrayList<>();
604        for (int i = 0; i < Array.getLength(valueFromDataHolder); i++)
605        {
606            @SuppressWarnings("unchecked")
607            String valueAsString = type.toString(Array.get(valueFromDataHolder, i));
608            valuesFromDataHolderAsString.add(valueAsString);
609        }
610        
611        String[] valuesToTest = valueToTest.split(",");
612        for (String value : valuesToTest)
613        {
614            if (!valuesFromDataHolderAsString.contains(value))
615            {
616                return false;
617            }
618        }
619        
620        return true;
621    }
622    
623    @Override
624    public void contextualize(Context context) throws ContextException
625    {
626        _cocoonContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
627        _context = context;
628    }
629    
630    /**
631     * Refresh the configuration values
632     * @param skinId the skin name
633     */
634    public void refreshValues(String skinId)
635    {
636        File configurationFile = null;
637
638        try
639        {
640            configurationFile = new File (_cocoonContext.getRealPath("/skins/" + skinId + "/conf/sitemap-icons.xml"));
641            if (!configurationFile.exists())
642            {
643                configurationFile = new File (_cocoonContext.getRealPath("/WEB-INF/param/sitemap-icons.xml"));
644            }
645            
646            if (configurationFile.exists())
647            {
648                long lastConfUpdate = _lastConfUpdate.containsKey(skinId) ? _lastConfUpdate.get(skinId) : 0;
649                if (lastConfUpdate == 0 || lastConfUpdate < configurationFile.lastModified())
650                {
651                    List<SitemapDecorator> decorators = new ArrayList<>();
652                    List<SitemapIcon> icons = new ArrayList<>();
653                    
654                    try  (InputStream is = new FileInputStream(configurationFile))
655                    {
656                        Configuration configuration = new DefaultConfigurationBuilder().build(is);
657                        
658                        Configuration[] iconsConf = configuration.getChild("icons").getChildren("icon");
659                        for (Configuration iconConf : iconsConf)
660                        {
661                            SitemapIcon icon = new SitemapIcon();
662                            icon.configure(iconConf);
663                            icons.add(icon);
664                        }
665                        
666                        Configuration[] decoratorsConf = configuration.getChild("decorators").getChildren("decorator");
667                        for (Configuration decoratorConf : decoratorsConf)
668                        {
669                            SitemapDecorator decorator = new SitemapDecorator();
670                            decorator.configure(decoratorConf);
671                            decorators.add(decorator);
672                        }
673                    }
674                    
675                    _lastConfUpdate.put(skinId, configurationFile.lastModified());
676                    _icons.put(skinId, icons);
677                    _decorators.put(skinId, decorators);
678                }
679            }
680            else
681            {
682                _decorators.put(skinId, new ArrayList<SitemapDecorator>());
683                _icons.put(skinId, new ArrayList<SitemapIcon>());
684            }
685        }
686        catch (Exception e)
687        {
688            if (_logger.isWarnEnabled())
689            {
690                _logger.warn("Cannot read the configuration file params/sitemap-icons.xml. Continue as if file was not existing", e);
691                _decorators.put(skinId, new ArrayList<SitemapDecorator>());
692                _icons.put(skinId, new ArrayList<SitemapIcon>());
693            }
694        }
695    }
696
697}