1 /*
2  * Copyright 2008-2019 by Emeric Vernat
3  *
4  *     This file is part of Java Melody.
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  */

18 package net.bull.javamelody.internal.model;
19
20 import java.io.IOException;
21 import java.io.Serializable;
22 import java.util.ArrayList;
23 import java.util.Collections;
24 import java.util.Comparator;
25 import java.util.Date;
26 import java.util.LinkedList;
27 import java.util.List;
28 import java.util.concurrent.ConcurrentHashMap;
29 import java.util.concurrent.ConcurrentMap;
30 import java.util.regex.Matcher;
31 import java.util.regex.Pattern;
32
33 import javax.servlet.http.HttpServletRequest;
34 import javax.servlet.http.HttpSession;
35
36 import net.bull.javamelody.SessionListener;
37 import net.bull.javamelody.internal.common.LOG;
38
39 /**
40  * Données statistiques des requêtes pour un compteur nommé comme http ou sql.
41  * Ces données sont accumulées au fil du temps selon les requêtes dans l'application.
42  * Elles correspondent soit aux statistiques courantes depuis une date initiale,
43  * soit à une période donnée pour un jour, une semaine, un mois ou une année.
44  *
45  * Toutes les méthodes sur une instance de cette classe sont conçues pour être thread-safe,
46  * c'est-à-dire qu'elles gère un état qui est non modifiable
47  * ou alors synchronisé pour être accessible et modifiable depuis plusieurs threads.
48  * Les instances sont sérialisables pour pouvoir être persistées sur disque
49  * et transmises au serveur de collecte.
50  * @author Emeric Vernat
51  */

52 public class Counter implements Cloneable, Serializable { // NOPMD
53     /**
54      * Nom du counter des requêtes http.
55      */

56     public static final String HTTP_COUNTER_NAME = "http";
57     /**
58      * Nom du counter des erreurs systèmes http.
59      */

60     public static final String ERROR_COUNTER_NAME = "error";
61     /**
62      * Nom du counter des logs d'erreurs systèmes.
63      */

64     public static final String LOG_COUNTER_NAME = "log";
65     /**
66      * Nom du counter des JSPs.
67      */

68     public static final String JSP_COUNTER_NAME = "jsp";
69     /**
70      * Nom du counter des actions Struts.
71      */

72     public static final String STRUTS_COUNTER_NAME = "struts";
73     /**
74      * Nom du counter des actions JSF RI (Mojarra).
75      */

76     public static final String JSF_COUNTER_NAME = "jsf";
77     /**
78      * Nom du counter des requêtes SQL.
79      */

80     public static final String SQL_COUNTER_NAME = "sql";
81     /**
82      * Nom du counter des jobs.
83      */

84     public static final String JOB_COUNTER_NAME = "job";
85     /**
86      * Nom du counter des builds Jenkins.
87      */

88     public static final String BUILDS_COUNTER_NAME = "builds";
89     /**
90      * Nombre max d'erreurs conservées par le counter (si counter d'erreurs http ou de log d'erreurs).
91      */

92     public static final int MAX_ERRORS_COUNT = 100;
93     /**
94      * Caractère de remplacement s'il y a des paramètres *-transform-pattern.
95      */

96     static final char TRANSFORM_REPLACEMENT_CHAR = '$';
97     /**
98      * Nombre max par défaut de requêtes conservées par counter, <br/>
99      * mais peut être redéfini par exemple pour le counter des erreurs http ou celui des logs.
100      */

101     static final int MAX_REQUESTS_COUNT = 10000;
102     private static final String TRANSFORM_REPLACEMENT = "\\" + TRANSFORM_REPLACEMENT_CHAR;
103     private static final long serialVersionUID = 6759729262180992976L;
104     private String application;
105     private boolean displayed = true;
106     private transient boolean used;
107     private final String name;
108     private final boolean errorCounter;
109     private final String storageName;
110     private final String iconName;
111     // on conserve childCounterName et pas childCounter pour assurer la synchronisation/clone et la sérialisation
112     private final String childCounterName;
113     @SuppressWarnings("all")
114     private final ConcurrentMap<String, CounterRequest> requests = new ConcurrentHashMap<>();
115     // note : même si rootCurrentContextsByThreadId n'est pas transient la map est normalement vide avant sérialisation
116     // (on garde en non transient pour ne pas avoir null après désérialisation ce qui pourrait donner des NPE)
117     @SuppressWarnings("all")
118     private final ConcurrentMap<Long, CounterRequestContext> rootCurrentContextsByThreadId = new ConcurrentHashMap<>();
119     private final LinkedList<CounterError> errors; // NOPMD
120     private Date startDate = new Date();
121     private int maxRequestsCount = MAX_REQUESTS_COUNT;
122     private long estimatedMemorySize;
123     // Pour les contextes, on utilise un ThreadLocal et pas un InheritableThreadLocal
124     // puisque si on crée des threads alors la requête parente peut se terminer avant les threads
125     // et le contexte serait incomplet.
126     private final transient ThreadLocal<CounterRequestContext> contextThreadLocal;
127     private transient Pattern requestTransformPattern;
128
129     /**
130      * Comparateur pour ordonner les requêtes par sommes des durées.
131      */

132     static final class CounterRequestComparator
133             implements Comparator<CounterRequest>, Serializable {
134         private static final long serialVersionUID = 1L;
135
136         /** {@inheritDoc} */
137         @Override
138         public int compare(CounterRequest request1, CounterRequest request2) {
139             return Long.compare(request1.getDurationsSum(), request2.getDurationsSum());
140         }
141     }
142
143     /**
144      * Comparateur pour ordonner les requêtes par nombre d'exécutions.
145      */

146     static final class CounterRequestByHitsComparator
147             implements Comparator<CounterRequest>, Serializable {
148         private static final long serialVersionUID = 1L;
149
150         /** {@inheritDoc} */
151         @Override
152         public int compare(CounterRequest request1, CounterRequest request2) {
153             return Long.compare(request1.getHits(), request2.getHits());
154         }
155     }
156
157     /**
158      * Comparateur pour ordonner les erreurs par heures d'exécution.
159      */

160     static final class CounterErrorComparator implements Comparator<CounterError>, Serializable {
161         private static final long serialVersionUID = 1L;
162
163         /** {@inheritDoc} */
164         @Override
165         public int compare(CounterError error1, CounterError error2) {
166             if (error1.getTime() < error2.getTime()) {
167                 return -1;
168             } else if (error1.getTime() > error2.getTime()) {
169                 return 1;
170             }
171             return 0;
172         }
173     }
174
175     /**
176      * Comparateur pour ordonner les requêtes en cours par durées écoulées.
177      */

178     public static final class CounterRequestContextComparator
179             implements Comparator<CounterRequestContext>, Serializable {
180         private static final long serialVersionUID = 1L;
181         private final long timeOfSnapshot;
182
183         public CounterRequestContextComparator(long timeOfSnapshot) {
184             super();
185             this.timeOfSnapshot = timeOfSnapshot;
186         }
187
188         /** {@inheritDoc} */
189         @Override
190         public int compare(CounterRequestContext context1, CounterRequestContext context2) {
191             return Integer.compare(context1.getDuration(timeOfSnapshot),
192                     context2.getDuration(timeOfSnapshot));
193         }
194     }
195
196     /**
197      * Constructeur d'un compteur.
198      * @param name Nom du compteur (par exemple: sql...)
199      * @param iconName Icône du compteur (par exemple: db.png)
200      */

201     public Counter(String name, String iconName) {
202         // ici, pas de compteur fils
203         this(name, name, iconName, nullnew ThreadLocal<CounterRequestContext>());
204     }
205
206     /**
207      * Constructeur d'un compteur.
208      * @param name Nom du compteur (par exemple: sql...)
209      * @param storageName Nom unique du compteur pour le stockage (par exemple: sql_20080724)
210      * @param iconName Icône du compteur (par exemple: db.png)
211      * @param childCounterName Nom du compteur fils (par exemple: sql)
212      */

213     public Counter(String name, String storageName, String iconName, String childCounterName) {
214         this(name, storageName, iconName, childCounterName,
215                 new ThreadLocal<CounterRequestContext>());
216     }
217
218     /**
219      * Constructeur d'un compteur.
220      * @param name Nom du compteur (par exemple: http...)
221      * @param iconName Icône du compteur (par exemple: db.png)
222      * @param childCounter Compteur fils (par exemple: sqlCounter)
223      */

224     public Counter(String name, String iconName, Counter childCounter) {
225         this(name, name, iconName, childCounter.getName(), childCounter.contextThreadLocal);
226     }
227
228     private Counter(String name, String storageName, String iconName, String childCounterName,
229             ThreadLocal<CounterRequestContext> contextThreadLocal) {
230         super();
231         assert name != null;
232         assert storageName != null;
233         this.name = name;
234         this.storageName = storageName;
235         this.errorCounter = ERROR_COUNTER_NAME.equals(name) || LOG_COUNTER_NAME.equals(name)
236                 || JOB_COUNTER_NAME.equals(name);
237         this.iconName = iconName;
238         this.childCounterName = childCounterName;
239         this.contextThreadLocal = contextThreadLocal;
240         if (errorCounter) {
241             this.errors = new LinkedList<>();
242         } else {
243             this.errors = null;
244         }
245     }
246
247     /**
248      * Définit le code de l'application de ce counter (non null).
249      * @param application String
250      */

251     void setApplication(String application) {
252         assert application != null;
253         this.application = application;
254     }
255
256     /**
257      * Retourne le code de l'application.
258      * @return String
259      */

260     String getApplication() {
261         return application;
262     }
263
264     /**
265      * Retourne le nom de ce counter (non null).
266      * @return String
267      */

268     public String getName() {
269         return name;
270     }
271
272     /**
273      * Retourne le nom de ce counter quand il est stocké sur disque (non null).
274      * @return String
275      */

276     public String getStorageName() {
277         return storageName;
278     }
279
280     /**
281      * Retourne le nom de l'icône de ce counter (peut être null).
282      * @return String
283      */

284     public String getIconName() {
285         return iconName;
286     }
287
288     /**
289      * Retourne le nom de l'éventuel counter fils (peut être null).
290      * @return String
291      */

292     public String getChildCounterName() {
293         return childCounterName;
294     }
295
296     boolean hasChildHits() {
297         for (final CounterRequest request : requests.values()) {
298             if (request.hasChildHits()) {
299                 return true;
300             }
301         }
302         return false;
303     }
304
305     /**
306      * Retourne la date et l'heure de début (non null).
307      * @return Date
308      */

309     public Date getStartDate() {
310         return startDate;
311     }
312
313     /**
314      * Définit la date et l'heure de début (non null).
315      * @param startDate Date
316      */

317     void setStartDate(Date startDate) {
318         assert startDate != null;
319         this.startDate = startDate;
320     }
321
322     /**
323      * Retourne true si ce counter est affiché dans les rapports.
324      * @return boolean
325      */

326     public boolean isDisplayed() {
327         return displayed;
328     }
329
330     /**
331      * Définit si ce counter est affiché dans les rapports.
332      * @param displayed boolean
333      */

334     public void setDisplayed(boolean displayed) {
335         this.displayed = displayed;
336     }
337
338     /**
339      * Retourne true si ce counter est utilisé
340      * (servira éventuellement à initialiser displayed dans FilterContext).
341      * @return boolean
342      */

343     public boolean isUsed() {
344         return used;
345     }
346
347     /**
348      * Définit si ce counter est utilisé
349      * (servira éventuellement à initialiser displayed dans FilterContext).
350      * @param used boolean
351      */

352     public void setUsed(boolean used) {
353         this.used = used;
354     }
355
356     /**
357      * Retourne l'expression régulière permettant de transformer les requêtes de ce counter
358      * avant agrégation dans les statistiques (peut être null).
359      * @return Pattern
360      */

361     Pattern getRequestTransformPattern() {
362         return requestTransformPattern;
363     }
364
365     /**
366      * Définit l'expression régulière permettant de transformer les requêtes de ce counter
367      * avant agrégation dans les statistiques.
368      * @param requestTransformPattern Pattern
369      */

370     public void setRequestTransformPattern(Pattern requestTransformPattern) {
371         this.requestTransformPattern = requestTransformPattern;
372     }
373
374     /**
375      * Retourne le nombre maximum de requêtes dans ce counter (entier positif).
376      * @return int
377      */

378     int getMaxRequestsCount() {
379         return maxRequestsCount;
380     }
381
382     /**
383      * Définit le nombre maximum de requêtes dans ce counter (entier positif).
384      * @param maxRequestsCount int
385      */

386     public void setMaxRequestsCount(int maxRequestsCount) {
387         assert maxRequestsCount > 0;
388         this.maxRequestsCount = maxRequestsCount;
389     }
390
391     /**
392      * Retourne l'estimation pessimiste de l'occupation mémoire de counter
393      * (c'est-à-dire la dernière taille sérialisée non compressée de ce counter)
394      * @return long
395      */

396     long getEstimatedMemorySize() {
397         return estimatedMemorySize;
398     }
399
400     public void bindContextIncludingCpu(String requestName) {
401         bindContext(requestName, requestName, null, ThreadInformations.getCurrentThreadCpuTime(),
402                 ThreadInformations.getCurrentThreadAllocatedBytes());
403     }
404
405     public void bindContext(String requestName, String completeRequestName,
406             HttpServletRequest httpRequest, long startCpuTime, long startAllocatedBytes) {
407         String remoteUser = null;
408         String sessionId = null;
409         if (httpRequest != null) {
410             remoteUser = httpRequest.getRemoteUser();
411             final HttpSession session = httpRequest.getSession(false);
412             if (session != null) {
413                 sessionId = session.getId();
414                 if (remoteUser == null) {
415                     final Object userAttribute = session
416                             .getAttribute(SessionListener.SESSION_REMOTE_USER);
417                     if (userAttribute instanceof String) {
418                         remoteUser = (String) userAttribute;
419                     }
420                 }
421             }
422         }
423         // requestName est la même chose que ce qui sera utilisée dans addRequest,
424         // completeRequestName est la même chose éventuellement complétée
425         // pour cette requête à destination de l'affichage dans les requêtes courantes
426         // (sinon mettre 2 fois la même chose)
427         final CounterRequestContext context = new CounterRequestContext(this,
428                 contextThreadLocal.get(), requestName, completeRequestName, httpRequest, remoteUser,
429                 startCpuTime, startAllocatedBytes, sessionId);
430         contextThreadLocal.set(context);
431         if (context.getParentContext() == null) {
432             rootCurrentContextsByThreadId.put(context.getThreadId(), context);
433         }
434     }
435
436     public void unbindContext() {
437         try {
438             contextThreadLocal.remove();
439         } finally {
440             rootCurrentContextsByThreadId.remove(Thread.currentThread().getId());
441         }
442     }
443
444     public void addRequestForCurrentContext(boolean systemError) {
445         final CounterRequestContext context = contextThreadLocal.get();
446         if (context != null) {
447             final long duration = context.getDuration(System.currentTimeMillis());
448             final int cpuUsedMillis = context.getCpuTime();
449             final int allocatedKBytes = context.getAllocatedKBytes();
450             addRequest(context.getRequestName(), duration, cpuUsedMillis, allocatedKBytes,
451                     systemError, -1);
452         }
453     }
454
455     public void addRequestForCurrentContext(String systemErrorStackTrace) {
456         assert errorCounter;
457         final CounterRequestContext context = contextThreadLocal.get();
458         // context peut être null (depuis JobGlobalListener, cf issue 34)
459         if (context != null) {
460             final long duration = context.getDuration(System.currentTimeMillis());
461             final int cpuUsedMillis = context.getCpuTime();
462             final int allocatedKBytes = context.getAllocatedKBytes();
463             addRequest(context.getRequestName(), duration, cpuUsedMillis, allocatedKBytes,
464                     systemErrorStackTrace != null, systemErrorStackTrace, -1);
465         }
466     }
467
468     public void addRequest(String requestName, long duration, int cpuTime, int allocatedKBytes,
469             boolean systemError, long responseSize) {
470         addRequest(requestName, duration, cpuTime, allocatedKBytes, systemError, null,
471                 responseSize);
472     }
473
474     private void addRequest(String requestName, long duration, int cpuTime, int allocatedKBytes,
475             boolean systemError, String systemErrorStackTrace, long responseSize) {
476         // la méthode addRequest n'est pas synchronisée pour ne pas avoir
477         // de synchronisation globale à l'application sur cette instance d'objet
478         // ce qui pourrait faire une contention et des ralentissements,
479         // par contre la map requests est synchronisée pour les modifications concurrentes
480
481         assert requestName != null;
482         assert duration >= 0;
483         assert cpuTime >= -1; // -1 pour requêtes sql
484         assert allocatedKBytes >= -1; // -1 pour requêtes sql
485         assert responseSize >= -1L; // -1 pour requêtes sql
486
487         final String aggregateRequestName = getAggregateRequestName(requestName);
488
489         final CounterRequestContext context = contextThreadLocal.get();
490         final CounterRequest request = getCounterRequestInternal(aggregateRequestName);
491         synchronized (request) {
492             // on synchronise par l'objet request pour éviter de mélanger des ajouts de hits
493             // concurrents entre plusieurs threads pour le même type de requête.
494             // Rq : on pourrait remplacer ce bloc synchronized par un synchronized
495             // sur les méthodes addHit et addChildHits dans la classe CounterRequest.
496             request.addHit(duration, cpuTime, allocatedKBytes, systemError, systemErrorStackTrace,
497                     responseSize);
498
499             if (context != null) {
500                 // on ajoute dans la requête parente toutes les requêtes filles du contexte
501                 if (context.getParentCounter() == this) {
502                     request.addChildHits(context);
503                 }
504                 request.addChildRequests(context.getChildRequestsExecutionsByRequestId());
505             }
506         }
507         // perf: on fait le reste hors du synchronized sur request
508         if (context != null) {
509             if (context.getParentCounter() == this) {
510                 final CounterRequestContext parentContext = context.getParentContext();
511                 if (parentContext == null) {
512                     // enlève du threadLocal le contexte que j'ai créé
513                     // si je suis le counter parent et s'il n'y a pas de contexte parent
514                     unbindContext();
515                 } else {
516                     // on ajoute une requête fille dans le contexte
517                     context.addChildRequest(this, aggregateRequestName, request.getId(), duration,
518                             systemError, responseSize);
519                     // et reporte les requêtes filles dans le contexte parent et rebinde celui-ci
520                     parentContext.closeChildContext();
521                     contextThreadLocal.set(parentContext);
522                 }
523             } else {
524                 // on ajoute une requête fille dans le contexte
525                 // (à priori il s'agit d'une requête sql)
526                 context.addChildRequest(this, aggregateRequestName, request.getId(), duration,
527                         systemError, responseSize);
528             }
529         }
530         if (systemErrorStackTrace != null) {
531             assert errorCounter;
532             synchronized (errors) {
533                 errors.addLast(new CounterError(requestName, systemErrorStackTrace));
534                 if (errors.size() > MAX_ERRORS_COUNT) {
535                     errors.removeFirst();
536                 }
537             }
538         }
539     }
540
541     public void addRequestForSystemError(String requestName, long duration, int cpuTime,
542             int allocatedKBytes, String stackTrace) {
543         // comme la méthode addRequest, cette méthode n'est pas synchronisée pour ne pas avoir
544         // de synchronisation globale à l'application sur cette instance d'objet
545         // ce qui pourrait faire une contention et des ralentissements,
546         // par contre on synchronise request et errors
547         assert requestName != null;
548         assert duration >= -1; // -1 pour le counter de log
549         assert cpuTime >= -1;
550         // on ne doit conserver les stackTraces que pour les compteurs d'erreurs et de logs plus limités en taille
551         // car sinon cela risquerait de donner des compteurs trop gros en mémoire et sur disque
552         assert errorCounter;
553         // le code ci-après suppose qu'il n'y a pas de contexte courant pour les erreurs systèmes
554         // contrairement à la méthode addRequest
555         assert contextThreadLocal.get() == null;
556         final String aggregateRequestName = getAggregateRequestName(requestName);
557         final CounterRequest request = getCounterRequestInternal(aggregateRequestName);
558         synchronized (request) {
559             request.addHit(duration, cpuTime, allocatedKBytes, true, stackTrace, -1);
560         }
561         synchronized (errors) {
562             errors.addLast(new CounterError(requestName, stackTrace));
563             if (errors.size() > MAX_ERRORS_COUNT) {
564                 errors.removeFirst();
565             }
566         }
567     }
568
569     public void addRumHit(String requestName, long networkTime, long domProcessing,
570             long pageRendering) {
571         assert HTTP_COUNTER_NAME.equals(name);
572         final String aggregateRequestName = getAggregateRequestName(requestName);
573         final CounterRequest request = requests.get(aggregateRequestName);
574         if (request != null) {
575             synchronized (request) {
576                 request.addRumHit(networkTime, domProcessing, pageRendering);
577             }
578         }
579     }
580
581     /**
582      * Retourne true si ce counter est un counter d'error
583      * (c'est-à-dire si son nom est "error""log" ou "job").
584      * @return boolean
585      */

586     public boolean isErrorCounter() {
587         return errorCounter;
588     }
589
590     /**
591      * Retourne true si ce counter est un counter de job
592      * (c'est-à-dire si son nom est "job").
593      * @return boolean
594      */

595     public boolean isJobCounter() {
596         return JOB_COUNTER_NAME.equals(name);
597     }
598
599     /**
600      * Retourne true si ce counter est un counter de jsp ou d'actions Struts
601      * (c'est-à-dire si son nom est "jsp").
602      * @return boolean
603      */

604     public boolean isJspOrStrutsCounter() {
605         return JSP_COUNTER_NAME.equals(name) || STRUTS_COUNTER_NAME.equals(name);
606     }
607
608     /**
609      * Retourne true si ce counter est un counter de "façades métiers" ou "business façades"
610      * (c'est-à-dire si son nom est "ejb""spring""guice" ou "services").
611      * @return boolean
612      */

613     public boolean isBusinessFacadeCounter() {
614         return "services".equals(name) || "ejb".equals(name) || "spring".equals(name)
615                 || "guice".equals(name);
616     }
617
618     public boolean isRequestIdFromThisCounter(String requestId) {
619         // cela marche car requestId commence par counter.getName() selon CounterRequest.buildId
620         return requestId.startsWith(getName());
621     }
622
623     private String getAggregateRequestName(String requestName) {
624         final String aggregateRequestName;
625         if (requestTransformPattern == null) {
626             aggregateRequestName = requestName;
627         } else {
628             // ce pattern optionnel permet de transformer la description de la requête
629             // pour supprimer des parties variables (identifiant d'objet par exemple)
630             // et pour permettre l'agrégation sur cette requête
631             final Matcher matcher = requestTransformPattern.matcher(requestName);
632             try {
633                 aggregateRequestName = matcher.replaceAll(TRANSFORM_REPLACEMENT);
634             } catch (final StackOverflowError e) {
635                 // regexp can throw StackOverflowError for (A|B)*
636                 // see https://github.com/javamelody/javamelody/issues/480
637                 LOG.warn(e.toString(), e);
638                 return requestName;
639             }
640         }
641         return aggregateRequestName;
642     }
643
644     void addRequestsAndErrors(Counter newCounter) {
645         assert getName().equals(newCounter.getName());
646
647         // Pour toutes les requêtes du compteur en paramètre,
648         // on ajoute les hits aux requêtes de ce compteur
649         // (utilisée dans serveur de collecte).
650
651         // Rq: cette méthode est thread-safe comme les autres méthodes dans cette classe,
652         // bien que cela ne soit à priori pas nécessaire telle qu'elle est utilisée dans CollectorServlet
653         for (final CounterRequest newRequest : newCounter.getRequests()) {
654             if (newRequest.getHits() > 0) {
655                 final CounterRequest request = getCounterRequestInternal(newRequest.getName());
656                 synchronized (request) {
657                     request.addHits(newRequest);
658                 }
659             }
660         }
661
662         int size = requests.size();
663         final int maxRequests = getMaxRequestsCount();
664         if (size > maxRequests) {
665             // Si le nombre de requêtes est supérieur à 10000 (sql non bindé par ex.),
666             // on essaye ici d'éviter de saturer la mémoire (et le disque dur)
667             // avec toutes ces requêtes différentes en éliminant celles ayant moins de 10 hits.
668             // (utile pour une agrégation par année dans PeriodCounterFactory par ex.)
669             // Mais inutile de le faire dans d'autres méthodes de Counter
670             // car ce serait mauvais pour les perfs, cela ne laisserait aucune chance
671             // à une nouvelle requête et car cela sera fait par la classe collector
672             for (final CounterRequest request : requests.values()) {
673                 if (request.getHits() < 10) {
674                     removeRequest(request.getName());
675                     size--;
676                     if (size <= maxRequests) {
677                         break;
678                     }
679                 }
680             }
681         }
682
683         if (isErrorCounter()) {
684             addErrors(newCounter.getErrors());
685         }
686     }
687
688     void addHits(CounterRequest counterRequest) {
689         if (counterRequest.getHits() > 0) {
690             // clone pour être thread-safe ici
691             final CounterRequest newRequest = counterRequest.clone();
692             final CounterRequest request = getCounterRequestInternal(newRequest.getName());
693             synchronized (request) {
694                 request.addHits(newRequest);
695             }
696         }
697     }
698
699     public void addErrors(List<CounterError> counterErrorList) {
700         assert errorCounter;
701         if (counterErrorList.isEmpty()) {
702             return;
703         }
704         synchronized (errors) {
705             if (!errors.isEmpty() && errors.get(0).getTime() > counterErrorList.get(0).getTime()) {
706                 // tant que faire se peut on les met à peu près dans l'ordre pour le sort ci après
707                 errors.addAll(0, counterErrorList);
708             } else {
709                 errors.addAll(counterErrorList);
710             }
711             if (errors.size() > 1) {
712                 // "sort" a les mêmes performances sur LinkedList que sur ArrayList car il y a un tableau intermédiaire
713                 // (selon Implementation Patterns, Kent Beck)
714                 Collections.sort(errors, new CounterErrorComparator());
715
716                 while (errors.size() > MAX_ERRORS_COUNT) {
717                     errors.removeFirst();
718                 }
719             }
720         }
721     }
722
723     void removeRequest(String requestName) {
724         assert requestName != null;
725         requests.remove(requestName);
726     }
727
728     /**
729      * Retourne l'objet {@link CounterRequest} correspondant au contexte de requête en cours en paramètre.
730      * @param context CounterRequestContext
731      * @return CounterRequest
732      */

733     public CounterRequest getCounterRequest(CounterRequestContext context) {
734         return getCounterRequestByName(context.getRequestName(), false);
735     }
736
737     /**
738      * Retourne l'objet {@link CounterRequest} correspondant au nom sans agrégation en paramètre.
739      * @param requestName Nom de la requête sans agrégation par requestTransformPattern
740      * @param saveRequestIfAbsent true except for current requests because the requestName may not be yet bestMatchingPattern
741      * @return CounterRequest
742      */

743     public CounterRequest getCounterRequestByName(String requestName, boolean saveRequestIfAbsent) {
744         // l'instance de CounterRequest retournée est clonée
745         // (nécessaire pour protéger la synchronisation interne du counter),
746         // son état peut donc être lu sans synchronisation
747         // mais toute modification de cet état ne sera pas conservée
748         final String aggregateRequestName = getAggregateRequestName(requestName);
749         final CounterRequest request = getCounterRequestInternal(aggregateRequestName,
750                 saveRequestIfAbsent);
751         synchronized (request) {
752             return request.clone();
753         }
754     }
755
756     private CounterRequest getCounterRequestInternal(String requestName) {
757         return getCounterRequestInternal(requestName, true);
758     }
759
760     private CounterRequest getCounterRequestInternal(String requestName,
761             boolean saveRequestIfAbsent) {
762         CounterRequest request = requests.get(requestName);
763         if (request == null) {
764             request = new CounterRequest(requestName, getName());
765             if (saveRequestIfAbsent) {
766                 // putIfAbsent a l'avantage d'être garanti atomique, même si ce n'est pas indispensable
767                 final CounterRequest precedentRequest = requests.putIfAbsent(requestName, request);
768                 if (precedentRequest != null) {
769                     request = precedentRequest;
770                 }
771             }
772         }
773         return request;
774     }
775
776     /**
777      * Retourne l'objet {@link CounterRequest} correspondant à l'id en paramètre ou null sinon.
778      * @param requestId Id de la requête
779      * @return CounterRequest
780      */

781     public CounterRequest getCounterRequestById(String requestId) {
782         if (isRequestIdFromThisCounter(requestId)) {
783             for (final CounterRequest request : requests.values()) {
784                 if (request.getId().equals(requestId)) {
785                     synchronized (request) {
786                         return request.clone();
787                     }
788                 }
789             }
790         }
791         return null;
792     }
793
794     /**
795      * Retourne le nombre de requêtes dans ce counter.
796      * @return int
797      */

798     public int getRequestsCount() {
799         return requests.size();
800     }
801
802     /**
803      * @return Liste des requêtes non triées,
804      *     la liste et ses objets peuvent être utilisés sans synchronized et sans crainte d'accès concurrents.
805      */

806     public List<CounterRequest> getRequests() {
807         // thread-safe :
808         // on crée une copie de la collection et on clone ici chaque CounterRequest de manière synchronisée
809         // de manière à ce que l'appelant n'ai pas à se préoccuper des synchronisations nécessaires
810         // Rq : l'Iterator sur ConcurrentHashMap.values() est garanti ne pas lancer ConcurrentModificationException
811         // même s'il y a des ajouts concurrents
812         final List<CounterRequest> result = new ArrayList<>(requests.size());
813         for (final CounterRequest request : requests.values()) {
814             // on synchronize sur request en cas d'ajout en parallèle d'un hit sur cette request
815             synchronized (request) {
816                 result.add(request.clone());
817             }
818         }
819         return result;
820     }
821
822     /**
823      * @return Liste des requêtes triées par durée cumulée décroissante,
824      *     la liste et ses objets peuvent être utilisés sans synchronized et sans crainte d'accès concurrents.
825      */

826     public List<CounterRequest> getOrderedRequests() {
827         final List<CounterRequest> requestList = getRequests();
828         if (requestList.size() > 1) {
829             Collections.sort(requestList, Collections.reverseOrder(new CounterRequestComparator()));
830         }
831         return requestList;
832     }
833
834     /**
835      * @return Liste des requêtes triées par hits décroissants,
836      *     la liste et ses objets peuvent être utilisés sans synchronized et sans crainte d'accès concurrents.
837      */

838     List<CounterRequest> getOrderedByHitsRequests() {
839         final List<CounterRequest> requestList = getRequests();
840         if (requestList.size() > 1) {
841             Collections.sort(requestList,
842                     Collections.reverseOrder(new CounterRequestByHitsComparator()));
843         }
844         return requestList;
845     }
846
847     /**
848      * @return Liste des contextes de requêtes courantes triées par durée écoulée décroissante,
849      *     la liste peut être utilisée sans synchronized et sans crainte d'accès concurrents,
850      *  toutefois les contextes ne sont pas actuellement clonés dans cette méthode.
851      */

852     List<CounterRequestContext> getOrderedRootCurrentContexts() {
853         final List<CounterRequestContext> contextList = new ArrayList<>(
854                 rootCurrentContextsByThreadId.size());
855         for (final CounterRequestContext rootCurrentContext : rootCurrentContextsByThreadId
856                 .values()) {
857             contextList.add(rootCurrentContext.clone());
858         }
859         if (contextList.size() > 1) {
860             Collections.sort(contextList, Collections
861                     .reverseOrder(new CounterRequestContextComparator(System.currentTimeMillis())));
862         }
863         return contextList;
864     }
865
866     /**
867      * @return Liste des erreurs triée par date croissante,
868      *     la liste et ses objets peuvent être utilisés sans synchronized et sans crainte d'accès concurrents.
869      */

870     public List<CounterError> getErrors() {
871         if (errors == null) {
872             return Collections.emptyList();
873         }
874         synchronized (errors) {
875             return new ArrayList<>(errors);
876         }
877     }
878
879     /**
880      * Retourne le nombre d'erreurs dans ce counter.
881      * @return int
882      */

883     public int getErrorsCount() {
884         if (errors == null) {
885             return 0;
886         }
887         synchronized (errors) {
888             return errors.size();
889         }
890     }
891
892     /**
893      * Purge les requêtes et erreurs puis positionne la date et heure de début à l'heure courante,
894      * mais sans toucher aux requêtes en cours pour qu'elles restent affichées,
895      * par exemple dans le serveur de collecte (#871).
896      */

897     public void clear() {
898         requests.clear();
899         if (errors != null) {
900             synchronized (errors) {
901                 errors.clear();
902             }
903         }
904         startDate = new Date();
905     }
906
907     /** {@inheritDoc} */
908     @Override
909     //CHECKSTYLE:OFF
910     public Counter clone() { // NOPMD
911         //CHECKSTYLE:ON
912         final Counter clone = new Counter(getName(), getStorageName(), getIconName(),
913                 getChildCounterName(), new ThreadLocal<CounterRequestContext>());
914         clone.application = getApplication();
915         clone.startDate = getStartDate();
916         clone.maxRequestsCount = getMaxRequestsCount();
917         clone.displayed = isDisplayed();
918         clone.requestTransformPattern = getRequestTransformPattern();
919         // on ne copie pas rootCurrentContextsByThreadId car on ne fournit pas les requêtes en cours
920         // qui sont très rapidement obsolètes au serveur de collecte (et sinon cela poserait la question
921         // des clones de parentCounter, de l'agrégation, de la synchro d'horloge pour la durée
922         // et des threadId pour la stack-trace),
923         // et on ne copie pas contextThreadLocal,
924         // et la méthode getRequests() clone les instances de CounterRequest
925         for (final CounterRequest request : getRequests()) {
926             clone.requests.put(request.getName(), request);
927         }
928         if (errors != null) {
929             clone.errors.addAll(getErrors());
930         }
931         return clone;
932     }
933
934     /**
935      * Enregistre le counter.
936      * @throws IOException e
937      */

938     void writeToFile() throws IOException {
939         // on clone le counter avant de le sérialiser pour ne pas avoir de problèmes de concurrences d'accès
940         final Counter counter = this.clone();
941         // on n'écrit pas rootCurrentContextsByThreadId en fichier
942         // puisque ces données ne seront plus vraies dans quelques secondes (clear pour être sûr ici)
943         counter.rootCurrentContextsByThreadId.clear();
944         estimatedMemorySize = new CounterStorage(counter).writeToFile();
945     }
946
947     /**
948      * Lecture du counter depuis son fichier.
949      * @throws IOException e
950      */

951     void readFromFile() throws IOException {
952         final Counter counter = new CounterStorage(this).readFromFile();
953         if (counter != null) {
954             final Counter newCounter = clone();
955             startDate = counter.getStartDate();
956             requests.clear();
957             for (final CounterRequest request : counter.getRequests()) {
958                 requests.put(request.getName(), request);
959             }
960             if (errors != null) {
961                 errors.clear();
962                 errors.addAll(counter.getErrors());
963             }
964             // on ajoute les nouvelles requêtes enregistrées avant de lire le fichier
965             // (par ex. les premières requêtes collectées par le serveur de collecte lors de l'initialisation)
966             addRequestsAndErrors(newCounter);
967         }
968     }
969
970     /** {@inheritDoc} */
971     @Override
972     public String toString() {
973         return getClass().getSimpleName() + "[application=" + getApplication() + ", name="
974                 + getName() + ", storageName=" + getStorageName() + ", startDate=" + getStartDate()
975                 + ", childCounterName=" + getChildCounterName() + ", " + requests.size()
976                 + " requests, " + (errors == null ? "" : errors.size() + " errors, ")
977                 + "maxRequestsCount=" + getMaxRequestsCount() + ", displayed=" + isDisplayed()
978                 + ']';
979     }
980 }
981