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