001    /* JAPI - (Yet another (hopefully) useful) Java API
002     *
003     * Copyright (C) 2004-2006 Christian Hujer
004     *
005     * This program is free software; you can redistribute it and/or
006     * modify it under the terms of the GNU General Public License as
007     * published by the Free Software Foundation; either version 2 of the
008     * License, or (at your option) any later version.
009     *
010     * This program is distributed in the hope that it will be useful, but
011     * WITHOUT ANY WARRANTY; without even the implied warranty of
012     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013     * General Public License for more details.
014     *
015     * You should have received a copy of the GNU General Public License
016     * along with this program; if not, write to the Free Software
017     * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
018     * 02111-1307, USA.
019     */
020    
021    package net.sf.japi.swing;
022    
023    import java.awt.Component;
024    import java.awt.Dimension;
025    import java.awt.Font;
026    import java.util.ArrayList;
027    import java.util.List;
028    import java.util.MissingResourceException;
029    import java.util.Random;
030    import java.util.ResourceBundle;
031    import static java.util.ResourceBundle.getBundle;
032    import java.util.prefs.Preferences;
033    import static java.util.prefs.Preferences.userNodeForPackage;
034    import java.io.InputStream;
035    import java.io.BufferedReader;
036    import java.io.InputStreamReader;
037    import java.io.UnsupportedEncodingException;
038    import java.security.PrivilegedAction;
039    import java.security.AccessController;
040    import javax.swing.Action;
041    import static javax.swing.Action.ACCELERATOR_KEY;
042    import javax.swing.JButton;
043    import javax.swing.JCheckBox;
044    import javax.swing.JDialog;
045    import javax.swing.JEditorPane;
046    import javax.swing.JLabel;
047    import javax.swing.JOptionPane;
048    import javax.swing.JScrollPane;
049    import javax.swing.KeyStroke;
050    import static javax.swing.SwingConstants.TRAILING;
051    import org.jetbrains.annotations.Nullable;
052    import static net.sf.japi.swing.ActionFactory.ACCELERATOR_KEY_2;
053    import static net.sf.japi.swing.IconManager.getDefaultIconManager;
054    
055    /** Class that manages tips of the day.
056     * The tips of the day are read from a property file.
057     * The name of the property file is tried to retrieve in exactly the following order:
058     * <ol>
059     *  <li>The System Property <code>net.sf.japi.swing.tod</code> is queried and taken as a ResourceBundle base name.</li>
060     *  <li>The Service file <code>META-INF/services/net.sf.japi.swing.tod</code> is read and its first line taken as a ResourceBundle base name.</li>
061     * </ol>
062     * If both fails, the behaviour is undefined.
063     * <p/>
064     * Setting the ResourceBundle for the TipOfTheDayManager via services is the preferred way, because you do not need any additional coding apart from
065     * invoking the TipOfTheDayManager somewhere at startup.
066     * <p />
067     * The format of that property file follows the normal Java Properties convention, with the property keys being numbered, starting at "tod.text.1".
068     * Example:
069     * <pre># Tip Of The Days, English Version
070     * tod.text.1=&lt;html&gt;For analysis with other tools you can export the symbol map to XML, MS-Excel and CSV.
071     * tod.text.2=&lt;html&gt;For analysis with other tools you can export the mapping map to XML, MS-Excel and CSV.
072     * tod.text.3=&lt;html&gt;The supported map file formats are: Intel, GCC and MSVC.
073     * </pre>
074     * @fixme The preferences properties lastTipOfTheDayNumber and showTipOfTheDayAtStartup are stored in the wrong package, they must be stored in the client package instead of this package
075     * @todo Allow parametrization of properties, e.g. via a String sequence like <code>${property.name}</code>, which should then be looked up using
076     * a defined scheme from one or perhaps more definable {@link ActionFactory ActionFactories}.
077     * @author <a href="mailto:chris@riedquat.de">Christian Hujer</a>
078     * @serial exclude
079     */
080    public final class TipOfTheDayManager extends JOptionPane {
081    
082        /** Action Factory. */
083        private static final ActionFactory ACTION_FACTORY = ActionFactory.getFactory("net.sf.japi.swing");
084    
085        /** Random number generator for random tods. */
086        private static final Random RND = new Random();
087    
088        /** Preferences. */
089        private static final Preferences PREFS = userNodeForPackage(TipOfTheDayManager.class);
090    
091        /** The Action keys used for accelerators. */
092        private static final String[] ACCELERATOR_KEYS = new String[] { ACCELERATOR_KEY, ACCELERATOR_KEY_2 };
093    
094        /** The static instance. */
095        private static final TipOfTheDayManager INSTANCE = new TipOfTheDayManager();
096    
097        /** JButton for close. */
098        private JButton closeButton;
099    
100        /** JDialog. */
101        private JDialog dialog;
102    
103        /** JCheckBox for showing at startup. */
104        private JCheckBox showAtStartup = new JCheckBox(ACTION_FACTORY.createAction(false, "todShowAtStartup", null));
105    
106        /** The JLabel showing the current number of the tod. */
107        private JLabel currentTodIndex = new JLabel();
108    
109        /** The JEditorPane displaying the tod text. */
110        private JEditorPane todText = new JEditorPane("text/html", "");
111    
112        /** List with tod texts. */
113        private List<String> todTexts = new ArrayList<String>();
114    
115        /** Number of current Tip of the day.
116         * In visible state, the index must range between 0 and the number of tods available - 1, inclusive.
117         */
118        private int todIndex;
119    
120        /** Show Tip Of The Day at startup.
121         * This method is only a proxy for show(Component) but looks at user preferences.
122         * If the user chose not to see TipOfTheDays this method simply returns.
123         * @param parentComponent  the parent component of this dialog.
124         */
125        public static void showAtStartup(final Component parentComponent) {
126            if (PREFS.getBoolean("showTipOfTheDayAtStartup", true)) {
127                show(parentComponent);
128            }
129        }
130    
131        /** Show a Tip Of The Day.
132         * @param parentComponent  the parent component of this dialog.
133         */
134        public static void show(final Component parentComponent) {
135            if (INSTANCE.dialog == null) {
136                INSTANCE.dialog = INSTANCE.createDialog(parentComponent, ACTION_FACTORY.getString("tipOfTheDay.windowTitle"));
137            }
138            INSTANCE.dialog.getRootPane().setDefaultButton(INSTANCE.closeButton);
139            INSTANCE.dialog.setModal(false);
140            INSTANCE.dialog.setResizable(true);
141            INSTANCE.dialog.setVisible(true);
142            INSTANCE.closeButton.requestFocusInWindow();
143        }
144    
145        /** Finds the bundle name.
146         * @return bundle name
147         */
148        private static String getBundleName() {
149            String bundleName = System.getProperty("net.sf.japi.swing.tod");
150            if (bundleName == null) {
151                bundleName = getServiceValue(getClassLoader());
152            }
153            return bundleName;
154        }
155    
156        /** This method attempts to return the first line of the resource META_INF/services/net.sf.japi.swing.tod from the provided ClassLoader.
157         * @param classLoader ClassLoader, may not be <code>null</code>.
158         * @return first line of resource, or <code>null</code>
159         */
160        @Nullable private static String getServiceValue(final ClassLoader classLoader) {
161            BufferedReader rd = null;
162            try {
163                final String serviceId = "META-INF/services/net.sf.japi.swing.tod";
164                final InputStream in = getResourceAsStream(classLoader, serviceId);
165                if (in != null) {
166                    try {
167                        rd = new BufferedReader(new InputStreamReader(in, "UTF-8"));
168                    } catch (final UnsupportedEncodingException e) {
169                        rd = new BufferedReader(new InputStreamReader(in));
170                    }
171                    return rd.readLine();
172                }
173            } catch (final Exception e) {
174                /* ignore. */
175            } finally {
176                try { rd.close(); } catch (final Exception e) { /* ignore */ } finally { rd = null; }
177            }
178            return null;
179        }
180    
181        /** Get a resource from a classloader as stream.
182         * @param classLoader ClassLoader to get resource from
183         * @param serviceId service file to get stream from
184         * @return stream for resource <var>serviceId</var> in <var>classLoader</var>
185         */
186        private static InputStream getResourceAsStream(final ClassLoader classLoader, final String serviceId) {
187            return AccessController.doPrivileged(new PrivilegedAction<InputStream>() {
188    
189                /** {@inheritDoc} */
190                public InputStream run() {
191                    return classLoader == null ?
192                        ClassLoader.getSystemResourceAsStream(serviceId) :
193                        classLoader.getResourceAsStream(serviceId);
194                }
195    
196            });
197        }
198    
199        /** Get the ClassLoader.
200         * @return ClassLoader
201         */
202        private static ClassLoader getClassLoader() {
203            try {
204                final ClassLoader contextClassLoader = getContextClassLoader();
205                if (contextClassLoader != null) {
206                    return contextClassLoader;
207                }
208            } catch (final Exception e) {
209                /* ignore */
210            }
211            return TipOfTheDayManager.class.getClassLoader();
212        }
213    
214        /** Get the context ClassLoader.
215         * @return context ClassLoader
216         */
217        private static ClassLoader getContextClassLoader() {
218            return AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
219    
220                /** {@inheritDoc} */
221                @Nullable public ClassLoader run() {
222                    try {
223                        return Thread.currentThread().getContextClassLoader();
224                    } catch (final SecurityException ex) {
225                        return null;
226                    }
227                }
228    
229            });
230        }
231    
232        /** Constructor. */
233        private TipOfTheDayManager() {
234            final String[] keys = { "todPrev", "todRand", "todNext", "todClose" };
235            final Action[] actions = new Action[keys.length];
236            final Object[] newOptions = new Object[keys.length];
237            for (int i = 0; i < keys.length; i++) {
238                actions[i] = ACTION_FACTORY.createAction(false, keys[i], this);
239                newOptions[i] = new JButton(actions[i]);
240                if ("todClose".equals(keys[i])) {
241                    closeButton = (JButton) newOptions[i];
242                }
243                for (final String key : ACCELERATOR_KEYS) {
244                    final KeyStroke ks = (KeyStroke) actions[i].getValue(key);
245                    if (ks != null) {
246                        getInputMap(WHEN_IN_FOCUSED_WINDOW).put(ks, keys[i]);
247                        getActionMap().put(keys[i], actions[i]);
248                    }
249                }
250            }
251            final Dimension size = new Dimension(512, 128);
252            showAtStartup.setSelected(PREFS.getBoolean("showTipOfTheDayAtStartup", true));
253            final JScrollPane todTextScroller = new JScrollPane(todText);
254            todTextScroller.setMinimumSize(size);
255            todTextScroller.setPreferredSize(size);
256            todTextScroller.setFocusable(true);
257            todTextScroller.setAutoscrolls(true);
258            todText.setEditable(false);
259            todText.setRequestFocusEnabled(false);
260            currentTodIndex.setHorizontalAlignment(TRAILING);
261            loadTodTexts();
262            setTodIndex(PREFS.getInt("lastTipOfTheDayNumber", -1) + 1);
263            setIcon(getDefaultIconManager().getIcon(ACTION_FACTORY.getString("tipOfTheDay.icon")));
264            final JLabel heading = new JLabel(ACTION_FACTORY.getString("todHeading"));
265            final Font oldFont = heading.getFont();
266            heading.setFont(oldFont.deriveFont((float) (oldFont.getSize2D() * 1.5)));
267            setMessage(new Object[] { heading, todTextScroller, currentTodIndex, showAtStartup });
268            setMessageType(INFORMATION_MESSAGE);
269            setOptions(newOptions);
270        }
271    
272        /** Loads the Tip of the day texts. */
273        private void loadTodTexts() {
274            final ResourceBundle todBundle = getBundle(getBundleName());
275            for (int i = 1;; i++) {
276                try {
277                    todTexts.add(todBundle.getString(new StringBuilder().append("tod.text.").append(i).toString()));
278                } catch (final MissingResourceException e) {
279                    break;
280                }
281            }
282        }
283    
284        /** Sets the tod index.
285         * @param todIndex new todIndex
286         */
287        private void setTodIndex(final int todIndex) {
288            try {
289                this.todIndex = (todIndex + todTexts.size()) % todTexts.size();
290                todText.setText(todTexts.get(this.todIndex));
291                currentTodIndex.setText(ACTION_FACTORY.format("todIndex", this.todIndex + 1, todTexts.size()));
292            } catch (final ArithmeticException e) {
293                todText.setText(ACTION_FACTORY.getString("todsUnavailable"));
294                currentTodIndex.setText("");
295            }
296        }
297    
298        /** Returns number of current Tip of the day.
299         * In visible state, the index must range between 0 and the number of tods available - 1, inclusive.
300         * @return number of current Tip of the day.
301         */
302        public int getTodIndex() {
303            return todIndex;
304        }
305    
306        /** Action method for close. */
307        public void todClose() {
308            setValue(closeButton);
309            PREFS.putBoolean("showTipOfTheDayAtStartup", showAtStartup.isSelected());
310            PREFS.putInt("lastTipOfTheDayNumber", todIndex);
311            dialog.setVisible(false);
312            dialog.dispose();
313            dialog = null;
314        }
315    
316        /** Action method for next. */
317        public void todNext() {
318            setTodIndex(todIndex + 1);
319        }
320    
321        /** Action method for previous. */
322        public void todPrev() {
323            setTodIndex(todIndex - 1);
324        }
325    
326        /** Action method for random. */
327        public void todRand() {
328            setTodIndex(RND.nextInt(todTexts.size()));
329        }
330    
331    } // class TipOfTheDayManager