001/*
002 *  Copyright 2019 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.inputdata;
017
018import java.util.ArrayList;
019import java.util.HashMap;
020import java.util.List;
021import java.util.Map;
022import java.util.concurrent.ConcurrentHashMap;
023import java.util.stream.Collectors;
024
025import org.apache.avalon.framework.component.Component;
026import org.apache.avalon.framework.logger.AbstractLogEnabled;
027import org.apache.avalon.framework.service.ServiceException;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.apache.avalon.framework.service.Serviceable;
030import org.apache.cocoon.ProcessingException;
031import org.apache.cocoon.xml.AttributesImpl;
032import org.apache.cocoon.xml.XMLUtils;
033import org.apache.commons.lang.StringUtils;
034import org.xml.sax.ContentHandler;
035import org.xml.sax.SAXException;
036
037import org.ametys.cms.data.type.ModelItemTypeConstants;
038import org.ametys.core.right.RightManager;
039import org.ametys.core.user.CurrentUserProvider;
040import org.ametys.core.user.UserIdentity;
041import org.ametys.plugins.repository.AmetysObjectIterable;
042import org.ametys.plugins.repository.provider.WorkspaceSelector;
043import org.ametys.runtime.model.type.ElementType;
044import org.ametys.runtime.model.type.ModelItemType;
045import org.ametys.web.pageaccess.RestrictedPagePolicy;
046import org.ametys.web.renderingcontext.RenderingContext;
047import org.ametys.web.renderingcontext.RenderingContextHandler;
048import org.ametys.web.repository.page.Page;
049import org.ametys.web.repository.page.Page.PageType;
050import org.ametys.web.repository.site.Site;
051import org.ametys.web.repository.site.SiteManager;
052import org.ametys.web.repository.sitemap.Sitemap;
053
054/**
055 * Send a sitemap as SAX events.
056 * Handles both cacheable and non-cacheable cases.
057 */
058public class SitemapSaxer extends AbstractLogEnabled implements Serviceable, Component
059{
060    /** Avalon role. */
061    public static final String ROLE = SitemapSaxer.class.getName();
062    
063    /** Prefix for sitemap namespace. */
064    public static final String NAMESPACE_PREFIX = "sitemap";
065    /** URI for sitemap namespace. */
066    public static final String NAMESPACE_URI = "http://www.ametys.org/inputdata/sitemap/3.0";
067    
068    // Constants for current path management
069    private static final int PATH_DESCENDANT = -2;
070    private static final int PATH_NOT_IN_PATH = -1;
071    private static final int PATH_IN_PATH = 0;
072    private static final int PATH_CURRENT = 1;
073
074    RightManager _rightManager;
075    private RenderingContextHandler _renderingContextHandler;
076    private CurrentUserProvider _currentUserProvider;
077    private WorkspaceSelector _workspaceSelector;
078    private SiteManager _siteManager;
079    
080    //          sitename   workspace    sitemap     renderingcontext  pages
081    private Map<String, Map<String, Map<String, Map<RenderingContext, List<BufferedPage>>>>> _bufferedPages = new ConcurrentHashMap<>();
082    
083    @Override
084    public void service(ServiceManager manager) throws ServiceException
085    {
086        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
087        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
088        _renderingContextHandler = (RenderingContextHandler) manager.lookup(RenderingContextHandler.ROLE);
089        _workspaceSelector = (WorkspaceSelector) manager.lookup(WorkspaceSelector.ROLE);
090        _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE);
091    }
092    
093    /**
094     * Clear the page cache.
095     * @param siteName the site.
096     * @param workspace the JCR workspace.
097     */
098    public void clearCache(String siteName, String workspace)
099    {
100        Map<String, Map<String, Map<RenderingContext, List<BufferedPage>>>> sitePages = _bufferedPages.get(siteName);
101        if (sitePages != null)
102        {
103            Map<String, Map<RenderingContext, List<BufferedPage>>> workspacePages = sitePages.get(workspace);
104            if (workspacePages != null)
105            {
106                workspacePages.clear();
107            }
108        }
109    }
110
111    /**
112     * Send SAX events representing pages from a sitemap.
113     * @param contentHandler the SAX handler.
114     * @param siteName the site name.
115     * @param sitemapName the sitemap name.
116     * @param currentPage the current {@link Page}.
117     * @param initialDepth max depth from the root.
118     * @param descendantDepth max depth under the current page.
119     * @throws SAXException if an error occurs with the content handler.
120     * @throws ProcessingException if an error occurs while processing data.
121     */
122    public void toSAX(ContentHandler contentHandler, String siteName, String sitemapName, Page currentPage, int initialDepth, int descendantDepth) throws SAXException, ProcessingException
123    {
124        RenderingContext renderingContext = _renderingContextHandler.getRenderingContext();
125        String workspace = _workspaceSelector.getWorkspace();
126        UserIdentity user = _currentUserProvider.getUser();
127        
128        Site site = _siteManager.getSite(siteName);
129        Sitemap sitemap = site.getSitemap(sitemapName);
130        RestrictedPagePolicy policy = site.getRestrictedPagePolicy();
131        
132        List<? extends PageWrapper> pages = null;
133        
134        if (policy == RestrictedPagePolicy.HIDDEN)
135        {
136            // input data not cacheable, so no need to cache anything
137            pages = sitemap.getChildrenPages().stream().map(RepositoryPage::new).collect(Collectors.toList());
138        }
139        else
140        {
141            // input data is cacheable, retrive data from cache, or fill the cache
142            Map<String, Map<String, Map<RenderingContext, List<BufferedPage>>>> sitePages = _bufferedPages.computeIfAbsent(siteName, s -> new ConcurrentHashMap<>());
143            Map<String, Map<RenderingContext, List<BufferedPage>>> workspacePages = sitePages.computeIfAbsent(workspace, w -> new ConcurrentHashMap<>());
144            Map<RenderingContext, List<BufferedPage>> sitemapPages = workspacePages.computeIfAbsent(sitemapName, s -> new ConcurrentHashMap<>());
145                
146            pages = sitemapPages.computeIfAbsent(renderingContext, r -> _fill(sitemap.getChildrenPages()));
147        }
148        
149        contentHandler.startPrefixMapping(NAMESPACE_PREFIX, NAMESPACE_URI);
150
151        AttributesImpl attrs = new AttributesImpl();
152        attrs.addCDATAAttribute(NAMESPACE_URI, "site", NAMESPACE_PREFIX + ":site", siteName);
153        attrs.addCDATAAttribute(NAMESPACE_URI, "lang", NAMESPACE_PREFIX + ":lang", sitemapName);
154        attrs.addCDATAAttribute(NAMESPACE_URI, "id", NAMESPACE_PREFIX + ":id", sitemap.getId());
155
156        XMLUtils.startElement(contentHandler, "sitemap", attrs);
157
158        // Process recursively pages
159        _saxPages(contentHandler, pages, currentPage, currentPage != null ? currentPage.getPathInSitemap() : null, user, renderingContext, site.getRestrictedPagePolicy(), initialDepth, descendantDepth, 1, -1);
160
161        XMLUtils.endElement(contentHandler, "sitemap");
162        contentHandler.endPrefixMapping(NAMESPACE_PREFIX);
163    }
164    
165    private List<BufferedPage> _fill(AmetysObjectIterable<? extends Page> pages)
166    {
167        List<BufferedPage> result = new ArrayList<>();
168        
169        for (Page page : pages)
170        {
171            BufferedPage bufferedPage = new BufferedPage();
172            
173            bufferedPage._path = page.getPathInSitemap();
174            
175            Map<String, String> internalAttributes = _getInternalAttributes(page);
176            bufferedPage._internalAttributes = internalAttributes;
177            if ("true".equals(internalAttributes.get("invisible")))
178            {
179                bufferedPage._visible = false;
180            }
181            
182            bufferedPage._attributes = _getAttributes(page);
183            
184            bufferedPage._children = _fill(page.getChildrenPages());
185            result.add(bufferedPage);
186        }
187 
188        return result;
189    }
190    
191    Map<String, String> _getInternalAttributes(Page page)
192    {
193        Map<String, String> internalAttributes = new HashMap<>();
194        String path = page.getPathInSitemap();
195        PageType pageType = page.getType();
196        internalAttributes.put("id", page.getId());
197        internalAttributes.put("name", page.getName());
198        internalAttributes.put("title", page.getTitle());
199        internalAttributes.put("long-title", page.getLongTitle());
200        internalAttributes.put("path", path);
201        internalAttributes.put("type", page.getType().name());
202        
203        if (!page.isVisible())
204        {
205            internalAttributes.put("invisible", "true");
206        }
207        
208        if (!_rightManager.hasAnonymousReadAccess(page))
209        {
210            internalAttributes.put("restricted", "true");
211        }
212        
213        // Add a flag if there is data
214        if (pageType == PageType.CONTAINER)
215        {
216            internalAttributes.put("container", "true");
217        }
218        // Add URL if found
219        else if (pageType == PageType.LINK)
220        {
221            internalAttributes.put("link", page.getURL());
222            internalAttributes.put("link-type", page.getURLType().name());
223        }
224        
225        return internalAttributes;
226    }
227    
228    @SuppressWarnings("unchecked")
229    Map<String, String> _getAttributes(Page page)
230    {
231        Map<String, String> attributes = new HashMap<>();
232        
233        for (String metadataName : page.getDataNames())
234        {
235            ModelItemType type = page.getType(metadataName);
236            
237            // SAX only non composite, non binary, non rich text and non multivalued metadatas
238            if (!page.isMultiple(metadataName) && type instanceof ElementType)
239            {
240                ElementType elementType = (ElementType) type;
241                if (elementType.isSimple() || ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID.equals(elementType.getId()))
242                {
243                    Object value = page.getValue(metadataName);
244                    attributes.put(metadataName, ((ElementType) type).toString(value));
245                }
246            }
247        }
248        
249        // tags
250        for (String tag : page.getTags())
251        {
252            attributes.put("PLUGIN_TAGS_" + tag, "empty");
253        }
254        
255        return attributes;
256    }
257    
258    private void _saxPages(ContentHandler handler, List<? extends PageWrapper> pages, Page currentPage, String currentPagePath, UserIdentity userIdentity, RenderingContext renderingContext, RestrictedPagePolicy policy, int initialDepth, int descendantDepth, int currentDepth, int currentDescendantDepth) throws SAXException
259    {
260        boolean inBackOffice = renderingContext == RenderingContext.BACK || renderingContext == RenderingContext.PREVIEW;
261
262        for (PageWrapper page : pages)
263        {
264            String pagePath = page.getPath();
265
266            if (inBackOffice || policy == RestrictedPagePolicy.DISPLAYED || page.hasReadAccess(userIdentity))
267            {
268                if (page.isVisible() || (StringUtils.isNotBlank(currentPagePath) && currentPagePath.startsWith(pagePath)))
269                {
270                    _saxPage(handler, page, currentPage, currentPagePath, userIdentity, renderingContext, policy, initialDepth, descendantDepth, currentDepth, currentDescendantDepth);
271                }
272            }
273        }
274    }
275    
276    private void _saxPage(ContentHandler handler, PageWrapper page, Page currentPage, String currentPagePath, UserIdentity userIdentity, RenderingContext renderingContext, RestrictedPagePolicy policy, int initialDepth, int descendantDepth, int currentDepth, int currentDescendantDepth) throws SAXException
277    {
278        String pagePath = page.getPath();
279
280        AttributesImpl attrs = new AttributesImpl();
281        Map<String, String> internalAttributes = page.getInternalAttributes();
282        
283        for (String name : internalAttributes.keySet())
284        {
285            attrs.addCDATAAttribute(NAMESPACE_URI, name, NAMESPACE_PREFIX + ':' + name, internalAttributes.get(name));
286        }
287        
288        Map<String, String> attributes = page.getAttributes();
289        
290        for (String name : attributes.keySet())
291        {
292            attrs.addCDATAAttribute(name, attributes.get(name));
293        }
294        
295        int inPath = _saxCurrentStatus(currentPagePath, pagePath, attrs);
296        
297        XMLUtils.startElement(handler, "page", attrs);
298        
299        // Continue if current depth is less than initial depth
300        // or if we are inside current path or in current page descendants
301        if (currentDepth <= initialDepth || inPath >= PATH_IN_PATH || (inPath == PATH_DESCENDANT && currentDescendantDepth <= descendantDepth))
302        {
303            int descendant = (inPath == PATH_CURRENT) ? 0 : (inPath == PATH_DESCENDANT ? currentDescendantDepth + 1 : -1);
304            _saxPages(handler, page.getChildren(), currentPage, currentPagePath, userIdentity, renderingContext, policy, initialDepth, descendantDepth, currentDepth + 1, descendant);
305        }
306        
307        XMLUtils.endElement(handler, "page");
308    }
309    
310    /**
311     * SAX current status.
312     * @param currentPagePath the path to the current page.
313     * @param pagePath the path to the page to process.
314     * @param attrs the attributes to populate.
315     * @return the current status.
316     */
317    protected int _saxCurrentStatus(String currentPagePath, String pagePath, AttributesImpl attrs)
318    {
319        int result = PATH_NOT_IN_PATH;
320
321        if (currentPagePath == null)
322        {
323            return PATH_NOT_IN_PATH;
324        }
325
326        // If the page in an ancestor of the current page
327        boolean isPageCurrent = currentPagePath.equals(pagePath);
328
329        if (currentPagePath.startsWith(pagePath + "/") || isPageCurrent)
330        {
331            attrs.addCDATAAttribute(NAMESPACE_URI, "in-path", NAMESPACE_PREFIX + ":in-path", "true");
332            result = PATH_IN_PATH;
333        }
334        else if (pagePath.startsWith(currentPagePath))
335        {
336            result = PATH_DESCENDANT;
337        }
338
339        // If this is the current page
340        if (isPageCurrent)
341        {
342            attrs.addCDATAAttribute(NAMESPACE_URI, "current", NAMESPACE_PREFIX + ":current", "true");
343            result = PATH_CURRENT;
344        }
345
346        return result;
347    }
348    
349    interface PageWrapper
350    {
351        List<? extends PageWrapper> getChildren();
352        boolean isVisible();
353        String getPath();
354        Map<String, String> getAttributes();
355        Map<String, String> getInternalAttributes();
356        boolean hasReadAccess(UserIdentity user);
357    }
358
359    class BufferedPage implements PageWrapper
360    {
361        String _path;
362        Map<String, String> _attributes;
363        Map<String, String> _internalAttributes;
364        List<BufferedPage> _children;
365        boolean _visible = true;
366        
367        public List<? extends PageWrapper> getChildren()
368        {
369            return _children;
370        }
371        
372        public boolean isVisible()
373        {
374            return _visible;
375        }
376        
377        public String getPath()
378        {
379            return _path;
380        }
381        
382        public Map<String, String> getAttributes()
383        {
384            return _attributes;
385        }
386        
387        public Map<String, String> getInternalAttributes()
388        {
389            return _internalAttributes;
390        }
391        
392        public boolean hasReadAccess(UserIdentity user)
393        {
394            throw new UnsupportedOperationException("A BufferedPage has no concept of read access");
395        }
396    }
397    
398    class RepositoryPage implements PageWrapper
399    {
400        Page _page;
401        
402        RepositoryPage(Page page)
403        {
404            _page = page;
405        }
406        
407        public List<? extends PageWrapper> getChildren()
408        {
409            return _page.getChildrenPages().stream().map(RepositoryPage::new).collect(Collectors.toList());
410        }
411        
412        public boolean isVisible()
413        {
414            return _page.isVisible();
415        }
416        
417        public String getPath()
418        {
419            return _page.getPathInSitemap();
420        }
421        
422        public Map<String, String> getAttributes()
423        {
424            return _getAttributes(_page);
425        }
426        
427        public Map<String, String> getInternalAttributes()
428        {
429            return _getInternalAttributes(_page);
430        }
431        
432        public boolean hasReadAccess(UserIdentity user)
433        {
434            return _rightManager.hasReadAccess(user, _page);
435        }
436    }
437}