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.sql.Clob;
021import java.sql.SQLException;
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.Collection;
025import java.util.Collections;
026import java.util.HashMap;
027import java.util.Iterator;
028import java.util.LinkedList;
029import java.util.List;
030import java.util.Map;
031import java.util.regex.Matcher;
032
033import org.apache.avalon.framework.service.ServiceException;
034import org.apache.avalon.framework.service.ServiceManager;
035import org.apache.cocoon.ProcessingException;
036import org.apache.cocoon.generation.ServiceableGenerator;
037import org.apache.cocoon.xml.AttributesImpl;
038import org.apache.cocoon.xml.XMLUtils;
039import org.apache.commons.lang3.StringUtils;
040import org.xml.sax.SAXException;
041
042import org.ametys.core.util.DateUtils;
043import org.ametys.plugins.repository.UnknownAmetysObjectException;
044import org.ametys.web.cache.monitoring.process.statistics.ResourceStatisticsComponent;
045import org.ametys.web.repository.site.Site;
046import org.ametys.web.repository.site.SiteManager;
047import org.ametys.web.site.SitesGenerator;
048
049import com.google.common.collect.ArrayListMultimap;
050import com.google.common.collect.HashMultimap;
051import com.google.common.collect.Multimap;
052
053
054/**
055 * Cache stats generator grouping data collected across each server cache (http
056 * server/front/back)
057 */
058public class ServersCacheStatsGenerator extends ServiceableGenerator
059{
060    /** List of paths used to during the sanitize process of the server path */
061    protected static final String[] _SPECIAL_PATH_PREFIXS = new String[]{"/skins/", "/plugins/", "/kernel/", "/_external/"};
062    
063    /** Resource statistics component */
064    protected ResourceStatisticsComponent _resourceStatisticsCmp;
065    
066    /** Ametys resolver */
067    protected SiteManager _siteManager;
068    
069    /**
070     * This multimap associates site names to a list of prefix. This is needed
071     * to sanitize the server path.
072     */
073    protected Multimap<String, String> _pathSanitizer;
074
075    /**
076     * Multimaps representing the all the paths to the resources in a recursive
077     * way. The map key's are the site names.
078     */
079    protected Map<String, Multimap<String, String>> _pathMaps;
080
081    /**
082     * Multimap containing httpserver stats entries classified by site name and path
083     */
084    protected Map<String, Map<String, FrontFromHTTPServerStatsEntry>> _fromHTTPServerStats;
085
086    /**
087     * Multimap containing (only) front stats entries classified by site name
088     * and path
089     */
090    protected Map<String, Map<String, FrontFromFrontStatsEntry>> _fromFrontOnlyStats;
091
092    /** Map containing back stats entries classified by path */
093    protected Map<String, BackStatsEntry> _backStats;
094
095    @Override
096    public void service(ServiceManager sm) throws ServiceException
097    {
098        super.service(sm);
099        _resourceStatisticsCmp = (ResourceStatisticsComponent) sm.lookup(ResourceStatisticsComponent.ROLE);
100        _siteManager = (SiteManager) sm.lookup(SiteManager.ROLE);
101    }
102
103    @Override
104    public void generate() throws IOException, SAXException, ProcessingException
105    {
106        String siteName = parameters.getParameter("siteName", "");
107        Site site = null;
108
109        try
110        {
111            site = _siteManager.getSite(siteName);
112        }
113        catch (UnknownAmetysObjectException e)
114        {
115            String message = "The site '" + siteName + "' does not exist.";
116            getLogger().error(message, e);
117            throw new ProcessingException(message, e);
118        }
119
120        // Retrieve raw info from DB.
121        List<RawStatsEntry> rawStats = _getStatsFromDb(siteName);
122
123        _pathMaps = new HashMap<>();
124        _fromHTTPServerStats = new HashMap<>();
125        _fromFrontOnlyStats = new HashMap<>();
126        _backStats = new HashMap<>();
127        _pathSanitizer = ArrayListMultimap.create();
128
129        // Populate and organize stats maps.
130        _initializeStats(rawStats);
131
132        long start = 0;
133        if (getLogger().isDebugEnabled())
134        {
135            start = System.currentTimeMillis();
136
137            getLogger().debug("Starting to SAX cache statistics for HTTPServer and the Front-office");
138        }
139
140        // Start SAX'ing
141        contentHandler.startDocument();
142        XMLUtils.startElement(contentHandler, "stats");
143
144        // sax info for the given site.
145        _saxStatsBySite(site);
146
147        // sax orphan (resource from front without site info).
148        _saxStatsOrphanEntries();
149
150        XMLUtils.endElement(contentHandler, "stats");
151        contentHandler.endDocument();
152
153        if (getLogger().isDebugEnabled())
154        {
155            long end = System.currentTimeMillis();
156            String duration = DateUtils.formatDuration(end - start);
157            getLogger().debug(String.format("The SAX process of the HTTPServer and Front-office cache statistics took %s", duration));
158        }
159    }
160
161    @Override
162    public void recycle()
163    {
164        _pathMaps = null;
165        _fromHTTPServerStats = null;
166        _fromFrontOnlyStats = null;
167        _backStats = null;
168        _pathSanitizer = null;
169
170        super.recycle();
171    }
172
173    /**
174     * Retrieves raw statistics information
175     * @param siteName The site name
176     * @return a list of {@link RawStatsEntry}
177     * @throws ProcessingException if an error occurs
178     */
179    private List<RawStatsEntry> _getStatsFromDb(String siteName) throws ProcessingException
180    {
181        try
182        {
183            List<Map<String, Object>> rawCacheStats = _resourceStatisticsCmp.getServerCacheStats(siteName);
184            return _processCacheStatsResultSet(rawCacheStats);
185        }
186        catch (Exception e)
187        {
188            String msg = "Error while retrieving cache stats";
189            getLogger().error(msg);
190            throw new ProcessingException(msg, e);
191        }
192    }
193    
194    private List<RawStatsEntry> _processCacheStatsResultSet(List<Map<String, Object>> rawCacheStats) throws SQLException
195    {
196        List<RawStatsEntry> entries = new ArrayList<>();
197        
198        for (Map<String, Object> rawCacheStat : rawCacheStats)
199        {
200            entries.add(new RawStatsEntry(rawCacheStat));
201        }
202        
203        return entries;
204    }
205
206    /**
207     * Organize the different Map of stats
208     * @param rawStats the raw statistics
209     */
210    private void _initializeStats(List<RawStatsEntry> rawStats)
211    {
212        // Iterate over raw entries and do the logic to populate
213        // HTTPServer/FrontOnly/Back stats map.
214        for (RawStatsEntry rawStatsEntry : rawStats)
215        {
216            if (rawStatsEntry.hasHTTPServerInfo())
217            {
218                FrontFromHTTPServerStatsEntry httpServerEntry = new FrontFromHTTPServerStatsEntry(rawStatsEntry._data);
219                _addToHTTPServerStats(httpServerEntry);
220            }
221            else
222            {
223                FrontFromFrontStatsEntry frontEntry = new FrontFromFrontStatsEntry(rawStatsEntry._data);
224                _addToFrontOnlyStats(frontEntry);
225            }
226
227            if (rawStatsEntry.hasBackInfo())
228            {
229                BackStatsEntry backEntry = new BackStatsEntry(rawStatsEntry._data);
230
231                // remove .html in page name to avoid issues when a page as children pages.
232                String backPath = StringUtils.removeEnd(backEntry._pagePath, ".html");
233                _backStats.put(backPath, backEntry);
234            }
235        }
236    }
237
238    private void _addToHTTPServerStats(FrontFromHTTPServerStatsEntry httpServerEntry)
239    {
240        String siteName = httpServerEntry._serverSite;
241
242        Map<String, FrontFromHTTPServerStatsEntry> siteMap = _fromHTTPServerStats.get(siteName);
243        if (siteMap == null)
244        {
245            siteMap = new HashMap<>();
246            _fromHTTPServerStats.put(siteName, siteMap);
247        }
248
249        // The path must be sanitized to be of the following form:
250        // /site/lang/page-path or /skin/... or /plugins/... etc...
251        String sanitizedPath = httpServerEntry.getSanitizedPath();
252
253        // remove .html in page name to avoid issues when a page as children pages.
254        sanitizedPath = StringUtils.removeEnd(sanitizedPath, ".html");
255
256        FrontFromHTTPServerStatsEntry previousHTTPServerEntry = siteMap.put(sanitizedPath, httpServerEntry);
257
258        // Must merge the info if a previous entry was already in the map (this can happens when a site as several url aliases)
259        if (previousHTTPServerEntry != null)
260        {
261            httpServerEntry.merge(previousHTTPServerEntry);
262        }
263
264        // Also register this new path into the path maps.
265        _registerPath(httpServerEntry._serverSite, sanitizedPath);
266    }
267
268    private void _addToFrontOnlyStats(FrontFromFrontStatsEntry frontEntry)
269    {
270        String siteName = frontEntry._frontSite;
271
272        Map<String, FrontFromFrontStatsEntry> siteMap = _fromFrontOnlyStats.get(siteName);
273        if (siteMap == null)
274        {
275            siteMap = new HashMap<>();
276            _fromFrontOnlyStats.put(siteName, siteMap);
277        }
278
279        // remove .html in page name to avoid issues when a page as children pages.
280        String frontPath = StringUtils.removeEnd(frontEntry._frontPath, ".html");
281        siteMap.put(frontPath, frontEntry);
282
283        // Also register this new path into the path maps.
284        _registerPath(siteName, frontPath);
285    }
286
287    private void _registerPath(String serverSite, String path)
288    {
289        Multimap<String, String> pathMap = _pathMaps.get(serverSite);
290        if (pathMap == null)
291        {
292            pathMap = HashMultimap.create(); // does not allow duplicate key-value entries.
293            _pathMaps.put(serverSite, pathMap);
294        }
295
296        String[] splitPath = StringUtils.split(path, '/');
297        _internalRegisterPath(pathMap, StringUtils.EMPTY, new LinkedList<>(Arrays.asList(splitPath)));
298    }
299
300    /**
301     * Populate the path map using recursion.
302     * @param pathMap Multimap representing the paths to the resources within a site.
303     * @param consumed the consumed part of the current path being registered.
304     * @param tail tail of the current path being registered. The tail will be consumed in the recursive nested calls of this internal function.
305     */
306    private void _internalRegisterPath(Multimap<String, String> pathMap, String consumed, LinkedList<String> tail)
307    {
308        if (!tail.isEmpty())
309        {
310            String head = tail.removeFirst();
311            pathMap.put(consumed, head);
312            _internalRegisterPath(pathMap, consumed + '/' + head, tail);
313        }
314    }
315
316    private void _saxStatsBySite(Site site) throws SAXException
317    {
318        String siteName = site.getName();
319
320        Multimap<String, String> pathMap = _pathMaps.get(siteName);
321        if (pathMap == null)
322        {
323            return;
324        }
325
326        Collection<String> entryPoints = pathMap.get(StringUtils.EMPTY);
327        if (entryPoints.isEmpty())
328        {
329            return;
330        }
331
332        Map<String, FrontFromHTTPServerStatsEntry> httpServerStats = _fromHTTPServerStats.get(siteName);
333        if (httpServerStats == null)
334        {
335            httpServerStats = Collections.EMPTY_MAP;
336        }
337
338        Map<String, FrontFromFrontStatsEntry> frontOnlyStats = _fromFrontOnlyStats.get(siteName);
339        if (frontOnlyStats == null)
340        {
341            frontOnlyStats = Collections.EMPTY_MAP;
342        }
343
344        AttributesImpl attrs = new AttributesImpl();
345        attrs.addCDATAAttribute("name", siteName);
346        attrs.addCDATAAttribute("id", site.getId());
347        XMLUtils.startElement(contentHandler, "site", attrs);
348
349        for (String name : entryPoints)
350        {
351            _saxStatsEntry(name, '/' + name, pathMap, httpServerStats, frontOnlyStats);
352        }
353
354        XMLUtils.endElement(contentHandler, "site");
355    }
356
357    private void _saxStatsEntry(String name, String path, Multimap<String, String> pathMap, Map<String, FrontFromHTTPServerStatsEntry> httpServerEntries, Map<String, FrontFromFrontStatsEntry> frontEntries) throws SAXException
358    {
359        FrontFromHTTPServerStatsEntry httpServerEntry = httpServerEntries.get(path);
360        FrontFromFrontStatsEntry frontEntry = frontEntries.get(path);
361        BackStatsEntry backEntry = _backStats.get(path);
362
363        AttributesImpl attrs = new AttributesImpl();
364
365        String type = "folder";
366        if (frontEntry != null)
367        {
368            type = frontEntry._frontPath.endsWith(".html") ? "page" : "asset";
369        }
370        else if (httpServerEntry != null)
371        {
372            if (httpServerEntry.hasFrontInfo())
373            {
374                type = httpServerEntry._frontPath.endsWith(".html") ? "page" : "asset";
375            }
376            else
377            {
378                type = httpServerEntry._serverPath.endsWith(".html") ? "page" : "asset";
379            }
380        }
381
382        Boolean cacheable = null;
383        if (httpServerEntry != null)
384        {
385            // No front info means implicit cache hit on the httpserver cache.
386            cacheable = httpServerEntry.hasFrontInfo() ? httpServerEntry._frontCacheable : true;
387        }
388        else if (frontEntry != null)
389        {
390            cacheable = frontEntry._frontCacheable;
391        }
392
393        attrs.addCDATAAttribute("type", type);
394        attrs.addCDATAAttribute("name", "page".equals(type) ? name + ".html" : name);
395        attrs.addCDATAAttribute("path", path);
396        __addAttrIfNotNull(attrs, "cacheable", cacheable);
397        XMLUtils.startElement(contentHandler, "resource", attrs);
398
399        _saxStatsHTTPServerEntry(httpServerEntry);
400        _saxStatsFrontEntry(frontEntry);
401        _saxStatsBackEntry(backEntry);
402
403        // Recursive call on children
404        for (String childName : pathMap.get(path))
405        {
406            _saxStatsEntry(childName, path + '/' + childName, pathMap, httpServerEntries, frontEntries);
407        }
408
409        XMLUtils.endElement(contentHandler, "resource");
410    }
411
412    private void _saxStatsHTTPServerEntry(FrontFromHTTPServerStatsEntry httpServerEntry) throws SAXException
413    {
414        if (httpServerEntry == null)
415        {
416            return;
417        }
418
419        AttributesImpl attrs = new AttributesImpl();
420        attrs.addCDATAAttribute("type", "httpserver");
421        __addAttrIfNotNull(attrs, "httpServerSite", httpServerEntry._serverSite);
422        __addAttrIfNotNull(attrs, "httpServerPath", httpServerEntry._serverPath);
423
424        // Add front info if available
425        if (httpServerEntry.hasFrontInfo())
426        {
427            __addAttrIfNotNull(attrs, "frontSite", httpServerEntry._frontSite);
428            attrs.addCDATAAttribute("frontPath", httpServerEntry._frontPath);
429            attrs.addCDATAAttribute("frontCacheable", String.valueOf(httpServerEntry._frontCacheable));
430        }
431
432        XMLUtils.startElement(contentHandler, "entry", attrs);
433
434        XMLUtils.createElement(contentHandler, "httpServerHits", String.valueOf(httpServerEntry._serverHits));
435        XMLUtils.createElement(contentHandler, "httpServerCacheHits", String.valueOf(httpServerEntry._serverCacheHits));
436
437        // Add front info if available
438        if (httpServerEntry.hasFrontInfo())
439        {
440            XMLUtils.createElement(contentHandler, "frontHits", String.valueOf(httpServerEntry._frontHits));
441            XMLUtils.createElement(contentHandler, "frontCacheHits1", String.valueOf(httpServerEntry._frontCacheHits1));
442            XMLUtils.createElement(contentHandler, "frontCacheHits2", String.valueOf(httpServerEntry._frontCacheHits2));
443        }
444
445        XMLUtils.endElement(contentHandler, "entry");
446    }
447
448    private void _saxStatsFrontEntry(FrontFromFrontStatsEntry frontEntry) throws SAXException
449    {
450        if (frontEntry == null)
451        {
452            return;
453        }
454
455        AttributesImpl attrs = new AttributesImpl();
456        attrs.addCDATAAttribute("type", "front");
457        __addAttrIfNotNull(attrs, "site", frontEntry._frontSite);
458        attrs.addCDATAAttribute("path", frontEntry._frontPath);
459        attrs.addCDATAAttribute("cacheable", String.valueOf(frontEntry._frontCacheable));
460        XMLUtils.startElement(contentHandler, "entry", attrs);
461
462        XMLUtils.createElement(contentHandler, "hits", String.valueOf(frontEntry._frontHits));
463        XMLUtils.createElement(contentHandler, "cacheHits1", String.valueOf(frontEntry._frontCacheHits1));
464        XMLUtils.createElement(contentHandler, "cacheHits2", String.valueOf(frontEntry._frontCacheHits2));
465
466        XMLUtils.endElement(contentHandler, "entry");
467    }
468
469    private void _saxStatsBackEntry(BackStatsEntry backEntry) throws SAXException
470    {
471        if (backEntry == null)
472        {
473            return;
474        }
475
476        AttributesImpl attrs = new AttributesImpl();
477        attrs.addCDATAAttribute("type", "back");
478        attrs.addCDATAAttribute("id", backEntry._pageId);
479        attrs.addCDATAAttribute("path", backEntry._pagePath);
480        attrs.addCDATAAttribute("cacheable", String.valueOf(backEntry._cacheable));
481        XMLUtils.startElement(contentHandler, "entry", attrs);
482
483        XMLUtils.createElement(contentHandler, "hits", String.valueOf(backEntry._hits));
484
485        XMLUtils.endElement(contentHandler, "entry");
486    }
487
488    private void _saxStatsOrphanEntries() throws SAXException
489    {
490        Multimap<String, String> pathMap = _pathMaps.get("-");
491        if (pathMap == null || pathMap.isEmpty())
492        {
493            return;
494        }
495
496        Collection<String> entryPoints = pathMap.get(StringUtils.EMPTY);
497        if (entryPoints.isEmpty())
498        {
499            return;
500        }
501
502        Map<String, FrontFromHTTPServerStatsEntry> httpServerStats = _fromHTTPServerStats.get("-");
503        if (httpServerStats == null)
504        {
505            httpServerStats = Collections.EMPTY_MAP;
506        }
507
508        Map<String, FrontFromFrontStatsEntry> frontOnlyStats = _fromFrontOnlyStats.get("-");
509        if (frontOnlyStats == null)
510        {
511            frontOnlyStats = Collections.EMPTY_MAP;
512        }
513
514        XMLUtils.startElement(contentHandler, "orphans");
515
516        for (String name : entryPoints)
517        {
518            _saxStatsEntry(name, '/' + name, pathMap, httpServerStats, frontOnlyStats);
519        }
520
521        XMLUtils.endElement(contentHandler, "orphans");
522    }
523
524    private void __addAttrIfNotNull(AttributesImpl attrs, String localName, String value)
525    {
526        if (StringUtils.isNotEmpty(value))
527        {
528            attrs.addCDATAAttribute(localName, value);
529        }
530    }
531
532    private void __addAttrIfNotNull(AttributesImpl attrs, String localName, Boolean value)
533    {
534        if (value != null)
535        {
536            attrs.addCDATAAttribute(localName, String.valueOf(value));
537        }
538    }
539
540    /**
541     * Object model representing a raw entry of stats retrieved through the DB.
542     */
543    protected class RawStatsEntry
544    {
545        /** The map of raw data objects */
546        protected final Map<String, Object> _data;
547
548        /**
549         * Constructor
550         * @param data map of raw data object.
551         * @throws SQLException if an error occurs while retrieving statistics in SQL database
552         */
553        protected RawStatsEntry(Map<String, Object> data) throws SQLException
554        {
555            // Mandatory pre-processing of data that contains a Path.
556            // It must be done while the underlying ResultSet object is not
557            // closed to correctly handle CLOB to String conversion.
558            for (String key : data.keySet())
559            {
560                if (key.contains("PATH"))
561                {
562                    data.put(key, __textToString(data.get(key)));
563                }
564            }
565            
566            _data = data;
567        }
568
569        /**
570         * Indicates if this instance has httpserver information.
571         * @return a boolean
572         */
573        protected boolean hasHTTPServerInfo()
574        {
575            String serverPath = (String) _data.get("SERVER_PATH");
576            return serverPath != null && !"-".equals(serverPath);
577        }
578
579        /**
580         * Indicates if this instance has back(-office) information.
581         * @return a boolean
582         */
583        protected boolean hasBackInfo()
584        {
585            String pageId = (String) _data.get("PAGE_ID");
586            return pageId != null && !"-".equals(pageId);
587        }
588        
589        /**
590         * Utility method
591         * Object representing a text to String
592         * @param object The object to convert
593         * @return the boolean
594         * @throws SQLException if an error occurs while retrieving statistics in SQL database
595         */
596        protected String __textToString(Object object) throws SQLException
597        {
598            if (object == null)
599            {
600                return null;
601            }
602            
603            if (object instanceof String)
604            {
605                return (String) object;
606            }
607            else if (object instanceof Clob)
608            {
609                Clob clob = (Clob) object;
610                return clob.getSubString(1, (int) clob.length());
611            }
612
613            throw new IllegalArgumentException("Not able to convert text object to String : unexpected object type.");
614        }
615    }
616
617    /**
618     * Object model representing an entry of stats for a front resource, coming
619     * from httpserver
620     */
621    protected class FrontFromHTTPServerStatsEntry extends FrontFromFrontStatsEntry
622    {
623        /** Server site name */
624        protected final String _serverSite;
625        /** Server path */
626        protected final String _serverPath;
627        /** server hits */
628        protected int _serverHits;
629        /** server cache hits */
630        protected int _serverCacheHits;
631
632        /**
633         * Ctor
634         * @param data map of raw data object.
635         */
636        protected FrontFromHTTPServerStatsEntry(Map<String, Object> data)
637        {
638            super(data);
639
640            _serverSite = (String) data.get("SERVER_SITE");
641            _serverPath = (String) data.get("SERVER_PATH");
642            _serverHits = (int) data.get("SERVER_HITS");
643            _serverCacheHits = (int) data.get("SERVER_CACHE_HITS");
644        }
645
646        /**
647         * Merge this instance of <code>FrontFromHTTPServerStatsEntry</code> with
648         * another instance of <code>FrontFromHTTPServerStatsEntry</code>
649         * @param that the stats entry to merhe with
650         */
651        protected void merge(FrontFromHTTPServerStatsEntry that)
652        {
653            if (that == null)
654            {
655                return;
656            }
657
658            _serverHits += that._serverHits;
659            _serverCacheHits += that._serverCacheHits;
660
661            if (that.hasFrontInfo())
662            {
663                _frontSite = that._frontSite;
664                _frontPath = that._frontPath;
665                _frontCacheable = that._frontCacheable;
666
667                _frontHits = hasFrontInfo() ? _frontHits + that._frontHits : that._frontHits;
668                _frontCacheHits1  = hasFrontInfo() ? _frontCacheHits1 + that._frontCacheHits1 : that._frontCacheHits1;
669                _frontCacheHits2  = hasFrontInfo() ? _frontCacheHits2 + that._frontCacheHits2 : that._frontCacheHits2;
670            }
671        }
672
673        /**
674         * Returns a sanitized path given the different path properties of this
675         * instance.
676         * @return The sanitized path
677         */
678        protected String getSanitizedPath()
679        {
680            if (hasFrontInfo())
681            {
682                return _frontPath;
683            }
684
685            // initialize allowed prefix path list for this site, given the site url aliases.
686            if (!_pathSanitizer.containsKey(_serverSite))
687            {
688                for (String url : _siteManager.getSite(_serverSite).getUrlAliases())
689                {
690                    Matcher matcher = SitesGenerator.URL_PATTERN.matcher(url);
691                    if (matcher.matches())
692                    {
693                        String path = matcher.group(5);
694                        if (StringUtils.isNotEmpty(path))
695                        {
696                            _pathSanitizer.put(_serverSite, path);
697                        }
698                    }
699                }
700
701                if (!_pathSanitizer.containsKey(_serverSite))
702                {
703                    _pathSanitizer.put(_serverSite, null);
704                }
705            }
706
707            // sanitize serverPath
708            Iterator<String> sitePathPrefixIt = _pathSanitizer.get(_serverSite).iterator();
709            boolean sanitized = false;
710            String sanitizedPath = _serverPath;
711
712            while (!sanitized && sitePathPrefixIt.hasNext())
713            {
714                String prefix = sitePathPrefixIt.next();
715
716                // Nothing to do in this case.
717                if (prefix == null)
718                {
719                    sanitized = true;
720                }
721                else if (StringUtils.startsWith(_serverPath, prefix))
722                {
723                    sanitizedPath = StringUtils.removeStart(_serverPath, prefix);
724                    sanitized = true;
725                }
726            }
727
728            // add site name when needed
729            if (!StringUtils.startsWithAny(sanitizedPath, _SPECIAL_PATH_PREFIXS))
730            {
731                sanitizedPath = '/' + _serverSite + sanitizedPath;
732            }
733
734            return sanitizedPath;
735        }
736
737        /**
738         * Indicates if this instance has front(-office) information.
739         * @return a boolean
740         */
741        protected boolean hasFrontInfo()
742        {
743            return _frontPath != null && !"-".equals(_frontPath);
744        }
745    }
746
747    /**
748     * Object model representing an entry of stats for a front resource, coming
749     * from the Front (direct request to tomcat, bypassing any HTTP server if any).
750     */
751    protected class FrontFromFrontStatsEntry
752    {
753        /** front site name */
754        protected String _frontSite;
755        /** front path */
756        protected String _frontPath;
757        /** is front cacheable */
758        protected boolean _frontCacheable;
759        /** front hits */
760        protected int _frontHits;
761        /** front cache hits 1 */
762        protected int _frontCacheHits1;
763        /** front cache hits 2 */
764        protected int _frontCacheHits2;
765
766        /**
767         * Ctor
768         * @param data map of raw data object.
769         */
770        protected FrontFromFrontStatsEntry(Map<String, Object> data)
771        {
772            _frontSite = (String) data.get("FRONT_SITE");
773            _frontPath = (String) data.get("FRONT_PATH");
774            _frontCacheable = (boolean) data.get("FRONT_CACHEABLE");
775            _frontHits = (int) data.get("FRONT_HITS");
776            _frontCacheHits1 = (int) data.get("FRONT_CACHE_HITS_1");
777            _frontCacheHits2 = (int) data.get("FRONT_CACHE_HITS_2");
778        }
779    }
780
781    /**
782     * Object model representing an entry of stats for a back resource (ie. a
783     * page).
784     */
785    protected class BackStatsEntry
786    {
787        /** page id */
788        protected final String _pageId;
789        /** page path */
790        protected final String _pagePath;
791        /** cacheable */
792        protected final boolean _cacheable;
793        /** hits */
794        protected final int _hits;
795
796        /**
797         * Ctor
798         * @param data map of raw data object.
799         */
800        protected BackStatsEntry(Map<String, Object> data)
801        {
802            _pageId = (String) data.get("PAGE_ID");
803            _pagePath = (String) data.get("PAGE_PATH");
804            _cacheable = (boolean) data.get("CACHEABLE");
805            _hits = (int) data.get("HITS");
806        }
807    }
808}