001/* 002 * Copyright 2023 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.statistics; 017 018import java.util.ArrayList; 019import java.util.Comparator; 020import java.util.HashMap; 021import java.util.List; 022import java.util.Map; 023import java.util.stream.Collectors; 024 025import org.apache.avalon.framework.service.ServiceException; 026import org.apache.avalon.framework.service.ServiceManager; 027import org.apache.avalon.framework.service.Serviceable; 028import org.apache.commons.lang3.StringUtils; 029 030import org.ametys.cms.repository.Content; 031import org.ametys.core.util.I18nUtils; 032import org.ametys.plugins.explorer.resources.Resource; 033import org.ametys.plugins.explorer.resources.ResourceCollection; 034import org.ametys.plugins.repository.AmetysObject; 035import org.ametys.plugins.repository.AmetysObjectIterable; 036import org.ametys.plugins.repository.AmetysObjectResolver; 037import org.ametys.plugins.repository.TraversableAmetysObject; 038import org.ametys.runtime.i18n.I18nizableText; 039import org.ametys.runtime.plugin.component.PluginAware; 040import org.ametys.runtime.plugins.admin.statistics.Statistics; 041import org.ametys.runtime.plugins.admin.statistics.StatisticsNode; 042import org.ametys.runtime.plugins.admin.statistics.StatisticsProvider; 043import org.ametys.runtime.plugins.admin.statistics.StatisticsValue; 044import org.ametys.web.cache.PageHelper; 045import org.ametys.web.repository.content.SharedContent; 046import org.ametys.web.repository.content.WebContent; 047import org.ametys.web.repository.page.Page; 048import org.ametys.web.repository.page.SitemapElement; 049import org.ametys.web.repository.page.Zone; 050import org.ametys.web.repository.page.ZoneItem; 051import org.ametys.web.repository.site.Site; 052import org.ametys.web.repository.site.SiteManager; 053import org.ametys.web.repository.sitemap.Sitemap; 054import org.ametys.web.service.Service; 055import org.ametys.web.service.ServiceExtensionPoint; 056import org.ametys.web.skin.SkinsManager; 057 058/** 059 * Web statistics 060 */ 061public class WebStatisticsProvider implements StatisticsProvider, Serviceable, PluginAware 062{ 063 private String _id; 064 private SiteManager _siteManager; 065 private PageHelper _pageHelper; 066 private ServiceExtensionPoint _serviceEP; 067 private I18nUtils _i18nUtils; 068 private AmetysObjectResolver _ametysResolver; 069 private SkinsManager _skinsManager; 070 071 public void service(ServiceManager manager) throws ServiceException 072 { 073 _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE); 074 _pageHelper = (PageHelper) manager.lookup(PageHelper.ROLE); 075 _serviceEP = (ServiceExtensionPoint) manager.lookup(ServiceExtensionPoint.ROLE); 076 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 077 _ametysResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 078 _skinsManager = (SkinsManager) manager.lookup(SkinsManager.ROLE); 079 } 080 081 public void setPluginInfo(String pluginName, String featureName, String id) 082 { 083 _id = id; 084 } 085 086 public Statistics getStatistics() 087 { 088 List<List<Map<String, Long>>> countPages = _countPages(); 089 int siteCount = countPages.size(); 090 int sitemapCount = countPages.stream().map(List<Map<String, Long>>::size).reduce(0, Integer::sum); 091 List<Long> nbPagesPerSitemap = countPages.stream().flatMap(List<Map<String, Long>>::stream).map(m -> m.get("NBPAGES")).sorted().toList(); 092 long pagesCount = nbPagesPerSitemap.stream().reduce(0L, Long::sum); 093 List<Long> nbContentsRefPerSitemap = countPages.stream().flatMap(List<Map<String, Long>>::stream).map(m -> m.get("CONTENTS")).sorted().toList(); 094 long contentsRefsCount = nbContentsRefPerSitemap.stream().reduce(0L, Long::sum); 095 List<Long> nbServicesPerSitemap = countPages.stream().flatMap(List<Map<String, Long>>::stream).map(m -> m.get("SERVICES")).sorted().toList(); 096 long servicesCount = nbServicesPerSitemap.stream().reduce(0L, Long::sum); 097 List<Long> nbCacheablePagesPerSitemap = countPages.stream().flatMap(List<Map<String, Long>>::stream).map(m -> m.get("NBCACHEABLE")).sorted().toList(); 098 long cacheablePagesCount = nbCacheablePagesPerSitemap.stream().reduce(0L, Long::sum); 099 100 Map<String, Long> contentsForSites = _processContentsForSites(); 101 102 return new StatisticsNode( 103 _id, 104 new I18nizableText("plugin.web", "PLUGINS_WEB_STATISTICS_LABEL"), 105 "ametysicon-code-html-link", 106 null, 107 List.of( 108 new StatisticsNode( 109 "sites", 110 new I18nizableText("plugin.web", "PLUGINS_WEB_STATISTICS_SITES_LABEL"), 111 "ametysicon-world-earth-black", 112 siteCount, 113 List.of( 114 new StatisticsNode( 115 "sitemaps", 116 new I18nizableText("plugin.web", "PLUGINS_WEB_STATISTICS_SITES_SITEMAP_LABEL"), 117 "ametysicon-world-flag", 118 sitemapCount, 119 List.of( 120 new StatisticsNode( 121 "pages", 122 new I18nizableText("plugin.web", "PLUGINS_WEB_STATISTICS_SITES_SITEMAP_PAGES_LABEL"), 123 "ametysicon-website38", 124 pagesCount, 125 List.of( 126 new StatisticsValue( 127 "max", 128 new I18nizableText("plugin.web", "PLUGINS_WEB_STATISTICS_SITES_SITEMAP_PAGES_MAX_LABEL"), 129 "ametysicon-sort51", 130 nbPagesPerSitemap.size() > 0 ? nbPagesPerSitemap.get(nbPagesPerSitemap.size() - 1) : 0 131 ), 132 new StatisticsValue( 133 "median", 134 new I18nizableText("plugin.web", "PLUGINS_WEB_STATISTICS_SITES_SITEMAP_PAGES_MEDIAN_LABEL"), 135 "ametysicon-maths-window-symbol-x", 136 nbPagesPerSitemap.size() > 0 ? nbPagesPerSitemap.get(nbPagesPerSitemap.size() / 2) : 0 137 ), 138 new StatisticsValue( 139 "cacheable", 140 new I18nizableText("plugin.web", "PLUGINS_WEB_STATISTICS_SITES_SITEMAP_PAGES_CACHEABLE_LABEL"), 141 "ametysicon-desktop-archive", 142 cacheablePagesCount 143 ) 144 ), 145 false 146 ), 147 new StatisticsNode( 148 "content-references", 149 new I18nizableText("plugin.web", "PLUGINS_WEB_STATISTICS_SITES_SITEMAP_CONTENTREFERENCES_LABEL"), 150 "ametysicon-text70", 151 contentsRefsCount, 152 List.of( 153 new StatisticsValue( 154 "max", 155 new I18nizableText("plugin.web", "PLUGINS_WEB_STATISTICS_SITES_SITEMAP_CONTENTREFERENCES_MAX_LABEL"), 156 "ametysicon-sort51", 157 nbContentsRefPerSitemap.size() > 0 ? nbContentsRefPerSitemap.get(nbContentsRefPerSitemap.size() - 1) : 0 158 ), 159 new StatisticsValue( 160 "median", 161 new I18nizableText("plugin.web", "PLUGINS_WEB_STATISTICS_SITES_SITEMAP_CONTENTREFERENCES_MEDIAN_LABEL"), 162 "ametysicon-maths-window-symbol-x", 163 nbContentsRefPerSitemap.size() > 0 ? nbContentsRefPerSitemap.get(nbContentsRefPerSitemap.size() / 2) : 0 164 ) 165 ), 166 false 167 ), 168 new StatisticsNode( 169 "content-references", 170 new I18nizableText("plugin.web", "PLUGINS_WEB_STATISTICS_SITES_SITEMAP_SERVICES_LABEL"), 171 "ametysicon-text70", 172 servicesCount, 173 List.of( 174 new StatisticsValue( 175 "max", 176 new I18nizableText("plugin.web", "PLUGINS_WEB_STATISTICS_SITES_SITEMAP_SERVICES_MAX_LABEL"), 177 "ametysicon-sort51", 178 nbServicesPerSitemap.size() > 0 ? nbServicesPerSitemap.get(nbServicesPerSitemap.size() - 1) : 0 179 ), 180 new StatisticsValue( 181 "median", 182 new I18nizableText("plugin.web", "PLUGINS_WEB_STATISTICS_SITES_SITEMAP_SERVICES_MEDIAN_LABEL"), 183 "ametysicon-maths-window-symbol-x", 184 nbServicesPerSitemap.size() > 0 ? nbServicesPerSitemap.get(nbServicesPerSitemap.size() / 2) : 0 185 ) 186 ), 187 false 188 ) 189 ), 190 false 191 ), 192 new StatisticsNode( 193 "contents", 194 new I18nizableText("plugin.web", "PLUGINS_WEB_STATISTICS_SITES_CONTENTS_LABEL"), 195 "ametysicon-text70", 196 contentsForSites.get("COUNT"), 197 List.of( 198 new StatisticsValue( 199 "orphans", 200 new I18nizableText("plugin.web", "PLUGINS_WEB_STATISTICS_SITES_CONTENTS_ORPHANS_LABEL"), 201 "ametysicon-sort51", 202 contentsForSites.get("ORPHANS") 203 ), 204 new StatisticsValue( 205 "shared", 206 new I18nizableText("plugin.web", "PLUGINS_WEB_STATISTICS_SITES_CONTENTS_EXTERNAL_LABEL"), 207 "ametysicon-maths-window-symbol-x", 208 contentsForSites.get("EXTERNAL") 209 ) 210 ), 211 false 212 ), 213 new StatisticsNode( 214 "services", 215 new I18nizableText("plugin.web", "PLUGINS_WEB_STATISTICS_SITES_SERVICES_LABEL"), 216 "ametysicon-system-monitoring", 217 null, 218 _getServices(), 219 false 220 ), 221 _processResources(), 222 new StatisticsValue( 223 "skins", 224 new I18nizableText("plugin.web", "PLUGINS_WEB_STATISTICS_SITES_SKINS_LABEL"), 225 "ametysicon-puzzle-piece1", 226 _skinsManager.getResourceSkins() 227 ) 228 ), 229 true 230 ) 231 ), 232 true 233 ); 234 } 235 236 private List<List<Map<String, Long>>> _countPages() 237 { 238 try (AmetysObjectIterable<Site> sites = _siteManager.getSites()) 239 { 240 List<List<Map<String, Long>>> sitesValues = new ArrayList<>(); 241 242 for (Site site : sites) 243 { 244 sitesValues.add(_countPages(site)); 245 } 246 247 return sitesValues; 248 } 249 } 250 251 private List<Map<String, Long>> _countPages(Site site) 252 { 253 List<Map<String, Long>> siteValues = new ArrayList<>(); 254 255 for (Sitemap sitemap : site.getSitemaps()) 256 { 257 siteValues.add(_countPages(sitemap)); 258 } 259 260 return siteValues; 261 } 262 263 private Map<String, Long> _countPages(SitemapElement pages) 264 { 265 Map<String, Long> sitemapElementValues = _newCountMap(); 266 267 AmetysObjectIterable<? extends Page> childPages = pages.getChildrenPages(); 268 269 sitemapElementValues.put("NBPAGES", sitemapElementValues.get("NBPAGES") + childPages.getSize()); 270 271 int cacheableCount = 0; 272 273 for (Page page : childPages) 274 { 275 boolean isCacheable = _pageHelper.isCacheable(page); 276 for (Zone zone : page.getZones()) 277 { 278 AmetysObjectIterable<? extends ZoneItem> zoneItems = zone.getZoneItems(); 279 for (ZoneItem item : zoneItems) 280 { 281 switch (item.getType()) 282 { 283 case CONTENT: 284 sitemapElementValues.put("CONTENTS", sitemapElementValues.get("CONTENTS") + 1); 285 break; 286 case SERVICE: 287 sitemapElementValues.put("SERVICES", sitemapElementValues.get("SERVICES") + 1); 288 break; 289 default: 290 break; 291 } 292 } 293 } 294 295 if (isCacheable) 296 { 297 cacheableCount++; 298 } 299 300 _sumMaps(sitemapElementValues, _countPages(page)); 301 } 302 303 sitemapElementValues.put("NBCACHEABLE", sitemapElementValues.get("NBCACHEABLE") + cacheableCount); 304 305 return sitemapElementValues; 306 } 307 308 309 private Map<String, Long> _newCountMap() 310 { 311 Map<String, Long> countMap = new HashMap<>(); 312 countMap.put("NBPAGES", 0L); // Number of pages 313 countMap.put("NBCACHEABLE", 0L); // Number of cacheable pages 314 countMap.put("SERVICES", 0L); // Number of services 315 countMap.put("CONTENTS", 0L); // Number of contents 316 return countMap; 317 } 318 319 private void _sumMaps(Map<String, Long> mainMap, Map<String, Long> mapToAdd) 320 { 321 for (String k : mainMap.keySet()) 322 { 323 mainMap.put(k, mainMap.get(k) + mapToAdd.get(k)); 324 } 325 } 326 327 private List<Statistics> _getServices() 328 { 329 List<Statistics> stats = new ArrayList<>(); 330 331 for (String serviceId : _serviceEP.getExtensionsIds()) 332 { 333 Service service = _serviceEP.getExtension(serviceId); 334 335 stats.add(new StatisticsValue( 336 serviceId, 337 new I18nizableText(_i18nUtils.translate(service.getLabel())), // Translate now for sorting 338 StringUtils.defaultIfBlank(service.getIconGlyph(), "ametysicon-system-monitoring"), 339 _countAllServices(serviceId) 340 )); 341 } 342 343 stats.sort(new Comparator<>() { 344 public int compare(Statistics o1, Statistics o2) 345 { 346 return o1.getLabel().getLabel().compareTo(o2.getLabel().getLabel()); 347 } 348 }); 349 350 String query = "//element(ametys:zoneItem, ametys:zoneItem)[@ametys-internal:type='SERVICE'" 351 + _serviceEP.getExtensionsIds().stream().map(id -> " and not(@ametys-internal:service='" + id + "')").collect(Collectors.joining()) 352 + "]"; 353 try (AmetysObjectIterable<ZoneItem> serviceZoneItems = _ametysResolver.query(query)) 354 { 355 stats.add(0, new StatisticsValue( 356 "unknown", 357 new I18nizableText("plugin.web", "PLUGINS_WEB_STATISTICS_SITES_SERVICES_UNKNOWN_LABEL"), 358 "ametysicon-file-extension-generic-unknown", 359 serviceZoneItems.getSize() 360 )); 361 } 362 363 return stats; 364 } 365 366 private long _countAllServices(String serviceId) 367 { 368 String query = "//element(ametys:zoneItem, ametys:zoneItem)[@ametys-internal:type='SERVICE'" 369 + (serviceId != null ? " and @ametys-internal:service='" + serviceId + "'" : "") 370 + "]"; 371 try (AmetysObjectIterable<ZoneItem> serviceZoneItems = _ametysResolver.query(query)) 372 { 373 return serviceZoneItems.getSize(); 374 } 375 } 376 377 private Map<String, Long> _processContentsForSites() 378 { 379 Map<String, Long> values = new HashMap<>(); 380 values.put("COUNT", 0L); 381 values.put("ORPHANS", 0L); 382 values.put("EXTERNAL", 0L); 383 384 try (AmetysObjectIterable<Site> sites = _siteManager.getSites()) 385 { 386 for (Site site : sites) 387 { 388 _processContents (site, values); 389 } 390 } 391 catch (Exception e) 392 { 393 throw new IllegalStateException(e); 394 } 395 396 return values; 397 } 398 399 private void _processContents (Site site, Map<String, Long> values) 400 { 401 // Number of contents 402 AmetysObjectIterable<Content> contents = site.getContents(); 403 values.put("COUNT", values.get("COUNT") + contents.getSize()); 404 405 int orphans = 0; 406 int external = 0; 407 for (Content content : contents) 408 { 409 if (content instanceof WebContent webContent 410 && webContent.getReferencingPages().size() == 0) 411 { 412 orphans++; 413 } 414 if (content instanceof SharedContent) 415 { 416 external++; 417 } 418 } 419 420 // Number of orphan contents 421 values.put("ORPHANS", values.get("ORPHANS") + orphans); 422 423 // Number of shared contents 424 values.put("EXTERNAL", values.get("EXTERNAL") + external); 425 } 426 427 private Statistics _processResources() 428 { 429 HashMap<String, Long> values = new HashMap<>(); 430 values.put("FOLDERCOUNT", 0L); 431 values.put("RESOURCECOUNT", 0L); 432 values.put("TOTALSIZE", 0L); 433 434 try (AmetysObjectIterable<Site> sites = _siteManager.getSites()) 435 { 436 for (Site site : sites) 437 { 438 _processResources (site.getRootResources(), values); 439 } 440 } 441 442 return new StatisticsNode( 443 "explorer", 444 new I18nizableText("plugin.web", "PLUGINS_WEB_STATISTICS_SITES_EXPLORER_LABEL"), 445 "ametysicon-folder249", 446 values.get("TOTALSIZE"), 447 List.of( 448 new StatisticsValue( 449 "folders", 450 new I18nizableText("plugin.web", "PLUGINS_WEB_STATISTICS_SITES_EXPLORER_FOLDER_LABEL"), 451 "ametysicon-folder250", 452 values.get("FOLDERCOUNT") 453 ), 454 new StatisticsValue( 455 "files", 456 new I18nizableText("plugin.web", "PLUGINS_WEB_STATISTICS_SITES_EXPLORER_FILES_LABEL"), 457 "ametysicon-document77", 458 values.get("RESOURCECOUNT") 459 ) 460 ), 461 false 462 ); 463 } 464 465 private void _processResources(TraversableAmetysObject resourceContainer, HashMap<String, Long> values) 466 { 467 AmetysObjectIterable<? extends AmetysObject> objects = resourceContainer.getChildren(); 468 469 for (AmetysObject object : objects) 470 { 471 // Traverse the child nodes if depth < 0 (generate all) or depth > 0 (we're not in the last level). 472 if (object instanceof ResourceCollection) 473 { 474 values.put("FOLDERCOUNT", values.get("FOLDERCOUNT") + 1); 475 476 _processResources((ResourceCollection) object, values); 477 } 478 else if (object instanceof Resource) 479 { 480 Resource resource = (Resource) object; 481 482 values.put("RESOURCECOUNT", values.get("RESOURCECOUNT") + 1); 483 values.put("TOTALSIZE", values.get("TOTALSIZE") + resource.getLength()); 484 } 485 } 486 } 487}