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