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