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