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