001/*
002 *  Copyright 2010 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.repository;
017
018import java.io.IOException;
019import java.net.MalformedURLException;
020import java.util.HashMap;
021import java.util.Map;
022import java.util.Map.Entry;
023import java.util.Objects;
024import java.util.Set;
025
026import org.apache.avalon.framework.service.ServiceException;
027import org.apache.avalon.framework.service.ServiceManager;
028import org.apache.cocoon.ProcessingException;
029import org.apache.cocoon.components.source.SourceUtil;
030import org.apache.cocoon.environment.ObjectModelHelper;
031import org.apache.cocoon.environment.Request;
032import org.apache.cocoon.generation.ServiceableGenerator;
033import org.apache.cocoon.xml.AttributesImpl;
034import org.apache.cocoon.xml.SaxBuffer;
035import org.apache.cocoon.xml.XMLUtils;
036import org.apache.commons.lang3.StringUtils;
037import org.apache.commons.lang3.exception.ExceptionUtils;
038import org.apache.excalibur.source.Source;
039import org.slf4j.Logger;
040import org.slf4j.LoggerFactory;
041import org.xml.sax.ContentHandler;
042import org.xml.sax.SAXException;
043
044import org.ametys.cms.content.ContentHelper;
045import org.ametys.cms.content.GetContentAction;
046import org.ametys.cms.contenttype.ContentTypeDescriptor;
047import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
048import org.ametys.cms.contenttype.ContentTypesHelper;
049import org.ametys.cms.contenttype.DynamicContentTypeDescriptorExtentionPoint;
050import org.ametys.cms.repository.Content;
051import org.ametys.cms.tag.Tag;
052import org.ametys.cms.tag.TagProviderExtensionPoint;
053import org.ametys.core.DevMode;
054import org.ametys.core.DevMode.DEVMODE;
055import org.ametys.core.right.RightManager;
056import org.ametys.core.right.RightManager.RightResult;
057import org.ametys.core.ui.ClientSideElement.ScriptFile;
058import org.ametys.core.util.IgnoreRootHandler;
059import org.ametys.core.util.URIUtils;
060import org.ametys.plugins.core.ui.ObfuscatedException;
061import org.ametys.plugins.repository.AmetysObject;
062import org.ametys.plugins.repository.AmetysObjectIterable;
063import org.ametys.plugins.repository.AmetysRepositoryException;
064import org.ametys.plugins.repository.provider.WorkspaceSelector;
065import org.ametys.runtime.authentication.AccessDeniedException;
066import org.ametys.runtime.authentication.AuthorizationRequiredException;
067import org.ametys.runtime.exception.ServiceUnavailableException;
068import org.ametys.web.WebConstants;
069import org.ametys.web.cache.monitoring.Constants;
070import org.ametys.web.cache.monitoring.process.access.ResourceAccessComponent;
071import org.ametys.web.cache.monitoring.process.access.impl.PageElementResourceAccess;
072import org.ametys.web.cache.monitoring.process.access.impl.PageElementResourceAccess.PageElementType;
073import org.ametys.web.cache.monitoring.process.access.impl.PageResourceAccess;
074import org.ametys.web.cache.pageelement.PageElementCache;
075import org.ametys.web.renderingcontext.RenderingContext;
076import org.ametys.web.renderingcontext.RenderingContextHandler;
077import org.ametys.web.repository.content.SharedContent;
078import org.ametys.web.repository.page.ContentTypesAssignmentHandler;
079import org.ametys.web.repository.page.ModifiablePage;
080import org.ametys.web.repository.page.MoveablePage;
081import org.ametys.web.repository.page.Page;
082import org.ametys.web.repository.page.Page.PageType;
083import org.ametys.web.repository.page.ServicesAssignmentHandler;
084import org.ametys.web.repository.page.SitemapElement;
085import org.ametys.web.repository.page.Zone;
086import org.ametys.web.repository.page.ZoneItem;
087import org.ametys.web.repository.page.ZoneItem.ZoneType;
088import org.ametys.web.repository.site.Site;
089import org.ametys.web.service.Service;
090import org.ametys.web.service.ServiceExtensionPoint;
091import org.ametys.web.skin.Skin;
092import org.ametys.web.skin.SkinTemplate;
093import org.ametys.web.skin.SkinTemplateZone;
094import org.ametys.web.skin.SkinsManager;
095
096/**
097 * Generator for SAXing <code>Content</code> associated with a Page.<br>
098 * SAX events are like :<br>
099 * <pageContents><br>
100 *   <zone id="..."><br>
101 *     <i><!-- XHTML content --></i><br>
102 *   </zone><br>
103 *   ...<br>
104 * </pageContents/><br>
105 */
106public class PageGenerator extends ServiceableGenerator
107{
108    private ServiceExtensionPoint _serviceExtPt;
109    private ContentTypeExtensionPoint _contentTypeExtPt;
110    private SkinsManager _skinsManager;
111    private TagProviderExtensionPoint _tagProviderEP;
112    private PageElementCache _zoneItemCache;
113    private WorkspaceSelector _workspaceSelector;
114    private RenderingContextHandler _renderingContextHandler;
115    private ContentTypesHelper _contentTypeHelper;
116    private DynamicContentTypeDescriptorExtentionPoint _dynamicCTDescriptorEP;
117    
118    /** The content type assignment handler. */
119    private ContentTypesAssignmentHandler _cTypeAssignmentHandler;
120
121    /** The service assignment handler. */
122    private ServicesAssignmentHandler _serviceAssignmentHandler;
123
124    /** The resource access monitoring component */
125    private ResourceAccessComponent _resourceAccessMonitor;
126
127    /** The monitored resource access */
128    private PageResourceAccess _pageAccess;
129
130    private ContentHelper _contentHelper;
131    
132    /** The right manager */
133    private RightManager _rightManager;
134
135    private int _zoneItemsInCache;
136    private int _zoneItemsSaxed;
137    private int _zonesSaxed;
138
139    private Logger _timeLogger = LoggerFactory.getLogger("org.ametys.web.rendering.time");
140
141    @Override
142    public void service(ServiceManager serviceManager) throws ServiceException
143    {
144        super.service(serviceManager);
145        _serviceExtPt = (ServiceExtensionPoint) serviceManager.lookup(ServiceExtensionPoint.ROLE);
146        _contentTypeExtPt = (ContentTypeExtensionPoint) serviceManager.lookup(ContentTypeExtensionPoint.ROLE);
147        _skinsManager = (SkinsManager) serviceManager.lookup(SkinsManager.ROLE);
148        _tagProviderEP = (TagProviderExtensionPoint) serviceManager.lookup(TagProviderExtensionPoint.ROLE);
149        _zoneItemCache = (PageElementCache) serviceManager.lookup(PageElementCache.ROLE + "/zoneItem");
150        _workspaceSelector = (WorkspaceSelector) serviceManager.lookup(WorkspaceSelector.ROLE);
151        _renderingContextHandler = (RenderingContextHandler) serviceManager.lookup(RenderingContextHandler.ROLE);
152        _cTypeAssignmentHandler = (ContentTypesAssignmentHandler) serviceManager.lookup(ContentTypesAssignmentHandler.ROLE);
153        _serviceAssignmentHandler = (ServicesAssignmentHandler) serviceManager.lookup(ServicesAssignmentHandler.ROLE);
154        _resourceAccessMonitor = (ResourceAccessComponent) serviceManager.lookup(ResourceAccessComponent.ROLE);
155        _contentTypeHelper = (ContentTypesHelper) serviceManager.lookup(ContentTypesHelper.ROLE);
156        _dynamicCTDescriptorEP = (DynamicContentTypeDescriptorExtentionPoint) serviceManager.lookup(DynamicContentTypeDescriptorExtentionPoint.ROLE);
157        _contentHelper = (ContentHelper) serviceManager.lookup(ContentHelper.ROLE);
158        _rightManager = (RightManager) serviceManager.lookup(RightManager.ROLE);
159    }
160
161    @Override
162    public void generate() throws IOException, SAXException, ProcessingException
163    {
164        long t0 = System.currentTimeMillis();
165
166        _zoneItemsInCache = 0;
167        _zoneItemsSaxed = 0;
168        _zonesSaxed = 0;
169
170        Request request = ObjectModelHelper.getRequest(objectModel);
171
172        Page page = (Page) request.getAttribute(WebConstants.REQUEST_ATTR_PAGE);
173        String title = page.getTitle();
174
175        RenderingContext renderingContext = _renderingContextHandler.getRenderingContext();
176        String workspace = _workspaceSelector.getWorkspace();
177        String siteName = page.getSiteName();
178
179        if (page.getType() != PageType.CONTAINER)
180        {
181            throw new IllegalStateException("Cannot invoke the PageGenerator on a Page without a PageContent");
182        }
183
184        // Monitor the access to this page.
185        _pageAccess = (PageResourceAccess) request.getAttribute(Constants.REQUEST_ATTRIBUTE_PAGEACCESS);
186        
187        if (_pageAccess != null)
188        {
189            _pageAccess.setRenderingContext(renderingContext);
190            _pageAccess.setWorkspaceJCR(workspace);
191            _resourceAccessMonitor.addAccessRecord(_pageAccess);
192        }
193
194        contentHandler.startDocument();
195        AttributesImpl attrs = new AttributesImpl();
196        attrs.addCDATAAttribute("title", title);
197        attrs.addCDATAAttribute("long-title", page.getLongTitle());
198        attrs.addCDATAAttribute("id", page.getId());
199        XMLUtils.startElement(contentHandler, "page", attrs);
200
201        try
202        {
203            XMLUtils.startElement(contentHandler, "metadata");
204            page.dataToSAX(contentHandler);
205            XMLUtils.endElement(contentHandler, "metadata");
206
207            // Tags
208            XMLUtils.startElement(contentHandler, "tags");
209            Set<String> tags = page.getTags();
210            for (String tagName : tags)
211            {
212                Map<String, Object> contextParameters = new HashMap<>();
213                contextParameters.put("siteName", siteName);
214                
215                Tag tag = _tagProviderEP.getTag(tagName, contextParameters);
216
217                if (tag != null)
218                {
219                    // tag may be null if it has been registered on the page and then removed from the application
220                    AttributesImpl tagattrs = new AttributesImpl();
221                    if (tag.getParentName() != null)
222                    {
223                        tagattrs.addCDATAAttribute("parent", tag.getParentName());
224                    }
225
226                    XMLUtils.startElement(contentHandler, tagName, tagattrs);
227                    tag.getTitle().toSAX(contentHandler);
228                    XMLUtils.endElement(contentHandler, tagName);
229                }
230            }
231            XMLUtils.endElement(contentHandler, "tags");
232        }
233        catch (AmetysRepositoryException e)
234        {
235            _pageAccess = null;
236            throw new ProcessingException("Unable to SAX page metadata", e);
237        }
238
239        long t1 = System.currentTimeMillis();
240
241        AttributesImpl pcattrs = new AttributesImpl();
242        pcattrs.addCDATAAttribute("modifiable", Boolean.toString(page instanceof ModifiablePage));
243        pcattrs.addCDATAAttribute("moveable", Boolean.toString(page instanceof MoveablePage));
244        XMLUtils.startElement(contentHandler, "pageContents", pcattrs);
245
246        try
247        {
248            // Iterate on existing zones
249            for (Zone zone : page.getZones())
250            {
251                String zoneName = zone.getName();
252                AmetysObjectIterable<? extends ZoneItem> zoneItems = zone.getZoneItems();
253
254                _saxZone(page, zoneName, zoneItems, workspace, siteName, renderingContext);
255            }
256
257            // Iterate on defined zone (that are not existing)
258            SkinTemplate skinTemplate = _getTemplateDefinition(page);
259            if (skinTemplate != null)
260            {
261                for (SkinTemplateZone zoneDef : skinTemplate.getZones().values())
262                {
263                    if (!page.hasZone(zoneDef.getId()))
264                    {
265                        _saxZone(page, zoneDef.getId(), null, workspace, siteName, renderingContext);
266                    }
267                }
268            }
269        }
270        catch (AmetysRepositoryException ex)
271        {
272            _pageAccess = null;
273            throw new ProcessingException("Unable to get Content", ex);
274        }
275
276        long t2 = System.currentTimeMillis();
277        _timeLogger.debug("Zones processing time: {} ms", t2 - t1);
278        if (getLogger().isInfoEnabled())
279        {
280            getLogger().info("PageGenerator\t/" + page.getSiteName() + "/" + page.getSitemapName() + "/" + page.getPathInSitemap() + "\t" + page.getId() + "\tprocessing time (in ms):\t" + (t2 - t0) + "\tRendering context:\t" + renderingContext + "\tSaxing (zones, total zoneItems, zoneItems from cache):\t" + _zonesSaxed + "\t" + _zoneItemsSaxed + "\t" + _zoneItemsInCache);
281        }
282
283        XMLUtils.endElement(contentHandler, "pageContents");
284        XMLUtils.endElement(contentHandler, "page");
285        contentHandler.endDocument();
286
287        _timeLogger.debug("Page processing time: {} ms", t2 - t0);
288
289        _pageAccess = null;
290    }
291
292    /**
293     * Sax a zone
294     * @param page The page
295     * @param zoneName The zone in the page to sax
296     * @param zoneItems The items of the zone or null
297     * @param workspace the workspace
298     * @param site the site's name
299     * @param renderingContext the rendering context
300     * @throws SAXException if an error occurs while saxing
301     * @throws IOException if an I/O exception occurs
302     * @throws ProcessingException if an error occurs
303     */
304    private void _saxZone(Page page, String zoneName, AmetysObjectIterable<? extends ZoneItem> zoneItems, String workspace, String site, RenderingContext renderingContext) throws SAXException, IOException, ProcessingException
305    {
306        _zonesSaxed++;
307
308        AmetysObjectIterable<? extends ZoneItem> localZoneItems = zoneItems;
309
310        AttributesImpl zoneAttrs = new AttributesImpl();
311        zoneAttrs.addCDATAAttribute("name", zoneName);
312
313        if (localZoneItems == null || !localZoneItems.iterator().hasNext())
314        {
315            // zone is empty => try to inherit
316            Zone parentPageZone = _inherit(page, page, zoneName);
317            if (parentPageZone != null)
318            {
319                zoneAttrs.addCDATAAttribute("inherited", parentPageZone.getSitemapElement().getId());
320
321                localZoneItems = parentPageZone.getZoneItems();
322            }
323        }
324
325        Request request = ObjectModelHelper.getRequest(objectModel);
326        request.setAttribute(Zone.class.getName(), zoneName);
327
328        XMLUtils.startElement(contentHandler, "zone", zoneAttrs);
329
330        if (renderingContext == RenderingContext.BACK)
331        {
332            _saxAvailableContentTypes(page, zoneName);
333            _saxAvailableServices(page, zoneName);
334        }
335        
336        _saxZoneItems(page, localZoneItems, workspace, site, renderingContext);
337
338        XMLUtils.endElement(contentHandler, "zone");
339        request.setAttribute(Zone.class.getName(), null);
340
341    }
342
343    /**
344     * Generate the list of available services for the given zone.
345     * @param page the page.
346     * @param zoneName the zone name in the page.
347     * @throws SAXException if something goes wrong when saxing the available services
348     */
349    private void _saxAvailableServices(Page page, String zoneName) throws SAXException
350    {
351        Set<String> services = _serviceAssignmentHandler.getAvailableServices(page, zoneName);
352
353        XMLUtils.startElement(contentHandler, "available-services");
354
355        for (String service : services)
356        {
357            AttributesImpl attrs = new AttributesImpl();
358            attrs.addCDATAAttribute("id", service);
359            XMLUtils.createElement(contentHandler, "service", attrs);
360        }
361
362        XMLUtils.endElement(contentHandler, "available-services");
363    }
364
365    /**
366     * Generate the list of available content types for the given zone.
367     * @param page the page.
368     * @param zoneName the zone name in the page.
369     * @throws SAXException if something goes wrong when saxing the available content types
370     */
371    private void _saxAvailableContentTypes(Page page, String zoneName) throws SAXException
372    {
373        Set<String> cTypes = _cTypeAssignmentHandler.getAvailableContentTypes(page, zoneName, true);
374
375        XMLUtils.startElement(contentHandler, "available-content-types");
376
377        for (String cType : cTypes)
378        {
379            AttributesImpl attrs = new AttributesImpl();
380            attrs.addCDATAAttribute("id", cType);
381            XMLUtils.createElement(contentHandler, "content-type", attrs);
382        }
383
384        XMLUtils.endElement(contentHandler, "available-content-types");
385    }
386
387    /**
388     * Sax zone items
389     * @param page the page
390     * @param zoneItems The zone items to sax
391     * @param workspace the workspace
392     * @param site the site's name
393     * @param renderingContext the rendering context
394     * @throws SAXException if an error occurs while saxing
395     * @throws IOException if an I/O exception occurs
396     * @throws ProcessingException if an error occurs
397     */
398    private void _saxZoneItems(Page page, AmetysObjectIterable< ? extends ZoneItem> zoneItems, String workspace, String site, RenderingContext renderingContext) throws SAXException, IOException, ProcessingException
399    {
400        if (zoneItems == null)
401        {
402            return;
403        }
404
405        Request request = ObjectModelHelper.getRequest(objectModel);
406
407        for (ZoneItem zoneItem : zoneItems)
408        {
409            _saxZoneItem(page, workspace, site, renderingContext, request, zoneItem);
410        }
411    }
412    
413    private void _handleZoneAccess(PageElementResourceAccess zoneAccess, boolean cacheable, boolean hit)
414    {
415        if (zoneAccess != null)
416        {
417            zoneAccess.setCacheable(cacheable);
418            zoneAccess.setCacheHit(hit);
419        }
420    }
421
422    private void _saxZoneItem(Page page, String workspace, String site, RenderingContext renderingContext, Request request, ZoneItem zoneItem) throws SAXException, ProcessingException, IOException
423    {
424        long t0 = System.currentTimeMillis();
425
426        _zoneItemsSaxed++;
427
428        String id = zoneItem.getId();
429        ZoneType type = zoneItem.getType();
430
431        request.setAttribute(WebConstants.REQUEST_ATTR_ZONEITEM, zoneItem);
432
433        AttributesImpl zoneItemAttrs = new AttributesImpl();
434        zoneItemAttrs.addCDATAAttribute("id", id);
435
436        PageElementResourceAccess zoneAccess = _pageAccess != null ? _pageAccess.createPageElementAccess(id, PageElementType.fromZoneItemType(type)) : null;
437
438        // try to get content from cache only if it is cacheable
439        SaxBuffer cachedContent = null;
440        Exception ex = null;
441
442        boolean isCacheable = false;
443        if (type == ZoneType.CONTENT)
444        {
445            // a Content is always cacheable
446            isCacheable = true;
447        }
448        else if (type == ZoneType.SERVICE)
449        {
450            String serviceId = zoneItem.getServiceId();
451            Service service = _serviceExtPt.getExtension(serviceId);
452
453            if (service != null)
454            {
455                try
456                {
457                    isCacheable = service.isCacheable(page, zoneItem);
458                }
459                catch (Exception e)
460                {
461                    ex = new ProcessingException("Error testing the service cachability.", e);
462                }
463            }
464            // if service is null, an exception will be thrown later
465        }
466
467        _handleZoneAccess(zoneAccess, isCacheable, false);
468        
469        request.setAttribute("IsZoneItemCacheable", isCacheable);
470        
471        if (isCacheable)
472        {
473            cachedContent = _zoneItemCache.getPageElement(workspace, site, _getType(zoneItem), id, page.getId(), renderingContext);
474        }
475        
476        if (cachedContent != null)
477        {
478            _handleZoneAccess(zoneAccess, true, true);
479            
480            _zoneItemsInCache++;
481            
482            request.setAttribute("IsZoneItemCacheable", true);
483
484            cachedContent.toSAX(contentHandler);
485        }
486        else
487        {
488
489            SaxBuffer buffer = null;
490            ContentHandler handler; // the actual ContentHandler, either the real one, or a buffer
491            if (isCacheable)
492            {
493                buffer = new SaxBuffer();
494                handler = buffer;
495            }
496            else
497            {
498                handler = contentHandler;
499            }
500
501            XMLUtils.startElement(handler, "zoneItem", zoneItemAttrs);
502
503            XMLUtils.startElement(handler, "information");
504            XMLUtils.createElement(handler, "type", type.toString());
505
506            Object result = null;
507            if (type == ZoneType.CONTENT)
508            {
509                if (getLogger().isDebugEnabled())
510                {
511                    Content content = zoneItem.getContent();
512                    getLogger().debug("Processing content " + content.getId() + " / " + content.getPath());
513                }
514                
515                result = _saxContentZoneItem(zoneItem, handler, request);
516            }
517            else if (type == ZoneType.SERVICE)
518            {
519                if (getLogger().isDebugEnabled())
520                {
521                    getLogger().debug("Processing service " + zoneItem.getServiceId());
522                }
523                
524                result = _saxServiceZoneItem(zoneItem, handler, ex);
525            }
526
527            Source src = null;
528            if (result instanceof Source)
529            {
530                src = (Source) result;
531            }
532            else
533            {
534                ex = (Exception) result;
535            }
536            
537            XMLUtils.endElement(handler, "information");
538
539            _saxSource(handler, src, ex, isCacheable);
540
541            XMLUtils.endElement(handler, "zoneItem");
542
543            // finally store the buffered data in the cache and SAX it to the pipeline
544            if (buffer != null)
545            {
546                buffer.toSAX(contentHandler);
547                _zoneItemCache.storePageElement(workspace, site, _getType(zoneItem), id, page.getId(), renderingContext, buffer);
548            }
549        }
550
551        // Monitor the access to this zone item.
552        _resourceAccessMonitor.addAccessRecord(zoneAccess);
553
554        // Empty content request attributes
555        request.setAttribute(Content.class.getName(), null);
556        // Empty zone item request attribute
557        request.setAttribute(WebConstants.REQUEST_ATTR_ZONEITEM, null);
558        // Empty zone item cacheable attribute
559        request.setAttribute("IsZoneItemCacheable", null);
560        
561        _timeLogger.debug("Zone item {} processing time: {} ms", id, System.currentTimeMillis() - t0);
562    }
563    
564    private Object _saxContentZoneItem(ZoneItem zoneItem, ContentHandler handler, Request request) throws SAXException, MalformedURLException, IOException
565    {
566        try
567        {
568            Content content = zoneItem.getContent();
569            String viewName = Objects.toString(zoneItem.getViewName(), "main");
570
571            XMLUtils.createElement(handler, "contentId", content.getId());
572            XMLUtils.createElement(handler, "contentName", content.getName());
573            XMLUtils.createElement(handler, "metadataSetName", viewName);
574            if (content instanceof SharedContent)
575            {
576                XMLUtils.createElement(handler, "sharedContent", "true");
577            }
578
579            String contentTypeId = _contentTypeHelper.getContentTypeIdForRendering(content);
580            
581            ContentTypeDescriptor contentType = _contentTypeExtPt.getExtension(contentTypeId);
582            if (contentType == null)
583            {
584                contentType = _dynamicCTDescriptorEP.getExtension(contentTypeId);
585            }
586            
587            if (contentType == null)
588            {
589                return new IllegalStateException("The content type '" + contentTypeId + "' is referenced but does not exist");
590            }
591            else
592            {
593                AttributesImpl contentTypeAttrs = new AttributesImpl();
594                contentTypeAttrs.addCDATAAttribute("id", contentType.getId());
595                XMLUtils.startElement(handler, "type-information", contentTypeAttrs);
596
597                contentType.getLabel().toSAX(handler, "label");
598                contentType.getDescription().toSAX(handler, "description");
599                
600                if (contentType.getIconGlyph() != null)
601                {
602                    XMLUtils.createElement(handler, "iconGlyph", contentType.getIconGlyph());
603                }
604                if (contentType.getIconDecorator() != null)
605                {
606                    XMLUtils.createElement(handler, "iconDecorator", contentType.getIconDecorator());
607                }
608                
609                if (contentType.getSmallIcon() != null)
610                {
611                    XMLUtils.createElement(handler, "smallIcon", contentType.getSmallIcon());
612                    XMLUtils.createElement(handler, "mediumIcon", contentType.getMediumIcon());
613                    XMLUtils.createElement(handler, "largeIcon", contentType.getLargeIcon());
614                }
615                
616                XMLUtils.startElement(handler, "css");
617                for (ScriptFile cssFile : contentType.getCSSFiles())
618                {
619                    _saxCSSFile(handler, cssFile);
620                }
621                XMLUtils.endElement(handler, "css");
622                
623                XMLUtils.endElement(handler, "type-information");
624
625                String url = "cocoon://_plugins/" + contentType.getPluginName() + "/" + contentType.getId() + ".html";
626                Map<String, String> urlParams = _contentHelper.getContentViewUrlParameters(content, viewName, "html");
627                
628                // FIXME use a context
629                request.setAttribute(Content.class.getName(), content);
630                request.setAttribute(GetContentAction.RESULT_CONTENTTYPE, contentTypeId);
631                return resolver.resolveURI(URIUtils.buildURI(url, urlParams));
632            }
633        }
634        catch (AmetysRepositoryException e)
635        {
636            return new ProcessingException("Unable to get content property", e);
637        }
638    }
639    
640    private Object _saxServiceZoneItem(ZoneItem zoneItem, ContentHandler handler, Exception ex) throws SAXException, MalformedURLException, IOException
641    {
642        String serviceId = zoneItem.getServiceId();
643        Service service = _serviceExtPt.getExtension(serviceId);
644
645        if (service == null)
646        {
647            return new ProcessingException("Unable to get service for name '" + serviceId + "'");
648        }
649        else if (ex == null) // If an exception was caught while testing the service cacheability, do not generate
650        {
651            AttributesImpl serviceAttrs = new AttributesImpl();
652            serviceAttrs.addCDATAAttribute("id", service.getId());
653            XMLUtils.startElement(handler, "type-information", serviceAttrs);
654
655            service.getLabel().toSAX(handler, "label");
656            service.getDescription().toSAX(handler, "description");
657            
658            if (service.getIconGlyph() != null)
659            {
660                XMLUtils.createElement(handler, "iconGlyph", service.getIconGlyph());
661            }
662            if (service.getIconDecorator() != null)
663            {
664                XMLUtils.createElement(handler, "iconDecorator", service.getIconDecorator());
665            }
666            if (service.getSmallIcon() != null)
667            {
668                XMLUtils.createElement(handler, "smallIcon", service.getSmallIcon());
669                XMLUtils.createElement(handler, "mediumIcon", service.getMediumIcon());
670            }
671            
672            XMLUtils.startElement(handler, "css");
673            for (ScriptFile cssFile : service.getCSSFiles())
674            {
675                _saxCSSFile(handler, cssFile);
676            }
677            XMLUtils.endElement(handler, "css");
678            
679            XMLUtils.endElement(handler, "type-information");
680
681            return resolver.resolveURI(service.getURL(), null, PageGeneratorHelper.getParameters(service, zoneItem));
682        }
683        else
684        {
685            return ex;
686        }
687    }
688    
689    private void _saxCSSFile(ContentHandler handler, ScriptFile cssFile) throws SAXException
690    {
691        AttributesImpl fileAttrs = new AttributesImpl();
692        if (!cssFile.isLangSpecific())
693        {
694            String rtlMode = cssFile.getRtlMode();
695            if (rtlMode != null && !"all".equals(rtlMode))
696            {
697                fileAttrs.addCDATAAttribute("rtl", rtlMode);
698            }
699            
700            XMLUtils.createElement(handler, "file", fileAttrs, cssFile.getPath());
701        }
702        else
703        {
704            fileAttrs.addCDATAAttribute("lang", "true");
705            XMLUtils.startElement(handler, "file", fileAttrs);
706            
707            String defaultLang = cssFile.getDefaultLang();
708            Map<String, String> langPaths = cssFile.getLangPaths();
709            
710            for (Entry<String, String> langPath : langPaths.entrySet())
711            {
712                AttributesImpl langAttrs = new AttributesImpl();
713                
714                String codeLang = langPath.getKey();
715                langAttrs.addCDATAAttribute("code", codeLang);
716                if (codeLang.equals(defaultLang))
717                {
718                    langAttrs.addCDATAAttribute("default", "true");
719                }
720                
721                XMLUtils.createElement(handler, "lang", langAttrs, langPath.getValue());
722            }
723
724            XMLUtils.endElement(handler, "file");
725        }
726    }
727
728    private void _saxSource(ContentHandler handler, Source src, Exception ex, boolean isCacheable) throws SAXException, IOException, ProcessingException
729    {
730        if (src == null)
731        {
732            Throwable e = _obfuscate(ex, isCacheable);
733            if (e instanceof ObfuscatedException obfuscatedException)
734            {
735                obfuscatedException.reveal();
736                getLogger().error("Unable to display zone item", ex);
737                obfuscatedException.obfuscate();
738            }
739            else
740            {
741                getLogger().error("Unable to display zone item", ex);
742            }
743            
744            _saxError(handler, e);
745        }
746        else
747        {
748            try
749            {
750                SourceUtil.toSAX(src, new IgnoreRootHandler(handler));
751            }
752            catch (ProcessingException e)
753            {
754                if (_throwException(e))
755                {
756                    getLogger().error("Unable to display zone item", e);
757                    throw e;
758                }
759                else
760                {
761                    Throwable cause = _obfuscate(e.getCause(), isCacheable);
762                    if (cause instanceof ObfuscatedException obfuscatedException)
763                    {
764                        obfuscatedException.reveal();
765                        getLogger().error("Unable to display zone item", cause);
766                        obfuscatedException.obfuscate();
767                    }
768                    else
769                    {
770                        getLogger().error("Unable to display zone item", cause);
771                    }
772                    _saxError(handler, cause);
773                }
774            }
775            finally
776            {
777                resolver.release(src);
778            }
779        }
780    }
781
782    private String _getType(ZoneItem zoneItem)
783    {
784        ZoneType type = zoneItem.getType();
785
786        if (type == ZoneType.CONTENT)
787        {
788            return "CONTENT";
789        }
790        else
791        {
792            return "SERVICE:" + zoneItem.getServiceId();
793        }
794    }
795
796    /**
797     * Get the template definition for a page
798     * @param sitemapElement The page. Cannot be null.
799     * @return The template definition. Null if the page is not a container or if the template is not declared.
800     */
801    private SkinTemplate _getTemplateDefinition(SitemapElement sitemapElement)
802    {
803        String templateName = sitemapElement.getTemplate();
804        if (templateName == null)
805        {
806            return null;
807        }
808
809        Site site = sitemapElement.getSite();
810        String skinId = site.getSkinId();
811        try
812        {
813            Skin skinDef = _skinsManager.getSkin(skinId);
814            return skinDef.getTemplate(templateName);
815        }
816        catch (IllegalStateException e)
817        {
818            getLogger().error("Cannot get template definition for page '" + sitemapElement.getId() + "' using template '" + templateName + "' in skin '" + skinId + "'");
819            return null;
820        }
821    }
822
823    /**
824     * Try to inherit the zone (as it is empty)
825     * @param childPage The child page that do inherit. Cannot be null
826     * @param page The page to inherit. Cannot be null
827     * @param zoneName The zone name in the page to inherit. Cannot be null or empty
828     * @return The zone inherited or null.
829     */
830    private Zone _inherit(Page childPage, SitemapElement page, String zoneName)
831    {
832        // The page has an existing zone at this place ?
833        if (page.hasZone(zoneName))
834        {
835            Zone zone = page.getZone(zoneName);
836            AmetysObjectIterable<? extends ZoneItem> zoneItems = zone.getZoneItems();
837
838            // With data ?
839            if (zoneItems.iterator().hasNext())
840            {
841                // This is it (end of the recursion)
842                return zone;
843            }
844        }
845
846        // Get the definition for the zone
847        SkinTemplateZone zoneDef;
848
849        Site site = page.getSite();
850        String skinId = site.getSkinId();
851        String templateName = page.getTemplate();
852        try
853        {
854            SkinTemplate templateDef = _getTemplateDefinition(page);
855            zoneDef = templateDef.getZone(zoneName);
856        }
857        catch (IllegalStateException e)
858        {
859            getLogger().error("The page '" + childPage.getId() + "' cannot inherit a zone '" + zoneName + "' of template '" + templateName + "' in skin '" + skinId + "' as asked for page '" + page.getId() + "' in site '" + site.getName() + "'", e);
860            return null;
861        }
862
863        // This zone is not defined for the template
864        if (zoneDef == null)
865        {
866            getLogger().warn("The page '" + childPage.getId() + "' cannot inherit the undefined zone '" + zoneName + "' of template '" + templateName + "' in skin '" + skinId + "' as asked for page '" + page.getId() + "' in site '" + site.getName() + "'.");
867            return null;
868        }
869
870        // Support inheritance ?
871        if (!zoneDef.hasInheritance())
872        {
873            return null;
874        }
875
876        // Get the parent page (that is a container page)
877        SitemapElement parentPage = page;
878        do
879        {
880            AmetysObject parentObject = parentPage.getParent();
881            if (parentObject instanceof SitemapElement pc)
882            {
883                parentPage = pc;
884            }
885            else
886            {
887                // inheritance goes back to the root
888                return null;
889            }
890        }
891        while (parentPage.getTemplate() == null);
892
893        // Get the name of the zone which will be the inheritance source
894        String parentPageTemplate = parentPage.getTemplate();
895        String inheritanceSrc = zoneDef.getInheritance(parentPageTemplate);
896        if (inheritanceSrc == null)
897        {
898            // No inheritance for this template
899            return null;
900        }
901
902        // Finally we will inherit from the parentPage and the zone inheritanceSrc
903        return _inherit(childPage, parentPage, inheritanceSrc);
904    }
905
906    /**
907     * Test if the error has to be thrown instead of SAXing it as an error zone item.
908     * @param ex the exception.
909     * @return true to throw the exception, false to catch it and SAX it as an error zone item.
910     */
911    private boolean _throwException(Exception ex)
912    {
913        return _renderingContextHandler.getRenderingContext() == RenderingContext.FRONT
914            && (
915                _isExceptionType(ex, AuthorizationRequiredException.class)
916                || _isExceptionType(ex, AccessDeniedException.class)
917                // See CMS-12302 if you want to change that one day
918                || _isExceptionType(ex, ServiceUnavailableException.class)
919            );
920    }
921    
922    private boolean _isExceptionType(Throwable throwable, Class<? extends Throwable> clazz)
923    {
924        return ExceptionUtils.indexOfThrowable(throwable, clazz) > -1;
925    }
926
927    private void _saxError(ContentHandler handler, Throwable e) throws SAXException
928    {
929        XMLUtils.startElement(handler, "zone-item-error");
930        
931        XMLUtils.createElement(handler, "exception-message", e != null ? StringUtils.defaultString(e.getMessage()) : "");
932        XMLUtils.createElement(handler, "exception-stack-trace", StringUtils.defaultString(ExceptionUtils.getStackTrace(e)));
933        
934        XMLUtils.endElement(handler, "zone-item-error");
935    }
936    
937    private Throwable _obfuscate(Throwable throwable, boolean isCacheable)
938    {
939        if (!DEVMODE.PRODUCTION.equals(DevMode.getDeveloperMode()) // Do not read dev mode from request to prevent override from request param
940                || !isCacheable && _rightManager.currentUserHasRight("Runtime_Rights_Admin_Access", "/admin").equals(RightResult.RIGHT_ALLOW))
941        {
942            return throwable;
943        }
944        else
945        {
946            return ObfuscatedException.obfuscate(throwable);
947        }
948    }
949}