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