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.util.Map;
020import java.util.Map.Entry;
021import java.util.Set;
022
023import org.apache.avalon.framework.service.ServiceException;
024import org.apache.avalon.framework.service.ServiceManager;
025import org.apache.cocoon.ProcessingException;
026import org.apache.cocoon.environment.ObjectModelHelper;
027import org.apache.cocoon.environment.Request;
028import org.apache.cocoon.generation.ServiceableGenerator;
029import org.apache.cocoon.xml.AttributesImpl;
030import org.apache.cocoon.xml.XMLUtils;
031import org.apache.commons.lang.StringUtils;
032import org.apache.commons.lang.exception.ExceptionUtils;
033import org.xml.sax.ContentHandler;
034import org.xml.sax.SAXException;
035
036import org.ametys.cms.content.GetContentAction;
037import org.ametys.cms.contenttype.ContentTypeDescriptor;
038import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
039import org.ametys.cms.contenttype.ContentTypesHelper;
040import org.ametys.cms.contenttype.DynamicContentTypeDescriptorExtentionPoint;
041import org.ametys.cms.repository.Content;
042import org.ametys.core.DevMode;
043import org.ametys.core.DevMode.DEVMODE;
044import org.ametys.core.right.RightManager;
045import org.ametys.core.right.RightManager.RightResult;
046import org.ametys.core.ui.ClientSideElement.ScriptFile;
047import org.ametys.core.util.I18nUtils;
048import org.ametys.plugins.core.ui.ObfuscatedException;
049import org.ametys.plugins.repository.AmetysObjectIterable;
050import org.ametys.plugins.repository.AmetysRepositoryException;
051import org.ametys.runtime.i18n.I18nizableText;
052import org.ametys.web.WebConstants;
053import org.ametys.web.repository.content.SharedContent;
054import org.ametys.web.repository.page.ContentTypesAssignmentHandler;
055import org.ametys.web.repository.page.ServicesAssignmentHandler;
056import org.ametys.web.repository.page.Zone;
057import org.ametys.web.repository.page.ZoneItem;
058import org.ametys.web.repository.page.ZoneItem.ZoneType;
059import org.ametys.web.repository.site.Site;
060import org.ametys.web.repository.sitemap.Sitemap;
061import org.ametys.web.service.Service;
062import org.ametys.web.service.ServiceExtensionPoint;
063import org.ametys.web.skin.Skin;
064import org.ametys.web.skin.SkinTemplate;
065import org.ametys.web.skin.SkinTemplateZone;
066import org.ametys.web.skin.SkinsManager;
067
068/**
069 * Emulator of {@link PageGenerator}
070 */
071public class SitemapPageGenerator extends ServiceableGenerator
072{
073    private ServiceExtensionPoint _serviceExtPt;
074    private ContentTypeExtensionPoint _contentTypeExtPt;
075    private SkinsManager _skinsManager;
076    private ContentTypesHelper _contentTypeHelper;
077    private DynamicContentTypeDescriptorExtentionPoint _dynamicCTDescriptorEP;
078    private RightManager _rightManager;
079   
080    /** The content type assignment handler. */
081    private ContentTypesAssignmentHandler _cTypeAssignmentHandler;
082
083    /** The service assignment handler. */
084    private ServicesAssignmentHandler _serviceAssignmentHandler;
085    private I18nUtils _i18nUtils;
086
087    @Override
088    public void service(ServiceManager serviceManager) throws ServiceException
089    {
090        super.service(serviceManager);
091        _serviceExtPt = (ServiceExtensionPoint) serviceManager.lookup(ServiceExtensionPoint.ROLE);
092        _contentTypeExtPt = (ContentTypeExtensionPoint) serviceManager.lookup(ContentTypeExtensionPoint.ROLE);
093        _skinsManager = (SkinsManager) serviceManager.lookup(SkinsManager.ROLE);
094        _cTypeAssignmentHandler = (ContentTypesAssignmentHandler) serviceManager.lookup(ContentTypesAssignmentHandler.ROLE);
095        _serviceAssignmentHandler = (ServicesAssignmentHandler) serviceManager.lookup(ServicesAssignmentHandler.ROLE);
096        _contentTypeHelper = (ContentTypesHelper) serviceManager.lookup(ContentTypesHelper.ROLE);
097        _dynamicCTDescriptorEP = (DynamicContentTypeDescriptorExtentionPoint) serviceManager.lookup(DynamicContentTypeDescriptorExtentionPoint.ROLE);
098        _i18nUtils = (I18nUtils) serviceManager.lookup(I18nUtils.ROLE);
099        _rightManager = (RightManager) serviceManager.lookup(RightManager.ROLE);
100    }
101
102    @Override
103    public void generate() throws IOException, SAXException, ProcessingException
104    {
105        Request request = ObjectModelHelper.getRequest(objectModel);
106
107        Sitemap sitemap = (Sitemap) request.getAttribute(WebConstants.REQUEST_ATTR_SITEMAP);
108
109        String siteName = sitemap.getSiteName();
110
111        if (sitemap.getTemplate() == null)
112        {
113            throw new IllegalStateException("Cannot invoke the SitemapPageGenerator on a Sitemap without a template");
114        }
115
116        contentHandler.startDocument();
117        AttributesImpl attrs = new AttributesImpl();
118        attrs.addCDATAAttribute("title", sitemap.getName());
119        attrs.addCDATAAttribute("long-title", sitemap.getName());
120        attrs.addCDATAAttribute("id", sitemap.getId());
121        XMLUtils.startElement(contentHandler, "page", attrs);
122
123        try
124        {
125            XMLUtils.startElement(contentHandler, "metadata");
126            sitemap.dataToSAX(contentHandler);
127            XMLUtils.endElement(contentHandler, "metadata");
128        }
129        catch (AmetysRepositoryException e)
130        {
131            throw new ProcessingException("Unable to SAX sitemap metadata", e);
132        }
133
134        AttributesImpl pcattrs = new AttributesImpl();
135        pcattrs.addCDATAAttribute("modifiable", "true");
136        pcattrs.addCDATAAttribute("moveable", "false");
137        XMLUtils.startElement(contentHandler, "pageContents", pcattrs);
138
139        try
140        {
141            // Iterate on existing zones
142            for (Zone zone : sitemap.getZones())
143            {
144                String zoneName = zone.getName();
145                AmetysObjectIterable<? extends ZoneItem> zoneItems = zone.getZoneItems();
146
147                _saxZone(sitemap, zoneName, zoneItems, siteName);
148            }
149
150            // Iterate on defined zone (that are not existing)
151            SkinTemplate skinTemplate = _getTemplateDefinition(sitemap);
152            if (skinTemplate != null)
153            {
154                for (SkinTemplateZone zoneDef : skinTemplate.getZones().values())
155                {
156                    if (!sitemap.hasZone(zoneDef.getId()))
157                    {
158                        _saxZone(sitemap, zoneDef.getId(), null, siteName);
159                    }
160                }
161            }
162        }
163        catch (AmetysRepositoryException ex)
164        {
165            throw new ProcessingException("Unable to get Content", ex);
166        }
167
168        XMLUtils.endElement(contentHandler, "pageContents");
169        XMLUtils.endElement(contentHandler, "page");
170        contentHandler.endDocument();
171    }
172
173    /**
174     * Sax a zone
175     * @param sitemap The page
176     * @param zoneName The zone in the page to sax
177     * @param zoneItems The items of the zone or null
178     * @param site the site's name
179     * @throws SAXException if an error occurs while saxing
180     * @throws IOException if an I/O exception occurs
181     * @throws ProcessingException if an error occurs
182     */
183    private void _saxZone(Sitemap sitemap, String zoneName, AmetysObjectIterable<? extends ZoneItem> zoneItems, String site) throws SAXException, IOException, ProcessingException
184    {
185        AmetysObjectIterable<? extends ZoneItem> localZoneItems = zoneItems;
186
187        AttributesImpl zoneAttrs = new AttributesImpl();
188        zoneAttrs.addCDATAAttribute("name", zoneName);
189
190        Request request = ObjectModelHelper.getRequest(objectModel);
191        request.setAttribute(Zone.class.getName(), zoneName);
192
193        XMLUtils.startElement(contentHandler, "zone", zoneAttrs);
194
195        _saxAvailableContentTypes(sitemap, zoneName);
196        _saxAvailableServices(sitemap, zoneName);
197
198        _saxZoneItems(localZoneItems);
199
200        XMLUtils.endElement(contentHandler, "zone");
201        request.setAttribute(Zone.class.getName(), null);
202
203    }
204
205    /**
206     * Generate the list of available services for the given zone.
207     * @param sitemap the page.
208     * @param zoneName the zone name in the page.
209     * @throws SAXException if something goes wrong when saxing the available services
210     */
211    private void _saxAvailableServices(Sitemap sitemap, String zoneName) throws SAXException
212    {
213        Set<String> services = _serviceAssignmentHandler.getAvailableServices(sitemap, zoneName);
214
215        XMLUtils.startElement(contentHandler, "available-services");
216
217        for (String service : services)
218        {
219            AttributesImpl attrs = new AttributesImpl();
220            attrs.addCDATAAttribute("id", service);
221            XMLUtils.createElement(contentHandler, "service", attrs);
222        }
223
224        XMLUtils.endElement(contentHandler, "available-services");
225    }
226
227    /**
228     * Generate the list of available content types for the given zone.
229     * @param sitemap the page.
230     * @param zoneName the zone name in the page.
231     * @throws SAXException if something goes wrong when saxing the available content types
232     */
233    private void _saxAvailableContentTypes(Sitemap sitemap, String zoneName) throws SAXException
234    {
235        Set<String> cTypes = _cTypeAssignmentHandler.getAvailableContentTypes(sitemap, zoneName, true);
236
237        XMLUtils.startElement(contentHandler, "available-content-types");
238
239        for (String cType : cTypes)
240        {
241            AttributesImpl attrs = new AttributesImpl();
242            attrs.addCDATAAttribute("id", cType);
243            XMLUtils.createElement(contentHandler, "content-type", attrs);
244        }
245
246        XMLUtils.endElement(contentHandler, "available-content-types");
247    }
248
249    /**
250     * Sax zone items
251     * @param zoneItems The zone items to sax
252     * @throws SAXException if an error occurs while saxing
253     * @throws IOException if an I/O exception occurs
254     * @throws ProcessingException if an error occurs
255     */
256    private void _saxZoneItems(AmetysObjectIterable< ? extends ZoneItem> zoneItems) throws SAXException, IOException, ProcessingException
257    {
258        if (zoneItems == null)
259        {
260            return;
261        }
262
263        Request request = ObjectModelHelper.getRequest(objectModel);
264
265        for (ZoneItem zoneItem : zoneItems)
266        {
267            _saxZoneItem(request, zoneItem);
268        }
269    }
270    
271    private void _saxZoneItem(Request request, ZoneItem zoneItem) throws SAXException
272    {
273        String id = zoneItem.getId();
274        ZoneType type = zoneItem.getType();
275
276        request.setAttribute(WebConstants.REQUEST_ATTR_ZONEITEM, zoneItem);
277
278        AttributesImpl zoneItemAttrs = new AttributesImpl();
279        zoneItemAttrs.addCDATAAttribute("id", id);
280
281        Object zoneItemObject = null;
282
283        ContentHandler handler = contentHandler;
284
285        XMLUtils.startElement(handler, "zoneItem", zoneItemAttrs);
286
287        XMLUtils.startElement(handler, "information");
288        XMLUtils.createElement(handler, "type", type.toString());
289
290        if (type == ZoneType.CONTENT)
291        {
292            if (getLogger().isDebugEnabled())
293            {
294                Content content = zoneItem.getContent();
295                getLogger().debug("Processing content " + content.getId() + " / " + content.getPath());
296            }
297            
298            zoneItemObject = _saxContentZoneItem(zoneItem, handler, request);
299        }
300        else if (type == ZoneType.SERVICE)
301        {
302            if (getLogger().isDebugEnabled())
303            {
304                getLogger().debug("Processing service " + zoneItem.getServiceId());
305            }
306            
307            zoneItemObject = _saxServiceZoneItem(zoneItem, handler);
308        }
309
310        XMLUtils.endElement(handler, "information");
311
312        if (zoneItemObject instanceof Exception ex)
313        {
314            Throwable e = _obfuscate(ex);
315            if (e instanceof ObfuscatedException obfuscatedException)
316            {
317                obfuscatedException.reveal();
318                getLogger().error("Unable to display zone item", ex);
319                obfuscatedException.obfuscate();
320            }
321            else
322            {
323                getLogger().error("Unable to display zone item", ex);
324            }
325
326            
327            _saxError(handler, ex);
328        }
329        else if (zoneItemObject instanceof Service service)
330        {
331            _saxHTMLTitle(_i18nUtils.translate(service.getLabel()));
332        }
333        else if (zoneItemObject instanceof Content content)
334        {
335            _saxHTMLTitle(content.getTitle());
336        }
337
338        XMLUtils.endElement(handler, "zoneItem");
339
340        // Empty content request attributes
341        request.setAttribute(Content.class.getName(), null);
342        // Empty zone item request attribute
343        request.setAttribute(WebConstants.REQUEST_ATTR_ZONEITEM, null);
344    }
345    
346    private void _saxHTMLTitle(String title) throws SAXException
347    {
348        XMLUtils.startElement(contentHandler, "html");
349        XMLUtils.startElement(contentHandler, "head");
350        XMLUtils.createElement(contentHandler, "title", title);
351        XMLUtils.endElement(contentHandler, "head");
352        XMLUtils.startElement(contentHandler, "body");
353        AttributesImpl attrs = new AttributesImpl();
354        attrs.addCDATAAttribute("class", "ametys-richtext-title-1");
355        XMLUtils.createElement(contentHandler, "h1", attrs, title);
356        XMLUtils.createElement(contentHandler, "p", _i18nUtils.translate(new I18nizableText("plugin.web", "PLUGINS_WEB_TOOL_SITEMAP_SITEMAPPAGETOOL_USE_PREVIEW")));
357        XMLUtils.endElement(contentHandler, "body");
358        XMLUtils.endElement(contentHandler, "html");
359    }
360    
361    private Object _saxContentZoneItem(ZoneItem zoneItem, ContentHandler handler, Request request) throws SAXException
362    {
363        try
364        {
365            Content content = zoneItem.getContent();
366            String viewName = StringUtils.defaultString(zoneItem.getViewName(), "main");
367
368            XMLUtils.createElement(handler, "contentId", content.getId());
369            XMLUtils.createElement(handler, "contentName", content.getName());
370            XMLUtils.createElement(handler, "metadataSetName", viewName);
371            if (content instanceof SharedContent)
372            {
373                XMLUtils.createElement(handler, "sharedContent", "true");
374            }
375
376            String contentTypeId = _contentTypeHelper.getContentTypeIdForRendering(content);
377            
378            ContentTypeDescriptor contentType = _contentTypeExtPt.getExtension(contentTypeId);
379            if (contentType == null)
380            {
381                contentType = _dynamicCTDescriptorEP.getExtension(contentTypeId);
382            }
383            
384            if (contentType == null)
385            {
386                return new IllegalStateException("The content type '" + contentTypeId + "' is referenced but does not exist");
387            }
388            else
389            {
390                AttributesImpl contentTypeAttrs = new AttributesImpl();
391                contentTypeAttrs.addCDATAAttribute("id", contentType.getId());
392                XMLUtils.startElement(handler, "type-information", contentTypeAttrs);
393
394                contentType.getLabel().toSAX(handler, "label");
395                contentType.getDescription().toSAX(handler, "description");
396                
397                if (contentType.getIconGlyph() != null)
398                {
399                    XMLUtils.createElement(handler, "iconGlyph", contentType.getIconGlyph());
400                }
401                if (contentType.getIconDecorator() != null)
402                {
403                    XMLUtils.createElement(handler, "iconDecorator", contentType.getIconDecorator());
404                }
405                
406                if (contentType.getSmallIcon() != null)
407                {
408                    XMLUtils.createElement(handler, "smallIcon", contentType.getSmallIcon());
409                    XMLUtils.createElement(handler, "mediumIcon", contentType.getMediumIcon());
410                    XMLUtils.createElement(handler, "largeIcon", contentType.getLargeIcon());
411                }
412                
413                XMLUtils.startElement(handler, "css");
414                for (ScriptFile cssFile : contentType.getCSSFiles())
415                {
416                    _saxCSSFile(handler, cssFile);
417                }
418                XMLUtils.endElement(handler, "css");
419                
420                XMLUtils.endElement(handler, "type-information");
421
422                // FIXME use a context
423                request.setAttribute(Content.class.getName(), content);
424                request.setAttribute(GetContentAction.RESULT_CONTENTTYPE, contentTypeId);
425                
426                return content;
427            }
428        }
429        catch (AmetysRepositoryException e)
430        {
431            return new ProcessingException("Unable to get content property", e);
432        }
433    }
434    
435    private Object _saxServiceZoneItem(ZoneItem zoneItem, ContentHandler handler) throws SAXException
436    {
437        String serviceId = zoneItem.getServiceId();
438        Service service = _serviceExtPt.getExtension(serviceId);
439
440        if (service == null)
441        {
442            return new ProcessingException("Unable to get service for name '" + serviceId + "'");
443        }
444        else
445        {
446            AttributesImpl serviceAttrs = new AttributesImpl();
447            serviceAttrs.addCDATAAttribute("id", service.getId());
448            XMLUtils.startElement(handler, "type-information", serviceAttrs);
449
450            service.getLabel().toSAX(handler, "label");
451            service.getDescription().toSAX(handler, "description");
452            
453            if (service.getIconGlyph() != null)
454            {
455                XMLUtils.createElement(handler, "iconGlyph", service.getIconGlyph());
456            }
457            if (service.getIconDecorator() != null)
458            {
459                XMLUtils.createElement(handler, "iconDecorator", service.getIconDecorator());
460            }
461            if (service.getSmallIcon() != null)
462            {
463                XMLUtils.createElement(handler, "smallIcon", service.getSmallIcon());
464                XMLUtils.createElement(handler, "mediumIcon", service.getMediumIcon());
465            }
466            
467            XMLUtils.startElement(handler, "css");
468            for (ScriptFile cssFile : service.getCSSFiles())
469            {
470                _saxCSSFile(handler, cssFile);
471            }
472            XMLUtils.endElement(handler, "css");
473            
474            XMLUtils.endElement(handler, "type-information");
475            
476            return service;
477        }
478    }
479    
480    private void _saxCSSFile(ContentHandler handler, ScriptFile cssFile) throws SAXException
481    {
482        AttributesImpl fileAttrs = new AttributesImpl();
483        if (!cssFile.isLangSpecific())
484        {
485            String rtlMode = cssFile.getRtlMode();
486            if (rtlMode != null && !"all".equals(rtlMode))
487            {
488                fileAttrs.addCDATAAttribute("rtl", rtlMode);
489            }
490            
491            XMLUtils.createElement(handler, "file", fileAttrs, cssFile.getPath());
492        }
493        else
494        {
495            fileAttrs.addCDATAAttribute("lang", "true");
496            XMLUtils.startElement(handler, "file", fileAttrs);
497            
498            String defaultLang = cssFile.getDefaultLang();
499            Map<String, String> langPaths = cssFile.getLangPaths();
500            
501            for (Entry<String, String> langPath : langPaths.entrySet())
502            {
503                AttributesImpl langAttrs = new AttributesImpl();
504                
505                String codeLang = langPath.getKey();
506                langAttrs.addCDATAAttribute("code", codeLang);
507                if (codeLang.equals(defaultLang))
508                {
509                    langAttrs.addCDATAAttribute("default", "true");
510                }
511                
512                XMLUtils.createElement(handler, "lang", langAttrs, langPath.getValue());
513            }
514
515            XMLUtils.endElement(handler, "file");
516        }
517    }
518
519    /**
520     * Get the template definition for a page
521     * @param sitemap The page. Cannot be null.
522     * @return The template definition. Null if the page is not a container or if the template is not declared.
523     */
524    private SkinTemplate _getTemplateDefinition(Sitemap sitemap)
525    {
526        Site site = sitemap.getSite();
527        String skinId = site.getSkinId();
528        String templateName = sitemap.getTemplate();
529        try
530        {
531            Skin skinDef = _skinsManager.getSkin(skinId);
532            return skinDef.getTemplate(templateName);
533        }
534        catch (IllegalStateException e)
535        {
536            getLogger().error("Cannot get template definition for sitemap '" + sitemap.getId() + "' using template '" + templateName + "' in skin '" + skinId + "'");
537            return null;
538        }
539    }
540
541    private void _saxError (ContentHandler handler, Throwable e) throws SAXException
542    {
543        XMLUtils.startElement(handler, "zone-item-error");
544        
545        XMLUtils.createElement(handler, "exception-message", e != null ? StringUtils.defaultString(e.getMessage()) : "");
546        XMLUtils.createElement(handler, "exception-stack-trace", StringUtils.defaultString(ExceptionUtils.getFullStackTrace(e)));
547
548        XMLUtils.endElement(handler, "zone-item-error");
549    }
550    
551    private Throwable _obfuscate(Throwable throwable)
552    {
553        if (!DEVMODE.PRODUCTION.equals(DevMode.getDeveloperMode()) // Do not read dev mode from request to prevent override from request param
554                || _rightManager.currentUserHasRight("Runtime_Rights_Admin_Access", "/admin").equals(RightResult.RIGHT_ALLOW))
555        {
556            return throwable;
557        }
558        else
559        {
560            return ObfuscatedException.obfuscate(throwable);
561        }
562        
563    }
564
565}