001package org.javasimon.utils;
002
003import java.text.DecimalFormat;
004import java.text.DecimalFormatSymbols;
005import java.text.SimpleDateFormat;
006import java.util.Date;
007import java.util.Locale;
008import java.util.concurrent.Callable;
009import java.util.regex.Pattern;
010
011import org.javasimon.Counter;
012import org.javasimon.Manager;
013import org.javasimon.Simon;
014import org.javasimon.SimonException;
015import org.javasimon.SimonFilter;
016import org.javasimon.SimonManager;
017import org.javasimon.Split;
018import org.javasimon.Stopwatch;
019
020/**
021 * SimonUtils provides static utility methods.
022 * <p/>
023 * <h3>Human readable outputs</h3>
024 * Both {@link org.javasimon.Stopwatch} and {@link org.javasimon.Counter} provide human readable
025 * {@code toString} outputs. All nanosecond values are converted into few valid digits with
026 * proper unit (ns, us, ms, s) - this is done via method {@link #presentNanoTime(long)}.
027 * Max/min counter values are checked for undefined state (max/min long value is converted
028 * to string "undef") - via method {@link #presentMinMaxCount(long)}.
029 * <p/>
030 * <h3>Aggregation utilities</h3>
031 * It is possible to sum up (aggregate) values for a subtree for a particular Simon type using
032 * {@link #calculateCounterAggregate(org.javasimon.Simon)} or {@link #calculateStopwatchAggregate(org.javasimon.Simon)}.
033 * Methods come also in versions allowing to filter by {@link org.javasimon.SimonFilter}.
034 * <p/>
035 * <h3>Simon tree operations</h3>
036 * For various debug purposes there is a method that creates string displaying the whole Simon sub-tree.
037 * Here is example code that initializes two random Simons and prints the whole Simon hierarchy
038 * (note that the method can be used to obtain any sub-tree of the hierarchy):
039 * <pre>
040 * Split split = SimonManager.getStopwatch("com.my.other.stopwatch").start();
041 * SimonManager.getCounter("com.my.counter").setState(SimonState.DISABLED, false);
042 * split.stop();
043 * System.out.println(SimonUtils.simonTreeString(SimonManager.getRootSimon()));</pre>
044 * And the output is:
045 * <pre>
046 * (+): Unknown Simon:  [ ENABLED]
047 *   com(+): Unknown Simon:  [com INHERIT]
048 *     my(+): Unknown Simon:  [com.my INHERIT]
049 *       other(+): Unknown Simon:  [com.my.other INHERIT]
050 *         stopwatch(+): Simon Stopwatch: total 24.2 ms, counter 1, max 24.2 ms, min 24.2 ms, mean 24.2 ms [com.my.other.stopwatch INHERIT]
051 *       counter(-): Simon Counter: counter=0, max=undef, min=undef [com.my.counter DISABLED]</pre>
052 * Notice +/- signs in parenthesis that displays effective Simon state (enabled/disabled), further
053 * details are printed via each Simon's {@code toString} method.
054 *
055 * <h3>Other utilities</h3>
056 * It is possible to obtain "local name" of the Simon (behind the last dot) via {@link #localName(String)}
057 * or check if the name is valid Simon name via {@link #checkName(String)}.
058 *
059 * @author <a href="mailto:virgo47@gmail.com">Richard "Virgo" Richter</a>
060 * @author Radovan Sninsky
061 * @since 1.0
062 */
063@SuppressWarnings({"UnusedDeclaration"})
064public final class SimonUtils {
065
066        /** Regex character class content for {@link #NAME_PATTERN}. */
067        public static final String NAME_PATTERN_CHAR_CLASS_CONTENT = "-_\\[\\]A-Za-z0-9.,@$%)(<>";
068
069        /** Regex pattern for Simon names. */
070        public static final Pattern NAME_PATTERN = Pattern.compile("[" + NAME_PATTERN_CHAR_CLASS_CONTENT + "]+");
071
072        /**
073         * Allowed Simon name characters.
074         *
075         * @since 2.3
076         */
077        public static final String ALLOWED_CHARS = "-_[]ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,@$%()<>";
078
079        /**
080         * Name of the attribute where manager is searched for in an appropriate context - used for Spring/JavaEE/console integration.
081         * While the name can be used in any context supporting named attributes, it is primarily aimed for ServletContext. Manager can
082         * be shared in ServletContext by {@code SimonWebConfigurationBean} (Spring module) and then picked up by {@code SimonServletFilter}
083         * (JavaEE module) and {@code SimonConsoleFilter} (Embeddable console). If no manager is found in the attribute of the context,
084         * it is expected that components will use default {@link org.javasimon.SimonManager} instead.
085         *
086         * @since 3.2
087         */
088        public static final String MANAGER_SERVLET_CTX_ATTRIBUTE = "manager-servlet-ctx-attribute";
089
090        private static final SimpleDateFormat TIMESTAMP_FORMAT = new SimpleDateFormat("yyMMdd-HHmmss.SSS");
091
092        private static final int UNIT_PREFIX_FACTOR = 1000;
093        private static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = new DecimalFormatSymbols(Locale.US);
094        private static final int TEN = 10;
095        private static final DecimalFormat UNDER_TEN_FORMAT = new DecimalFormat("0.00", DECIMAL_FORMAT_SYMBOLS);
096        private static final int HUNDRED = 100;
097        private static final DecimalFormat UNDER_HUNDRED_FORMAT = new DecimalFormat("00.0", DECIMAL_FORMAT_SYMBOLS);
098        private static final DecimalFormat DEFAULT_FORMAT = new DecimalFormat("000", DECIMAL_FORMAT_SYMBOLS);
099
100        private static final String UNDEF_STRING = "undef";
101        private static final int CLIENT_CODE_STACK_INDEX;
102
103        static {
104                // Finds out the index of "this code" in the returned stack trace - funny but it differs in JDK 1.5 and 1.6
105                int i = 1;
106                for (StackTraceElement ste : Thread.currentThread().getStackTrace()) {
107                        i++;
108                        if (ste.getClassName().equals(SimonUtils.class.getName())) {
109                                break;
110                        }
111                }
112                CLIENT_CODE_STACK_INDEX = i;
113        }
114
115        private SimonUtils() {
116                throw new AssertionError();
117        }
118
119        /**
120         * Returns nano-time in human readable form with unit. Number is always from 10 to 9999
121         * except for seconds that are the biggest unit used. In case of undefined value ({@link Long#MAX_VALUE} or
122         * {@link Long#MIN_VALUE}) string {@link #UNDEF_STRING} is returned, so it can be used for min/max values as well.
123         *
124         * @param nanos time in nanoseconds
125         * @return human readable time string
126         */
127        public static String presentNanoTime(long nanos) {
128                if (nanos == Long.MAX_VALUE || nanos == Long.MIN_VALUE) {
129                        return UNDEF_STRING;
130                }
131                return presentNanoTimePrivate((double) nanos);
132        }
133
134        /**
135         * Returns nano-time in human readable form with unit. Number is always from 10 to 9999
136         * except for seconds that are the biggest unit used.
137         *
138         * @param nanos time in nanoseconds
139         * @return human readable time string
140         */
141        public static String presentNanoTime(double nanos) {
142                if (nanos == Double.MAX_VALUE) {
143                        return UNDEF_STRING;
144                }
145                return presentNanoTimePrivate(nanos);
146        }
147
148        private static String presentNanoTimePrivate(double time) {
149                if (Math.abs(time) < 1d) {
150                        return "0";
151                }
152
153                if (time < UNIT_PREFIX_FACTOR) {
154                        return ((long) time) + " ns";
155                }
156
157                time /= UNIT_PREFIX_FACTOR;
158                if (time < UNIT_PREFIX_FACTOR) {
159                        return formatTime(time, " us");
160                }
161
162                time /= UNIT_PREFIX_FACTOR;
163                if (time < UNIT_PREFIX_FACTOR) {
164                        return formatTime(time, " ms");
165                }
166
167                time /= UNIT_PREFIX_FACTOR;
168                return formatTime(time, " s");
169        }
170
171        private static synchronized String formatTime(double time, String unit) {
172                if (time < TEN) {
173                        return UNDER_TEN_FORMAT.format(time) + unit;
174                }
175                if (time < HUNDRED) {
176                        return UNDER_HUNDRED_FORMAT.format(time) + unit;
177                }
178                return DEFAULT_FORMAT.format(time) + unit;
179        }
180
181        /**
182         * Returns timestamp in human readable (yet condensed) form "yyMMdd-HHmmss.SSS".
183         *
184         * @param timestamp timestamp in millis
185         * @return timestamp as a human readable string
186         */
187        public static String presentTimestamp(long timestamp) {
188                if (timestamp == 0) {
189                        return UNDEF_STRING;
190                }
191                synchronized (TIMESTAMP_FORMAT) {
192                        return TIMESTAMP_FORMAT.format(new Date(timestamp));
193                }
194        }
195
196        /**
197         * Returns min/max counter values in human readable form - if the value is max or min long value
198         * it is considered unused and string "undef" is returned.
199         *
200         * @param minmax counter extreme value
201         * @return counter value or "undef" if counter contains {@code Long.MIN_VALUE} or {@code Long.MAX_VALUE}
202         */
203        public static String presentMinMaxCount(long minmax) {
204                if (minmax == Long.MAX_VALUE || minmax == Long.MIN_VALUE) {
205                        return UNDEF_STRING;
206                }
207                return String.valueOf(minmax);
208        }
209
210        /**
211         * Returns multi-line string containing Simon tree starting with the specified Simon.
212         * Root Simon can be used to obtain tree with all Simons. Returns {@code null} for
213         * input value of null or for NullSimon or any Simon with name equal to null (anonymous
214         * Simons) - this is also the case when the Manager is disabled and tree for its root
215         * Simon is requested.
216         *
217         * @param simon root Simon of the output tree
218         * @return string containing the tree or null if the Simon is null Simon
219         */
220        public static String simonTreeString(Simon simon) {
221                if (simon == null || simon.getName() == null) {
222                        return null;
223                }
224                StringBuilder sb = new StringBuilder();
225                printSimonTree(0, simon, sb);
226                return sb.toString();
227        }
228
229        private static void printSimonTree(int level, Simon simon, StringBuilder sb) {
230                printSimon(level, simon, sb);
231                for (Simon child : simon.getChildren()) {
232                        printSimonTree(level + 1, child, sb);
233                }
234        }
235
236        private static void printSimon(int level, Simon simon, StringBuilder sb) {
237                for (int i = 0; i < level; i++) {
238                        sb.append("  ");
239                }
240                sb.append(localName(simon.getName()))
241                        .append('(')
242                        .append(simon.isEnabled() ? '+' : '-')
243                        .append("): ")
244                        .append(simon.toString())
245                        .append('\n');
246        }
247
248        /**
249         * Returns last part of Simon name - local name.
250         *
251         * @param name full Simon name
252         * @return string containing local name
253         */
254        public static String localName(String name) {
255                int ix = name.lastIndexOf(Manager.HIERARCHY_DELIMITER);
256                if (ix == -1) {
257                        return name;
258                }
259                return name.substring(ix + 1);
260        }
261
262        /**
263         * Checks if the input string is correct Simon name. Simon name is checked against
264         * public {@link #NAME_PATTERN}.
265         *
266         * @param name checked Simon name
267         * @return true if the string is proper Simon name
268         */
269        public static boolean checkName(String name) {
270                return NAME_PATTERN.matcher(name).matches();
271        }
272
273        /**
274         * Checks if the input string is correct Simon name and throws {@link SimonException} otherwise.
275         *
276         * @param name checked Simon name
277         * @throws SimonException if the string is not proper Simon name
278         */
279        public static void validateSimonName(String name) {
280                if (!checkName(name)) {
281                        throw new SimonException("Simon name must match following pattern: '" + SimonUtils.NAME_PATTERN.pattern() + "', used name: " + name);
282                }
283        }
284
285        /**
286         * Autogenerates name for the Simon using the fully-qualified class name.
287         * <i>This method has inherent performance penalty (getting the stack-trace) so it is recommended to store
288         * pre-generated name. (Whole get-start-stop cycle is roughly 30-times slower with this method included.)</i>
289         *
290         * @param suffix name suffix for eventual Simon discrimination
291         * @return autogenerated name for Simon
292         * @since 3.1
293         */
294        public static String generateNameForClass(String suffix) {
295                return generatePrivate(suffix, false);
296        }
297
298        /**
299         * Autogenerates name for the Simon using the fully qualified class name and the method name.
300         * <i>This method has inherent performance penalty (getting the stack-trace) so it is recommended to store
301         * pre-generated name. (Whole get-start-stop cycle is roughly 30-times slower with this method included.)</i>
302         *
303         * @param suffix name suffix for eventual Simon discrimination
304         * @return autogenerated name for Simon
305         * @since 3.1
306         */
307        public static String generateNameForClassAndMethod(String suffix) {
308                return generatePrivate(suffix, true);
309        }
310
311        // method is extracted, so the stack trace index is always right
312        private static String generatePrivate(String suffix, boolean includeMethodName) {
313                StackTraceElement stackElement = Thread.currentThread().getStackTrace()[CLIENT_CODE_STACK_INDEX];
314                StringBuilder nameBuilder = new StringBuilder(stackElement.getClassName());
315                if (includeMethodName) {
316                        nameBuilder.append('.').append(stackElement.getMethodName());
317                }
318                if (suffix != null) {
319                        nameBuilder.append(suffix);
320                }
321                return nameBuilder.toString();
322        }
323
324        /**
325         * Autogenerates name for the Simon using the class name and the method name.
326         *
327         * @return autogenerated name for Simon
328         * @see #generateNameForClassAndMethod(String)
329         */
330        public static String generateName() {
331                return generatePrivate(null, true);
332        }
333
334        /**
335         * Calls a block of code with stopwatch around and returns result.
336         *
337         * @param name name of the Stopwatch
338         * @param callable callable block of code
339         * @param <T> return type
340         * @return whatever block of code returns
341         * @throws Exception whatever block of code throws
342         * @since 3.0
343         */
344        public static <T> T doWithStopwatch(String name, Callable<T> callable) throws Exception {
345                try (Split split = SimonManager.getStopwatch(name).start()) {
346                        return callable.call();
347                }
348        }
349
350        /**
351         * Calls a block of code with stopwatch around, can not return any result or throw an exception
352         * (use {@link #doWithStopwatch(String, java.util.concurrent.Callable)} instead).
353         *
354         * @param name name of the Stopwatch
355         * @param runnable wrapped block of code
356         * @since 3.0
357         */
358        public static void doWithStopwatch(String name, Runnable runnable) {
359                try (Split split = SimonManager.getStopwatch(name).start()) {
360                        runnable.run();
361                }
362        }
363
364        private static final String SHRINKED_STRING = "...";
365
366        /**
367         * Shrinks the middle of the input string if it is too long, so it does not exceed limitTo.
368         *
369         * @since 3.2
370         */
371        public static String compact(String input, int limitTo) {
372                if (input == null || input.length() <= limitTo) {
373                        return input;
374                }
375
376                int headLength = limitTo / 2;
377                int tailLength = limitTo - SHRINKED_STRING.length() - headLength;
378                if (tailLength < 0) {
379                        tailLength = 1;
380                }
381
382                return input.substring(0, headLength) + SHRINKED_STRING + input.substring(input.length() - tailLength);
383        }
384
385        /**
386         * Aggregate statistics from all stopwatches in hierarchy of simons.
387         *
388         * @param simon root of the hierarchy of simons for which statistics will be aggregated
389         * @return aggregated statistics
390         * @since 3.5
391         */
392        public static StopwatchAggregate calculateStopwatchAggregate(Simon simon) {
393                return calculateStopwatchAggregate(simon, null);
394        }
395
396        /**
397         * Aggregate statistics from all stopwatches in hierarchy that pass specified filter. Filter is applied
398         * to all simons in the hierarchy of all types. If a simon is rejected by filter its children are not considered.
399         * Simons are aggregated in the top-bottom fashion, i.e. parent simons are aggregated before children.
400         *
401         * @param simon root of the hierarchy of simons for which statistics will be aggregated
402         * @param filter filter to select subsets of simons to aggregate
403         * @return aggregates statistics
404         * @since 3.5
405         */
406        public static StopwatchAggregate calculateStopwatchAggregate(Simon simon, SimonFilter filter) {
407                StopwatchAggregate stopwatchAggregate = new StopwatchAggregate();
408                aggregateStopwatches(stopwatchAggregate, simon,
409                        filter != null ? filter : SimonFilter.ACCEPT_ALL_FILTER);
410
411                return stopwatchAggregate;
412        }
413
414        private static void aggregateStopwatches(StopwatchAggregate aggregate, Simon simon, SimonFilter filter) {
415                if (filter.accept(simon)) {
416                        if (simon instanceof Stopwatch) {
417                                Stopwatch stopwatch = (Stopwatch) simon;
418                                aggregate.addSample(stopwatch.sample());
419                        }
420
421                        for (Simon child : simon.getChildren()) {
422                                aggregateStopwatches(aggregate, child, filter);
423                        }
424                }
425        }
426
427        /**
428         * Aggregate statistics from all counters in hierarchy of simons.
429         *
430         * @param simon root of the hierarchy of simons for which statistics will be aggregated
431         * @return aggregated statistics
432         * @since 3.5
433         */
434        public static CounterAggregate calculateCounterAggregate(Simon simon) {
435                return calculateCounterAggregate(simon, null);
436        }
437
438        /**
439         * Aggregate statistics from all counters in hierarchy that pass specified filter. Filter is applied
440         * to all simons in the hierarchy of all types. If a simon is rejected by filter its children are not considered.
441         * Simons are aggregated in the top-bottom fashion, i.e. parent simons are aggregated before children.
442         *
443         * @param simon root of the hierarchy of simons for which statistics will be aggregated
444         * @param filter filter to select subsets of simons to aggregate
445         * @return aggregates statistics
446         * @since 3.5
447         */
448        public static CounterAggregate calculateCounterAggregate(Simon simon, SimonFilter filter) {
449                CounterAggregate aggregate = new CounterAggregate();
450                aggregateCounters(aggregate, simon,
451                        filter != null ? filter : SimonFilter.ACCEPT_ALL_FILTER);
452
453                return aggregate;
454        }
455
456        private static void aggregateCounters(CounterAggregate aggregate, Simon simon, SimonFilter filter) {
457                if (filter.accept(simon)) {
458                        if (simon instanceof Counter) {
459                                Counter counter = (Counter) simon;
460                                aggregate.addSample(counter.sample());
461                        }
462
463                        for (Simon child : simon.getChildren()) {
464                                aggregateCounters(aggregate, child, filter);
465                        }
466                }
467        }
468}