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