001/*
002 *  Copyright 2013 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 */
016
017package org.ametys.web.cache.monitoring.ui;
018
019import java.io.IOException;
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.Collection;
023import java.util.List;
024import java.util.Map;
025
026import org.apache.avalon.framework.service.ServiceException;
027import org.apache.avalon.framework.service.ServiceManager;
028import org.apache.cocoon.ProcessingException;
029import org.apache.cocoon.generation.ServiceableGenerator;
030import org.apache.cocoon.xml.AttributesImpl;
031import org.apache.cocoon.xml.XMLUtils;
032import org.apache.commons.lang.StringUtils;
033import org.apache.commons.lang.builder.EqualsBuilder;
034import org.apache.commons.lang.builder.HashCodeBuilder;
035import org.xml.sax.SAXException;
036
037import org.ametys.cms.repository.Content;
038import org.ametys.core.util.DateUtils;
039import org.ametys.plugins.repository.AmetysObjectIterable;
040import org.ametys.plugins.repository.AmetysRepositoryException;
041import org.ametys.plugins.repository.UnknownAmetysObjectException;
042import org.ametys.web.cache.monitoring.process.statistics.ResourceStatisticsComponent;
043import org.ametys.web.inputdata.InputDataExtensionPoint;
044import org.ametys.web.repository.page.Page;
045import org.ametys.web.repository.page.Zone;
046import org.ametys.web.repository.page.ZoneItem;
047import org.ametys.web.repository.page.ZoneItem.ZoneType;
048import org.ametys.web.repository.site.Site;
049import org.ametys.web.repository.site.SiteManager;
050import org.ametys.web.repository.sitemap.Sitemap;
051
052import com.google.common.collect.HashMultimap;
053import com.google.common.collect.LinkedListMultimap;
054import com.google.common.collect.Multimap;
055
056/**
057 * Cache stats generator for related to the back-office (pages and page elements)
058 */
059public class PageElementCacheStatsGenerator extends ServiceableGenerator
060{
061    /** Resource statistics component */
062    protected ResourceStatisticsComponent _resourceStatisticsCmp;
063    
064    /** Input Data extension point */
065    protected InputDataExtensionPoint _inputDataExt;
066    
067    /** Ametys resolver */
068    protected SiteManager _siteManager;
069    
070    /** Map linking page Id to page stats entries */
071    protected Multimap<String, PageStatsEntry> _pageIdMap;
072    
073    /** Map of stats entries */
074    protected Multimap<PageStatsEntry, PageElementStatsEntry> _statsMap;
075    
076    @Override
077    public void service(ServiceManager sm) throws ServiceException
078    {
079        super.service(sm);
080        _resourceStatisticsCmp = (ResourceStatisticsComponent) sm.lookup(ResourceStatisticsComponent.ROLE);
081        _inputDataExt = (InputDataExtensionPoint) sm.lookup(InputDataExtensionPoint.ROLE);
082        _siteManager = (SiteManager) sm.lookup(SiteManager.ROLE);
083        
084    }
085    
086    @Override
087    public void recycle()
088    {
089        _statsMap = null;
090        _pageIdMap = null;
091        
092        super.recycle();
093    }
094    
095    @Override
096    public void generate() throws IOException, SAXException, ProcessingException
097    {
098        String siteName = parameters.getParameter("siteName", "");
099        Site site = null;
100        
101        try
102        {
103            site = _siteManager.getSite(siteName);
104        }
105        catch (UnknownAmetysObjectException e)
106        {
107            String message = "The site '" + siteName + "' does not exist.";
108            getLogger().error(message, e);
109            throw new ProcessingException(message, e);
110        }
111        
112        _pageIdMap = HashMultimap.create(); // does not allow duplicate key-value entries.
113        _statsMap = LinkedListMultimap.create(); // can contain non-distinct key-value pairs.
114        
115        _initializePECacheStats();
116        
117        List<String> contexts = _getContextsFilter(parameters.getParameter("contexts", null));
118        
119        long start = 0;
120        if (getLogger().isDebugEnabled())
121        {
122            start = System.currentTimeMillis();
123            
124            getLogger().debug("Starting to SAX back-office cache statistics.");
125        }
126        
127        contentHandler.startDocument();
128        XMLUtils.startElement(contentHandler, "stats");
129        
130        // sax info for the given site.
131        _saxStats(site, contexts);
132        
133        XMLUtils.endElement(contentHandler, "stats");
134        contentHandler.endDocument();
135        
136        if (getLogger().isDebugEnabled())
137        {
138            long end = System.currentTimeMillis();
139            String duration = DateUtils.formatDuration(end - start);
140            getLogger().debug(String.format("The SAX process of the back-office cache statistics took %s", duration));
141        }
142    }
143    
144    /**
145     * Initialize statistics by retrieving data from the monitoring database.
146     * @throws ProcessingException if an error occurs
147     */
148    private void _initializePECacheStats() throws ProcessingException
149    {
150        try
151        {
152            List<Map<String, Object>> rawCacheStats = _resourceStatisticsCmp.getPageElementCacheStats();
153            _processCacheStatsResultSet(rawCacheStats);
154        }
155        catch (Exception e)
156        {
157            String msg = "Error while retrieving page element cache stats";
158            getLogger().error(msg);
159            throw new ProcessingException(msg, e);
160        }
161    }
162    
163    private void _processCacheStatsResultSet(List<Map<String, Object>> rawCacheStats)
164    {
165        for (Map<String, Object> rawCacheStat : rawCacheStats)
166        {
167            PageStatsEntry pageStatsEntry = new PageStatsEntry(rawCacheStat);
168            PageElementStatsEntry pageElementStatsEntry = new PageElementStatsEntry(rawCacheStat);
169            
170            _pageIdMap.put(pageStatsEntry._pageId, pageStatsEntry);
171            _statsMap.put(pageStatsEntry, pageElementStatsEntry);
172        }
173    }
174    
175    /**
176     * Analyse the 'contexts' parameter to filter the data to sax depending on requested contexts.
177     * A context is a couple as follows : 'renderingContext' - 'workspaceJcr'
178     * @param contexts The contexts
179     * @return the list of filters for the given contexts
180     */
181    private List<String> _getContextsFilter(String contexts)
182    {
183        if (StringUtils.isEmpty(contexts))
184        {
185            return null;
186        }
187        
188        return Arrays.asList(StringUtils.split(contexts, '#'));
189    }
190    
191    private void _saxStats(Site site, List<String> contexts) throws SAXException
192    {
193        AttributesImpl attrs = new AttributesImpl();
194        attrs.addCDATAAttribute("id", site.getId());
195        attrs.addCDATAAttribute("name", site.getName());
196        XMLUtils.startElement(contentHandler, "site", attrs);
197        
198        for (Sitemap sitemap : site.getSitemaps())
199        {
200            _saxStats(sitemap, contexts);
201        }
202        
203        XMLUtils.endElement(contentHandler, "site");
204    }
205    
206    private void _saxStats(Sitemap sitemap, List<String> contexts) throws SAXException
207    {
208        AttributesImpl attrs = new AttributesImpl();
209        attrs.addCDATAAttribute("id", sitemap.getId());
210        attrs.addCDATAAttribute("name", sitemap.getName());
211        XMLUtils.startElement(contentHandler, "sitemap", attrs);
212        
213        for (Page page : sitemap.getChildrenPages())
214        {
215            _saxPageStats(page, contexts);
216        }
217        
218        XMLUtils.endElement(contentHandler, "sitemap");
219    }
220    
221    private void _saxPageStats(Page page, List<String> contexts) throws SAXException
222    {
223        String pageId = page.getId();
224        
225        // SAX'ing page info
226        AttributesImpl attrs = new AttributesImpl();
227        attrs.addCDATAAttribute("id", pageId);
228        attrs.addCDATAAttribute("title", page.getTitle());
229        attrs.addCDATAAttribute("name", page.getName());
230        attrs.addCDATAAttribute("path", page.getPathInSitemap());
231        XMLUtils.startElement(contentHandler, "page", attrs);
232        
233        // SAX stats by contexts
234        if (_pageIdMap.containsKey(pageId))
235        {
236            List<Zone> zones = new ArrayList<>();
237            for (Zone zone : page.getZones())
238            {
239                zones.add(zone);
240            }
241            
242            _saxPageStatsContexts(page, zones, contexts);
243        }
244        
245        for (Page child : page.getChildrenPages())
246        {
247            _saxPageStats(child, contexts);
248        }
249        
250        XMLUtils.endElement(contentHandler, "page");
251    }
252    
253    private void _saxPageStatsContexts(Page page, List<Zone> zones, List<String> contexts) throws SAXException
254    {
255        Collection<PageStatsEntry> pageStatsEntries = _pageIdMap.get(page.getId());
256        
257        for (PageStatsEntry pageStats : pageStatsEntries)
258        {
259            // Filter out contexts that are not in the 'contexts' list.
260            // null lists implies that all contexts are allowed
261            if (contexts == null || contexts.contains(pageStats.getContext()))
262            {
263                _saxPageStatsContext(pageStats, zones);
264            }
265        }
266    }
267    
268    private void _saxPageStatsContext(PageStatsEntry pageStats, List<Zone> zones) throws SAXException
269    {
270        AttributesImpl attrs = new AttributesImpl();
271        attrs.addCDATAAttribute("renderingContext", pageStats._renderingContext);
272        attrs.addCDATAAttribute("workspaceJCR", pageStats._workspaceJCR);
273        attrs.addCDATAAttribute("cacheable", String.valueOf(pageStats._cacheable));
274        XMLUtils.startElement(contentHandler, "context", attrs);
275        
276        XMLUtils.createElement(contentHandler, "hits", String.valueOf(pageStats._hits));
277        
278        Collection<PageElementStatsEntry> pageElementStats = _statsMap.get(pageStats);
279        
280        // Iterate on existing zones
281        for (Zone zone : zones)
282        {
283            _saxZoneStats(zone, pageElementStats);
284        }
285        
286        // Iterate on input data
287        _saxInputDataStats(pageElementStats);
288        
289        XMLUtils.endElement(contentHandler, "context");
290    }
291    
292    private void _saxZoneStats(Zone zone, Collection<PageElementStatsEntry> pageElementStats) throws SAXException
293    {
294        AttributesImpl attrs = new AttributesImpl();
295        attrs.addCDATAAttribute("id", zone.getId());
296        attrs.addCDATAAttribute("name", zone.getName());
297        XMLUtils.startElement(contentHandler, "zone", attrs);
298        
299        AmetysObjectIterable<? extends ZoneItem> zoneItems = zone.getZoneItems();
300        int index = 0;
301        for (ZoneItem zoneItem : zoneItems)
302        {
303            index++;
304            PageElementStatsEntry entry = _findPageElementStatsEntry(pageElementStats, zoneItem.getId());
305            if (entry != null)
306            {
307                _saxZoneItemStats(zoneItem, entry, index);
308            }
309        }
310        
311        XMLUtils.endElement(contentHandler, "zone");
312    }
313    
314    
315    private void _saxZoneItemStats(ZoneItem zoneItem, PageElementStatsEntry entry, int index) throws SAXException
316    {
317        AttributesImpl attrs = new AttributesImpl();
318        attrs.addCDATAAttribute("id", entry._pageElementID);
319        attrs.addCDATAAttribute("index", String.valueOf(index));
320        attrs.addCDATAAttribute("type", zoneItem.getType().toString().toLowerCase());
321        attrs.addCDATAAttribute("cacheable", String.valueOf(entry._cacheable));
322        _addZoneItemSaxInfo(attrs, zoneItem);
323        XMLUtils.startElement(contentHandler, "zoneitem", attrs);
324        
325        XMLUtils.createElement(contentHandler, "hits", String.valueOf(entry._hits));
326        XMLUtils.createElement(contentHandler, "cacheHits", String.valueOf(entry._cacheHits));
327        
328        XMLUtils.endElement(contentHandler, "zoneitem");
329    }
330    
331    private void _addZoneItemSaxInfo(AttributesImpl attrs, ZoneItem zoneItem)
332    {
333        try
334        {
335            switch (zoneItem.getType())
336            {
337                case CONTENT:
338                    Content content = zoneItem.getContent();
339                    attrs.addCDATAAttribute("contentId", content.getId());
340                    attrs.addCDATAAttribute("contentType", StringUtils.join(content.getTypes()));
341                    break;
342                case SERVICE:
343                    attrs.addCDATAAttribute("serviceId", zoneItem.getServiceId());
344                    break;
345                default:
346                    throw new IllegalArgumentException("Illegal zone item type. Allowed values are : " + Arrays.asList(ZoneType.values()));
347            }
348        }
349        catch (AmetysRepositoryException e)
350        {
351            // Ignore
352        }
353    }
354    
355    private void _saxInputDataStats(Collection<PageElementStatsEntry> pageElementStats) throws SAXException
356    {
357        XMLUtils.startElement(contentHandler, "inputdata");
358        
359        for (String id : _inputDataExt.getExtensionsIds())
360        {
361            PageElementStatsEntry entry = _findPageElementStatsEntry(pageElementStats, id);
362            if (entry != null)
363            {
364                _saxInputDataStatsEntry(entry, id);
365            }
366        }
367        
368        XMLUtils.endElement(contentHandler, "inputdata");
369    }
370    
371    private void _saxInputDataStatsEntry(PageElementStatsEntry entry, String inputDataId) throws SAXException
372    {
373        AttributesImpl attrs = new AttributesImpl();
374        attrs.addCDATAAttribute("id", inputDataId);
375        attrs.addCDATAAttribute("title", inputDataId);
376        attrs.addCDATAAttribute("cacheable", String.valueOf(entry._cacheable));
377        XMLUtils.startElement(contentHandler, "inputdataitem", attrs);
378        
379        XMLUtils.createElement(contentHandler, "hits", String.valueOf(entry._hits));
380        XMLUtils.createElement(contentHandler, "cacheHits", String.valueOf(entry._cacheHits));
381        
382        XMLUtils.endElement(contentHandler, "inputdataitem");
383    }
384    
385    private PageElementStatsEntry _findPageElementStatsEntry(Collection<PageElementStatsEntry> pageElementStats, String id)
386    {
387        for (PageElementStatsEntry entry : pageElementStats)
388        {
389            if (id.equals(entry._pageElementID))
390            {
391                return entry;
392            }
393        }
394        return null;
395    }
396    
397    /**
398     * Object model representing an entry of stats for a Page
399     */
400    protected class PageStatsEntry
401    {
402        /** page id */
403        protected final String _pageId;
404        /** rendering context */
405        protected final String _renderingContext;
406        /** workspace JCR */
407        protected final String _workspaceJCR;
408        /** is cacheable */
409        protected final boolean _cacheable;
410        /** hits */
411        protected final int _hits;
412        
413        /**
414         * Ctor
415         * @param data map of raw data object.
416         */
417        protected PageStatsEntry(Map<String, Object> data)
418        {
419            _pageId = (String) data.get("PAGE_ID");
420            _renderingContext = (String) data.get("RENDERING_CONTEXT");
421            _workspaceJCR = (String) data.get("WORKSPACE_JCR");
422            _cacheable = (boolean) data.get("P_CACHEABLE");
423            _hits = (int) data.get("P_HITS");
424        }
425        
426        /**
427         * Returns the formatted context name
428         * @return context name
429         */
430        public String getContext()
431        {
432            return _renderingContext + '-' + _workspaceJCR;
433        }
434        
435        @Override
436        public boolean equals(Object obj)
437        {
438            if (obj == null)
439            {
440                return false;
441            }
442            if (obj == this)
443            {
444                return true;
445            }
446            if (!(obj instanceof PageStatsEntry))
447            {
448                return false;
449            }
450            
451            PageStatsEntry that = (PageStatsEntry) obj;
452            
453            EqualsBuilder eb = new EqualsBuilder();
454            eb.append(_pageId, that._pageId);
455            eb.append(_renderingContext, that._renderingContext);
456            eb.append(_workspaceJCR, that._workspaceJCR);
457            return eb.isEquals();
458        }
459        
460        @Override
461        public int hashCode()
462        {
463            HashCodeBuilder hsb = new HashCodeBuilder();
464            hsb.append(_pageId).append(_renderingContext).append(_workspaceJCR);
465            return hsb.toHashCode();
466        }
467    }
468    
469    /**
470     * Object model representing an entry of stats for a PageElement
471     */
472    protected class PageElementStatsEntry
473    {
474        /** page element id */
475        protected final String _pageElementID;
476        /** rendering context */
477        protected final String _renderingContext;
478        /** workspace JCR */
479        protected final String _workspaceJCR;
480        /** is cacheable */
481        protected final boolean _cacheable;
482        /** hits */
483        protected final int _hits;
484        /** cache hits */
485        protected final int _cacheHits;
486        
487        /**
488         * Ctor
489         * @param data map of raw data object.
490         */
491        protected PageElementStatsEntry(Map<String, Object> data)
492        {
493            _pageElementID = (String) data.get("PAGE_ELEMENT_ID");
494            _renderingContext = (String) data.get("RENDERING_CONTEXT");
495            _workspaceJCR = (String) data.get("WORKSPACE_JCR");
496            _cacheable = (boolean) data.get("PE_CACHEABLE");
497            _hits = (int) data.get("PE_HITS");
498            _cacheHits = (int) data.get("PE_CACHE_HITS");
499        }
500    }
501}