001/*
002 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003 *
004 * Copyright (c) 2006 Sun Microsystems Inc. All Rights Reserved
005 *
006 * The contents of this file are subject to the terms
007 * of the Common Development and Distribution License
008 * (the License). You may not use this file except in
009 * compliance with the License.
010 *
011 * You can obtain a copy of the License at
012 * https://opensso.dev.java.net/public/CDDLv1.0.html or
013 * opensso/legal/CDDLv1.0.txt
014 * See the License for the specific language governing
015 * permission and limitations under the License.
016 *
017 * When distributing Covered Code, include this CDDL
018 * Header Notice in each file and include the License file
019 * at opensso/legal/CDDLv1.0.txt.
020 * If applicable, add the following below the CDDL Header,
021 * with the fields enclosed by brackets [] replaced by
022 * your own identifying information:
023 * "Portions Copyrighted [year] [name of copyright owner]"
024 *
025 * $Id: Stats.java,v 1.5 2008/08/08 00:40:59 ww203982 Exp $
026 *
027 * Portions Copyrighted 2016 ForgeRock AS.
028 */
029
030package com.sun.identity.shared.stats;
031
032import static org.forgerock.openam.utils.Time.*;
033
034import com.sun.identity.common.SystemTimer;
035import com.sun.identity.shared.Constants;
036import com.sun.identity.shared.configuration.SystemPropertiesManager;
037import org.forgerock.util.thread.listener.ShutdownListener;
038import org.forgerock.util.thread.listener.ShutdownManager;
039
040import java.io.BufferedWriter;
041import java.io.File;
042import java.io.FileOutputStream;
043import java.io.IOException;
044import java.io.OutputStreamWriter;
045import java.io.PrintWriter;
046import java.io.StringWriter;
047import java.text.DateFormat;
048import java.text.SimpleDateFormat;
049import java.util.Date;
050import java.util.HashMap;
051import java.util.Map;
052import java.util.MissingResourceException;
053import java.util.ResourceBundle;
054
055// NOTE: Since JVM specs guarantee atomic access/updates to int variables
056// (actually all variables except double and long), the design consciously
057// avoids synchronized methods, particularly for message(). This is done to
058// reduce the performance overhead of synchronized message() when statistics
059// is disabled. This does not have serious side-effects other than an occasional
060// invocation of message() missing concurrent update of 'statsState'.
061
062/*******************************************************************************
063 * <p>
064 * Allows a uniform interface to statistics information in a uniform format.
065 * <code>Stats</code> supports different states of filing stats information:
066 * <code>OFF</code>, <code>FILE</code> and <code>CONSOLE</code>. <BR>
067 * <li> <code>OFF</code> statistics is turned off.
068 * <li> <code>FILE</code> statistics information is written to a file
069 * <li> <code>CONSOLE</code> statistics information is written on console
070 * <p>
071 * Stats service uses the property file, <code>AMConfig.properties</code>, to
072 * set the default stats level and the output directory where the stats files
073 * will be placed. The properties file is located (using
074 * {@link java.util.ResourceBundle} semantics) from one of the directories in
075 * the CLASSPATH.
076 * <p>
077 * The following keys are used to configure the Stats service. Possible values
078 * for the key 'state' are: off | off | file | console The key 'directory'
079 * specifies the output directory where the stats files will be created.
080 * 
081 * <blockquote>
082 * 
083 * <pre>
084 *  com.iplanet.services.stats.state
085 *  com.iplanet.services.stats.directory
086 * </pre>
087 * 
088 * </blockquote>
089 * 
090 * If there is an error reading or loading the properties, all the information
091 * is redirected to <code>System.out</code>
092 * 
093 * If these properties are changed, the server must be restarted for the changes
094 * to take effect.
095 * 
096 * <p>
097 * <b>NOTE:</b> Printing Statistics is an IO intensive operation and may hurt
098 * application performance when abused. Particularly, note that Java evaluates
099 * the arguments to <code>message()</code> and <code>warning()</code> even
100 * when statistics is turned off. It is recommended that the stats state be
101 * checked before invoking any <code>message()</code> or
102 * <code>warning()</code> methods to avoid unnecessary argument evaluation and
103 * to maximize application performance.
104 * </p>
105 * @supported.all.api
106 */
107public class Stats implements ShutdownListener {
108    /** flags the disabled stats state. */
109    public static final int OFF = 0;
110
111    /**
112     * Flags the state where all the statistic information is printed to a file
113     */
114    public static final int FILE = 1;
115
116    /**
117     * Flags the state where printing to a file is disabled. All printing is
118     * done on System.out.
119     */
120    public static final int CONSOLE = 2;
121
122    /**
123     * statsMap is a container of all active Stats objects. Log file name is the
124     * key and Stats is the value of this map.
125     */
126    private static Map statsMap = new HashMap();
127
128    /** serviceInitialized indicates if the service is already initialized. */
129    private static boolean serviceInitialized = false;
130
131    private static DateFormat dateFormat;
132
133    /**
134     * The default stats level for the entire service and the level that is used
135     * when a Stats object is first created and before its level is modified.
136     * Don't initialize the following two variables in a static
137     * initializer/block because other components may initialize Stats in their
138     * static initializers (as opposed to constructors or methods). The order of
139     * execution of static blocks is not guaranteed by JVM. So if we set the
140     * following two static variables to some default values here, then it will
141     * interfere with the execution of {@link #initService}.
142     */
143    private static String defaultStatsLevel;
144
145    private static String outputDirectory;
146
147    private final String statsName;
148
149    private PrintWriter statsFile = null;
150
151    private int statsState;
152
153    private static StatsRunner statsListeners = new StatsRunner();
154
155    /**
156     * Initializes the Stats service so that Stats objects can be created. At
157     * startup (when the first Stats object is ever created in a JVM), this
158     * method reads <code>AMConfig.properties</code> file (using
159     * {@link java.util.ResourceBundle} semantics) from one of the directories
160     * in the <code>CLASSPATH</code>, and loads the properties. It creates
161     * the stats directory. If all the directories in output dir don't have
162     * adequate permissions then the creation of the stats directory will fail
163     * and all the stats files will be located in the "current working
164     * directory" of the process running stats code. If there is an error
165     * reading or loading the properties, it will set the stats service to
166     * redirect all stats information to <code>System.out</code>
167     */
168    private static void initService() {
169        /*
170         * We will use the double-checked locking pattern. Rarely entered block.
171         * Push synchronization inside it. This is the first check.
172         */
173        if (!serviceInitialized) {
174            /*
175             * Only 1 thread at a time gets past the next point. Rarely executed
176             * synchronization statement and hence synchronization penalty is
177             * not paid every time this method is called.
178             */
179            synchronized (Stats.class) {
180                /*
181                 * If a second thread was waiting to get here, it will now find
182                 * that the instance has already been initialized, and it will
183                 * not re-initialize the instance variable. This is the (second)
184                 * double-check.
185                 */
186                if (!serviceInitialized) {
187                    dateFormat = new SimpleDateFormat(
188                            "MM/dd/yyyy hh:mm:ss:SSS a zzz");
189                    try {
190                        defaultStatsLevel = SystemPropertiesManager.get(
191                            Constants.SERVICES_STATS_STATE);
192                        outputDirectory = SystemPropertiesManager.get(
193                            Constants.SERVICES_STATS_DIRECTORY);
194                        ResourceBundle bundle = 
195                            com.sun.identity.shared.locale.Locale
196                                .getInstallResourceBundle("amUtilMsgs");
197                        if (outputDirectory != null) {
198                            File createDir = new File(outputDirectory);
199                            if (!createDir.exists()) {
200                                if (!createDir.mkdirs()) {
201                                    System.err.println(bundle.getString(
202                                           "com.iplanet.services.stats.nodir"));
203                                }
204                            }
205
206                        }
207                    } catch (MissingResourceException e) {
208                        System.err.println(e.getMessage());
209                        e.printStackTrace();
210
211                        // If there is any error in getting the level or
212                        // outputDirectory, defaultStatsLevel will be set to
213                        // ON so that output will go to
214                        // System.out
215
216                        defaultStatsLevel = "console";
217                        outputDirectory = null;
218                    } catch (SecurityException se) {
219                        System.err.println(se.getMessage());
220                    }
221                    
222                    SystemTimer.getTimer().schedule(statsListeners, new Date(((
223                            currentTimeMillis() +
224                                    statsListeners.getRunPeriod()) / 1000) * 1000));
225
226                    serviceInitialized = true;
227                }
228            }
229        }
230    }
231
232    /**
233     * This constructor takes as an argument the name of the stats file. The
234     * stats file is neither created nor opened until the first time
235     * <code>message()</code>, <code>warning()</code> or
236     * <code>error()</code> is invoked and the stats state is neither
237     * <code>OFF</code> nor <code>ON</code>.
238     * <p>
239     * <b>NOTE:</b>The recommended and preferred method to create Stats objects
240     * is <code>getInstance(String)</code>. This constructor may be
241     * deprecated in future.
242     * </p>
243     * 
244     * @param statsName name of the stats file to create or use
245     */
246    private Stats(String statsName) {
247        // Initialize the stats service the first time a Stats object is
248        // created.
249
250        initService();
251
252        // Now initialize this instance itself
253
254        this.statsName = statsName;
255        setStats(defaultStatsLevel);
256
257        synchronized (statsMap) {
258            // explicitly ignore any duplicate instances.
259            statsMap.put(statsName, this);
260        }
261        ShutdownManager shutdownMan = com.sun.identity.common.ShutdownManager.getInstance();
262        shutdownMan.addShutdownListener(this);
263
264    }
265
266    /**
267     * Returns an existing instance of Stats for the specified stats file or a
268     * new one if no such instance already exists. If a Stats object has to be
269     * created, its level is set to the level defined in the
270     * <code>AMConfig.properties</code> file. The level can be changed later
271     * by using {@link #setStats(int)} or {@link #setStats(String)}
272     * 
273     * @param statsName
274     *            name of statistic instance.
275     * @return an existing instance of Stats for the specified stats file.
276     */
277    public static synchronized Stats getInstance(String statsName) {
278        Stats statsObj = (Stats) statsMap.get(statsName);
279        if (statsObj == null) {
280            statsObj = new Stats(statsName);
281        }
282        return statsObj;
283    }
284
285    /**
286     * Checks if statistics is enabled.
287     * 
288     * <p>
289     * <b>NOTE:</b> It is recommended that <code>isEnabled()</code> be used
290     * instead of <code>isEnabled()</code> as the former is more intuitive.
291     * 
292     * @return <code>true</code> if statistics is enabled <code>false</code>
293     *         if statistics is disabled
294     * 
295     */
296    public boolean isEnabled() {
297        return (statsState > Stats.OFF);
298    }
299
300    /**
301     * Returns one of the 3 possible values.
302     * <ul>
303     * <li><code>Stats.OFF</code>
304     * <li><code>Stats.FILE</code>
305     * <li><code>Stats.CONSOLE</code>
306     * </ul>
307     * 
308     * @return state of Stats.
309     */
310    public int getState() {
311        return statsState;
312    }
313
314    /**
315     * Prints messages only when the stats state is either
316     * <code>Stats.FILE</code> or <code>Stats.CONSOLE</code>.
317     * 
318     * <p>
319     * <b>NOTE:</b> Printing Statistics is an IO intensive operation and may
320     * hurt application performance when abused. Particularly, note that Java
321     * evaluates arguments to <code>message()</code> even when statistics is
322     * turned off. So when the argument to this method involves the String
323     * concatenation operator '+' or any other method invocation,
324     * <code>isEnabled</code> <b>MUST</b> be used. It is recommended that the
325     * stats state be checked by invoking <code>isEnabled()</code> before
326     * invoking any <code>message()</code> methods to avoid unnecessary
327     * argument evaluation and maximize application performance.
328     * </p>
329     * 
330     * @param msg
331     *            message to be recorded.
332     */
333    public void record(String msg) {
334        if (statsState > Stats.OFF) {
335            formatAndWrite(null, msg);
336        }
337    }
338
339    private void formatAndWrite(String prefix, String msg) {
340        if (statsState == Stats.CONSOLE) {
341            if (msg != null) {
342                if (prefix == null) {
343                    System.out.println(msg);
344                } else {
345                    System.out.println(prefix + msg);
346                }
347            }
348            return;
349        }
350
351        // The default capacity of StringBuffer in StringWriter is 16, but we
352        // know for sure that the minimum header size is about 35. Hence, to
353        // avoid reallocation allocate at least 160 chars.
354
355        String serverInstance = System.getProperty("server.name");
356        StringWriter swriter = new StringWriter(160);
357        PrintWriter buf = new PrintWriter(swriter, true);
358        synchronized (dateFormat) {
359            buf.write(dateFormat.format(newDate()));
360        }
361        if ((serverInstance != null) && (serverInstance != "")) {
362            buf.write(": ");
363            buf.write("Server Instance: " + serverInstance);
364        }
365        buf.write(": ");
366        buf.write(Thread.currentThread().toString());
367        buf.write("\n");
368        if (prefix != null) {
369            buf.write(prefix);
370        }
371        if (msg != null) {
372            buf.write(msg);
373        }
374        buf.flush();
375
376        write(swriter.toString());
377    }
378
379    /**
380     * Actually writes to the stats file. If it cannot write to the stats file,
381     * it turn off statistics. The first time this method is invoked on a Stats
382     * object, that object's stats file is created/opened in the directory
383     * specified by the
384     * <code>property com.iplanet.services.stats.directory</code> in the
385     * properties file, <code>AMConfig.properties</code>.
386     */
387    private synchronized void write(String msg) {
388        try {
389            // statistics is enabled.
390            // First, see if the statsFile is already open. If not, open it now.
391
392            if (statsFile == null) {
393                // open file in append mode
394                FileOutputStream fos = new FileOutputStream(outputDirectory
395                        + File.separator + statsName, true);
396                statsFile = new PrintWriter(new BufferedWriter(
397                        new OutputStreamWriter(fos, "UTF-8")), true); 
398
399                statsFile.println("*********************************" +
400                        "*********************");
401            }
402
403            statsFile.println(msg);
404        } catch (IOException e) {
405            System.err.println(msg);
406
407            // turn off statistics because statsFile is not accessible
408            statsState = Stats.OFF;
409        }
410    }
411
412    /**
413     * Sets the stats capabilities based on the values of the
414     * <code>statsType</code> argument.
415     * 
416     * @param statsType
417     *            is any one of five possible values:
418     *            <p>
419     *            <code>Stats.OFF</code>
420     *            <p>
421     *            <p>
422     *            <code>Stats.FILE</code>
423     *            <p>
424     *            <p>
425     *            <code>Stats.CONSOLE</code>
426     *            <p>
427     */
428    public void setStats(int statsType) {
429        switch (statsType) {
430        case Stats.OFF:
431        case Stats.FILE:
432        case Stats.CONSOLE:
433            statsState = statsType;
434            break;
435
436        default:
437            // ignore invalid statsType values
438            break;
439        }
440    }
441
442    /**
443     * Sets the <code>stats</code> capabilities based on the values of the
444     * <code>statsType</code> argument.
445     * 
446     * @param statsType
447     *            is any one of the following possible values:
448     *            <p>
449     *            off - statistics is disabled
450     *            </p>
451     *            <p>
452     *            file - statistics are written to the stats file
453     *            <code>System.out</code>
454     *            </p>
455     *            <p>
456     *            console - statistics are written to the stats to the console
457     */
458    public void setStats(String statsType) {
459        if (statsType == null) {
460            return;
461        } else if (statsType.equalsIgnoreCase("console")) {
462            statsState = Stats.CONSOLE;
463        } else if (statsType.equalsIgnoreCase("file")) {
464            statsState = Stats.FILE;
465        } else if (statsType.equalsIgnoreCase("off")) {
466            statsState = Stats.OFF;
467        } else if (statsType.equals("*")) {
468            statsState = Stats.CONSOLE;
469        } else {
470            if (statsType.endsWith("*")) {
471                statsType = statsType.substring(0, statsType.length() - 1);
472            }
473            if (statsName.startsWith(statsType)) {
474                statsState = Stats.CONSOLE;
475            }
476        }
477    }
478
479    /**
480     * Destroys the stats object, closes the stats file and releases any system
481     * resources. Note that the stats file will remain open until
482     * <code>destroy()</code> is invoked. To conserve file resources, you
483     * should invoke <code>destroy()</code> explicitly rather than wait for
484     * the garbage collector to clean up.
485     * 
486     * <p>
487     * If this object is accessed after <code>destroy()</code> has been
488     * invoked, the results are undefined.
489     * </p>
490     */
491    public void destroy() {
492        finalize();
493    }
494    
495    public void shutdown() {
496        finalize();
497    }
498
499    /** Flushes and then closes the stats file. */
500    protected void finalize() {
501        synchronized (statsMap) {
502            statsMap.remove(statsName);
503        }
504
505        synchronized (this) {
506            if (statsFile == null) {
507                return;
508            }
509
510            statsState = Stats.OFF;
511            statsFile.flush();
512            statsFile.close();
513            statsFile = null;
514        }
515    }
516
517    public void addStatsListener(StatsListener listener) {
518        statsListeners.addElement(listener);
519    }
520}