001/*
002 *  Copyright 2015 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.site;
017
018import java.util.ArrayList;
019import java.util.HashMap;
020import java.util.List;
021import java.util.Map;
022
023import javax.jcr.Node;
024import javax.jcr.Property;
025import javax.jcr.PropertyIterator;
026import javax.jcr.RepositoryException;
027
028import org.apache.avalon.framework.parameters.Parameters;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.cocoon.ProcessingException;
032import org.apache.cocoon.acting.ServiceableAction;
033import org.apache.cocoon.environment.ObjectModelHelper;
034import org.apache.cocoon.environment.Redirector;
035import org.apache.cocoon.environment.Request;
036import org.apache.cocoon.environment.SourceResolver;
037
038import org.ametys.cms.repository.Content;
039import org.ametys.core.cocoon.JSonReader;
040import org.ametys.plugins.explorer.resources.Resource;
041import org.ametys.plugins.explorer.resources.ResourceCollection;
042import org.ametys.plugins.repository.AmetysObject;
043import org.ametys.plugins.repository.AmetysObjectIterable;
044import org.ametys.plugins.repository.TraversableAmetysObject;
045import org.ametys.plugins.repository.jcr.JCRAmetysObject;
046import org.ametys.runtime.i18n.I18nizableText;
047import org.ametys.web.cache.PageHelper;
048import org.ametys.web.repository.page.Page;
049import org.ametys.web.repository.page.PagesContainer;
050import org.ametys.web.repository.page.Zone;
051import org.ametys.web.repository.page.ZoneItem;
052import org.ametys.web.repository.site.Site;
053import org.ametys.web.repository.site.SiteManager;
054import org.ametys.web.repository.sitemap.Sitemap;
055import org.ametys.web.service.Service;
056import org.ametys.web.service.ServiceExtensionPoint;
057
058/**
059 * Provides some statistics about a {@link Site}, such as :<br>
060 * <ul>
061 * <li>Number of pages
062 * <li>Number of live contents
063 * <li>Number of orphaned contents
064 * <li>Number of external contents
065 * </ul>
066 */
067public class SiteStatisticsAction extends ServiceableAction
068{
069
070    /** The site manager. */
071    protected SiteManager _siteManager;
072    
073    /** The service extension point. */
074    protected ServiceExtensionPoint _serviceExtPt;
075    
076    /** The page cache helper. */
077    protected PageHelper _pageCacheHelper;
078    
079    @Override
080    public void service(ServiceManager sManager) throws ServiceException
081    {
082        super.service(sManager);
083        _siteManager = (SiteManager) sManager.lookup(SiteManager.ROLE);
084        _serviceExtPt = (ServiceExtensionPoint) sManager.lookup(ServiceExtensionPoint.ROLE);
085        _pageCacheHelper = (PageHelper) sManager.lookup(PageHelper.ROLE);
086    }
087    
088    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
089    {
090        Map<String, Object> result = new HashMap<>();
091        
092        Request request = ObjectModelHelper.getRequest(objectModel);
093        String siteName = parameters.getParameter("siteName", request.getParameter("siteName"));
094        Site site = _siteManager.getSite(siteName);
095        
096        List<Map<String, Object>> statistics = new ArrayList<>();
097        
098        // Statistics on sitemaps
099        for (Sitemap sitemap : site.getSitemaps())
100        {
101            statistics.add(_sitemap2json(sitemap));
102        }
103        
104        
105        try
106        {
107            // Statistics on contents
108            statistics.add(_contents2json(site));
109        }
110        catch (RepositoryException e)
111        {
112            throw new ProcessingException("Unable to process contents for site " + site.getName(), e);
113        }
114        
115        // Statistics on resources
116        statistics.add(_resources2json(site));
117        
118        result.put("values", statistics);
119        
120        request.setAttribute(JSonReader.OBJECT_TO_READ, result);
121        
122        return EMPTY_MAP;
123    }
124    
125    /**
126     * Statistics on contents
127     * @param site the site
128     * @return the statistics on contents
129     * @throws RepositoryException if an error occurs
130     */
131    protected Map<String, Object> _contents2json (Site site) throws RepositoryException
132    {
133        Map<String, Object> contents2json = new HashMap<>();
134        
135        contents2json.put("label", new I18nizableText("plugin.web", "PLUGINS_WEB_ADMINISTRATOR_SITE_STATISTICS_CONTENTS"));
136        contents2json.put("type", "contents");
137        
138        HashMap<String, Long> values = new HashMap<>();
139        values.put("COUNT", 0L);
140        values.put("ORPHANED", 0L);
141        values.put("EXTERNAL", 0L);
142        
143        _processContents (site, values);
144        
145        List<Map<String, Object>> values2json = new ArrayList<>();
146        
147        for (String name : values.keySet())
148        {
149            Map<String, Object> stat = new HashMap<>();
150            stat.put("label", new I18nizableText("plugin.web", "PLUGINS_WEB_ADMINISTRATOR_SITE_STATISTICS_VALUE_CONTENTS_" + name));
151            stat.put("value", values.get(name));
152            stat.put("values", new ArrayList<>());
153            values2json.add(stat);
154        }
155        
156        contents2json.put("values", values2json);
157        
158        return contents2json;
159    }
160    
161    /**
162     * Process the contents of the site to gather information on them
163     * @param site the site
164     * @param values the information on the contents
165     * @throws RepositoryException if an error occurs on the repository
166     */
167    protected void _processContents (Site site, Map<String, Long> values) throws RepositoryException
168    {
169        // Number of contents
170        AmetysObjectIterable<Content> contents = site.getContents();
171        values.put("COUNT", values.get("COUNT") + contents.getSize());
172        
173        int orphaned = 0;
174        int external = 0;
175        for (Content content : contents)
176        {
177            int referers = 0;
178            int ext = 0;
179            
180            // FIXME API getNode
181            PropertyIterator it = ((JCRAmetysObject) content).getNode().getReferences();
182            while (it.hasNext())
183            {
184                Property property = it.nextProperty();
185                Node node = property.getParent();
186                if (!node.isNodeType("oswf:entry"))
187                {
188                    referers++;
189                    
190                    if (node.isNodeType("ametys:zoneItem"))
191                    {
192                        Node parent = node.getParent();
193                        while (!parent.isNodeType("ametys:site"))
194                        {
195                            parent = parent.getParent();
196                        }
197                        
198                        if (!parent.getName().equals(site.getName()))
199                        {
200                            ext++;
201                        }
202                    }
203                }
204            }
205            
206            if (referers == 0)
207            {
208                orphaned++;
209            }
210            
211            if (ext > 0)
212            {
213                external++;
214            }
215        }
216        
217        // Number of orphan contents
218        values.put("ORPHANED", values.get("ORPHANED") + orphaned);
219        
220        // Number of shared contents
221        values.put("EXTERNAL", values.get("ORPHANED") + external);
222    }
223    
224    /**
225     * Statistics on resources
226     * @param site the site
227     * @return the statistics on resources
228     */
229    protected Map<String, Object> _resources2json(Site site)
230    {
231        Map<String, Object> resources2json = new HashMap<>();
232        
233        resources2json.put("label", new I18nizableText("plugin.web", "PLUGINS_WEB_ADMINISTRATOR_SITE_STATISTICS_RESOURCES"));
234        resources2json.put("type", "resources");
235        
236        HashMap<String, Long> values = new HashMap<>();
237        values.put("FOLDERCOUNT", 0L);
238        values.put("RESOURCECOUNT", 0L);
239        values.put("TOTALSIZE", 0L);
240        
241        _processResources(site.getRootResources(), values);
242        
243        List<Map<String, Object>> values2json = new ArrayList<>();
244        
245        for (String name : values.keySet())
246        {
247            Map<String, Object> stat = new HashMap<>();
248            stat.put("label", new I18nizableText("plugin.web", "PLUGINS_WEB_ADMINISTRATOR_SITE_STATISTICS_VALUE_RESOURCES_" + name));
249            stat.put("value", values.get(name));
250            stat.put("values", new ArrayList<>());
251            values2json.add(stat);
252            
253        }
254        
255        resources2json.put("values", values2json);
256        
257        return resources2json;
258    }
259    
260    /**
261     * Statistics on sitemap
262     * @param sitemap the sitemap
263     * @return the statistics on sitemap
264     */
265    protected Map<String, Object> _sitemap2json(Sitemap sitemap)
266    {
267        Map<String, Object> sitemap2json = new HashMap<>();
268        
269        sitemap2json.put("name", sitemap.getName());
270        sitemap2json.put("type", "sitemap");
271        sitemap2json.put("label", new I18nizableText("plugin.web", "PLUGINS_WEB_ADMINISTRATOR_SITE_STATISTICS_SITEMAP_" + sitemap.getName().toUpperCase()));
272        
273        Map<String, Long> values = new HashMap<>();
274        values.put("NBPAGES", 0L); // Number of pages
275        values.put("NBCACHEABLE", 0L); // Number of cacheable pages
276        values.put("SERVICES", 0L); // Number of services
277        values.put("CONTENTS", 0L); // Number of contents
278        
279        _processPages(sitemap, values);
280        
281        List<Map<String, Object>> values2json = new ArrayList<>();
282        
283        for (String name : values.keySet())
284        {
285            Map<String, Object> stat = new HashMap<>();
286            stat.put("label", new I18nizableText("plugin.web", "PLUGINS_WEB_ADMINISTRATOR_SITE_STATISTICS_VALUE_SITEMAP_" + name));
287            stat.put("value", values.get(name));
288            stat.put("values", new ArrayList<>());
289            values2json.add(stat);
290            
291        }
292        
293        sitemap2json.put("values", values2json);
294        
295        return sitemap2json;
296    }
297    
298    /**
299     * Process statistics on a pages container
300     * @param pages The pages container (page or sitemap)
301     * @param values The values
302     */
303    protected void _processPages(PagesContainer pages, Map<String, Long> values)
304    {
305        AmetysObjectIterable<? extends Page> childPages = pages.getChildrenPages();
306        
307        values.put("NBPAGES", values.get("NBPAGES") + childPages.getSize());
308        
309        int cacheableCount = 0;
310        
311        for (Page page : childPages)
312        {
313            boolean isCacheable = _pageCacheHelper.isCacheable(page);
314            for (Zone zone : page.getZones())
315            {
316                for (ZoneItem item : zone.getZoneItems())
317                {
318                    switch (item.getType())
319                    {
320                        case CONTENT:
321                            values.put("CONTENTS", values.get("CONTENTS") + 1);
322                            break;
323                        case SERVICE:
324                            values.put("SERVICES", values.get("SERVICES") + 1);
325                            
326                            // Test if the service is cacheable, only if the page is cacheable since then.
327                            if (isCacheable)
328                            {
329                                Service service = _serviceExtPt.getExtension(item.getServiceId());
330                                if (service != null && !service.isCacheable(page, item))
331                                {
332                                    isCacheable = false;
333                                }
334                            }
335                            break;
336                        default:
337                            break;
338                    }
339                }
340            }
341            
342            if (isCacheable)
343            {
344                cacheableCount++;
345            }
346            
347            _processPages(page, values);
348        }
349        
350        values.put("NBCACHEABLE", values.get("NBCACHEABLE") + cacheableCount);
351    }
352    
353    /**
354     * Process statistics on a resources
355     * @param resourceContainer The resources container
356     * @param values The values
357     */
358    protected void _processResources(TraversableAmetysObject resourceContainer, HashMap<String, Long> values)
359    {
360        AmetysObjectIterable<? extends AmetysObject> objects = resourceContainer.getChildren();
361        
362        for (AmetysObject object : objects)
363        {
364            // Traverse the child nodes if depth < 0 (generate all) or depth > 0 (we're not in the last level).
365            if (object instanceof ResourceCollection)
366            {
367                values.put("FOLDERCOUNT", values.get("FOLDERCOUNT") + 1);
368                
369                _processResources((ResourceCollection) object, values);
370            }
371            else if (object instanceof Resource)
372            {
373                Resource resource = (Resource) object;
374                
375                values.put("RESOURCECOUNT", values.get("RESOURCECOUNT") + 1);
376                values.put("TOTALSIZE", values.get("TOTALSIZE") + resource.getLength());
377            }
378        }
379    }
380
381}