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