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