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