/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2003-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2009-2012, Geomatys * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotoolkit.gui.swing; import javax.swing.*; import java.awt.Dimension; import java.awt.Component; import java.awt.EventQueue; import java.awt.BorderLayout; import java.awt.GridBagLayout; import java.awt.GridBagConstraints; import javax.swing.tree.DefaultMutableTreeNode; import java.net.URL; import java.io.IOException; import java.io.InputStream; import javax.imageio.spi.IIORegistry; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.spi.ImageWriterSpi; import javax.imageio.spi.ImageReaderWriterSpi; import java.util.Map; import java.util.Date; import java.util.Locale; import java.util.Arrays; import java.util.TreeMap; import java.util.Iterator; import java.util.jar.Manifest; import java.util.jar.Attributes; import java.text.DateFormat; import java.text.FieldPosition; import java.text.ParseException; import java.text.SimpleDateFormat; import javax.media.jai.JAI; import org.geotoolkit.util.Utilities; import org.apache.sis.util.CharSequences; import org.apache.sis.util.ArraysExt; import org.apache.sis.util.logging.Logging; import org.geotoolkit.resources.Vocabulary; import org.geotoolkit.internal.Threads; import org.geotoolkit.internal.DaemonThread; import org.geotoolkit.internal.swing.SwingUtilities; import org.geotoolkit.gui.swing.image.RegisteredOperationBrowser; /** * An "About" dialog box. This dialog box contains the application's title and some system * informations (Java and OS version, free memory, image readers and writers, running threads, * etc.). The application version can be fetched from a {@link Manifest} object, usually build * from the {@code META-INF/Manifest.mf} file. This manifest should contains entries for * {@code Implementation-Title}, {@code Implementation-Version} and {@code Implementation-Vendor} * values, as suggested in the * <A HREF="http://java.sun.com/docs/books/tutorial/jar/basics/manifest.html#versioning">Java * tutorial</A>. * <p> * In addition to the above-cited standard entries, the {@code About} class understand also * an optional {@code Implementation-Date} entry. This entry can contains the product date * in the <code>"yyyy-MM-dd HH:mm:ss"</code> pattern. If presents, this date will be localized * according user's locale and appended to the version number. * <p> * If none of the above information is available, then {@link Version#GEOTOOLKIT} is used. * * @author Martin Desruisseaux (IRD) * @version 3.12 * * @since 2.0 * @module */ @SuppressWarnings("serial") public class About extends JComponent implements Dialog { /** * The amount of bytes in one "unit of memory" to be displayed. */ private static final float HEAP_SIZE_UNIT = (1024f * 1024f); /** * The entry for timestamp in the manifest file. */ private static final String TIMESTAMP = "Implementation-Date"; /** * Thread qui aura la charge de faire des mises à jour en arrière-plan. * Ce champ sera {@code null} s'il n'y en a pas. */ private final ThreadList updater; /** * The localized resources to use. */ private final Vocabulary resources; /** * Constructs a new dialog box with the Geotk logo. */ public About() { this("org/geotoolkit/resources/Geotoolkit.png", About.class, Threads.GEOTOOLKIT); } /** * Constructs a new dialog box for the specified application class. This constructor * uses the class loader for loading the manifest file. It also uses the class package * to find the right entry into the manifest. * * @param logo The application's logo. It may be a {@link JComponent}, an {@link Icon} object * or an resource path (i.e. a file to be fetch in the classpath) as a {@link String}. * @param application The application's class. Application name will be fetch from the manifest * file ({@code META-INF/Manifest.mf}). * @param tasks Group of running threads, or {@code null} if there is none. */ public About(final Object logo, final Class<?> application, final ThreadGroup tasks) { this(logo, getAttributes(application), application.getClassLoader(), tasks); } /** * Constructs a new dialog box from the specified manifest attributes. * * @param logo The application's logo. It may be a {@link JComponent}, an {@link Icon} object * or an resource path (i.e. a file to be fetch in the classpath) as a {@link String}. * @param attributes The manifest attributes containing application name and version number. * @param tasks Group of running threads, or {@code null} if there is none. */ public About(final Object logo, final Attributes attributes, final ThreadGroup tasks) { this(logo, attributes, null, tasks); } /** * Constructs a new dialog box. * * @param logo The application's logo. It may be a {@link JComponent}, an {@link Icon} object * or an resource path (i.e. a file to be fetch in the classpath) as a {@link String}. * @param attributes The manifest attributes containing application name and version number. * @param loader The application's class loader. * @param tasks Group of running threads, or {@code null} if there is none. */ private About(final Object logo, final Attributes attributes, ClassLoader loader, final ThreadGroup tasks) { setLayout(new GridBagLayout()); final Locale locale = getDefaultLocale(); resources = Vocabulary.getResources(locale); if (loader == null) { loader = getClass().getClassLoader(); // TODO: it would be nice to fetch the caller's class loader instead } /* * Get the free memory before any futher work. */ final Runtime system = Runtime.getRuntime(); system.gc(); final float freeMemory = system.freeMemory() / HEAP_SIZE_UNIT; final float totalMemory = system.totalMemory() / HEAP_SIZE_UNIT; /* * Get application's name, version and vendor from the manifest attributes. * If an implementation date is specified, append it to the version string. */ String application = attributes.getValue(Attributes.Name.IMPLEMENTATION_TITLE); String version = attributes.getValue(Attributes.Name.IMPLEMENTATION_VERSION); String vendor = attributes.getValue(Attributes.Name.IMPLEMENTATION_VENDOR); try { final String dateString = attributes.getValue(TIMESTAMP); if (dateString != null) { final Date date = getDateFormat().parse(dateString); final DateFormat format = DateFormat.getDateInstance(DateFormat.LONG); if (version != null && !version.trim().isEmpty()) { StringBuffer buffer = new StringBuffer(version); buffer.append(" ("); buffer = format.format(date, buffer, new FieldPosition(0)); buffer.append(')'); version = buffer.toString(); } else { version = format.format(date); } } } catch (ParseException exception) { /* * The implementation date can't be parsed. This is not a show-stopper; * the "About" dialog box will just not includes the implementation date. */ Logging.unexpectedException(null, About.class, "<init>", exception); } if (application == null) { application = "<html><h2>Geotoolkit.org</h2></html>"; if (version == null) { version = Utilities.VERSION.toString(); } } /* * If the user supplied a logo, load it and display it in the dialog's upper part (NORTH). * The tabbed pane will be added below the logo, in the dialog's central part (CENTER). */ final GridBagConstraints gc = new GridBagConstraints(); if (logo != null) { final JComponent title; if (logo instanceof JComponent) { title = (JComponent) logo; } else if (logo instanceof Icon) { title = new JLabel((Icon) logo); } else { final String text = String.valueOf(logo); final URL url = loader.getResource(text); if (url == null) { final JLabel label = new JLabel(text); label.setHorizontalAlignment(JLabel.CENTER); label.setBorder(BorderFactory.createEmptyBorder( 6/*top*/, 6/*left*/, 6/*bottom*/, 6/*right*/)); title = label; } else { title = new JLabel(new ImageIcon(url)); } } title.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEmptyBorder(0/*top*/, 0/*left*/, 6/*bottom*/, 0/*right*/), BorderFactory.createCompoundBorder( BorderFactory.createLoweredBevelBorder(), title.getBorder()))); gc.gridx=0; gc.gridy=0; gc.weightx=1; gc.insets.top=9; add(title, gc); } final JTabbedPane tabs = new JTabbedPane(); final JLabel totalMemoryLabel = new JLabel(resources.getString(Vocabulary.Keys.MemoryHeapSize_1, totalMemory)); final JLabel percentUsedLabel = new JLabel(resources.getString(Vocabulary.Keys.MemoryHeapUsage_1, 1 - freeMemory/totalMemory)); gc.gridx=0; gc.gridy=1; gc.weightx=1; gc.weighty=1; gc.fill=GridBagConstraints.BOTH; add(tabs, gc); /* * MAIN TAB (Application name and version informations) */ if (true) { final JPanel pane = new JPanel(new GridBagLayout()); final GridBagConstraints c = new GridBagConstraints(); c.gridx=0; c.weightx=1; c.gridy=0; c.insets.top=12; pane.add(new JLabel(application), c); c.gridy++; c.insets.top=0; pane.add(new JLabel(resources.getString(Vocabulary.Keys.Version_1, version)), c); c.gridy++; pane.add(new JLabel(vendor), c); c.gridy++; c.insets.top=6; pane.add(new JLabel(resources.getString(Vocabulary.Keys.JavaVersion_1, System.getProperty("java.version"))), c); c.gridy++; c.insets.top=0; pane.add(new JLabel(resources.getString(Vocabulary.Keys.JavaVendor_1, System.getProperty("java.vendor" ))), c); c.gridy++; c.insets.top=6; pane.add(new JLabel(resources.getString(Vocabulary.Keys.OsName_1, System.getProperty("os.name"))), c); c.gridy++; c.insets.top=0; pane.add(new JLabel(resources.getString(Vocabulary.Keys.OsVersion_2, System.getProperty("os.version"), System.getProperty("os.arch"))), c); c.gridy++; c.insets.top=12; pane.add(new JLabel(resources.getString(Vocabulary.Keys.TileCacheCapacity_1, JAI.getDefaultInstance().getTileCache().getMemoryCapacity()/HEAP_SIZE_UNIT)), c); c.gridy++; c.insets.top=0; pane.add(totalMemoryLabel, c); c.gridy++; c.insets.bottom=12; pane.add(percentUsedLabel, c); pane.setOpaque(false); tabs.addTab(resources.getString(Vocabulary.Keys.System), pane); } /* * RUNNING TASKS TAB */ if (tasks != null) { updater = new ThreadList(tasks, totalMemoryLabel, percentUsedLabel, resources); final JPanel pane = new JPanel(new BorderLayout()); final JList<String> list = new JList<>(updater); pane.add(new JLabel(resources.getString(Vocabulary.Keys.RunningTasks)), BorderLayout.NORTH); pane.add(new JScrollPane(list), BorderLayout.CENTER); pane.setBorder(BorderFactory.createEmptyBorder(9,9,9,9)); pane.setOpaque(false); tabs.addTab(resources.getString(Vocabulary.Keys.Tasks), pane); } else { updater = null; } /* * IMAGE ENCODERS/DECODERS TAB */ if (true) { final StringBuilder rootName = new StringBuilder(); final Map<String, DefaultMutableTreeNode[]> mimes = new TreeMap<>(); /* * The array in the above map will have a length of 2. The first element is for * readers, and the second element is for writer. The following loop is executed * twice: first for readers, then for writers. */ for (int index=0; index<2; index++) { final short titleKey; final Class<? extends ImageReaderWriterSpi> category; switch (index) { case 0: { titleKey = Vocabulary.Keys.Decoders; category = ImageReaderSpi.class; break; } case 1: { titleKey = Vocabulary.Keys.Encoders; category = ImageWriterSpi.class; break; } default: throw new AssertionError(index); } final String title = resources.getString(titleKey); final Iterator<? extends ImageReaderWriterSpi> it = IIORegistry.getDefaultInstance().getServiceProviders(category, true); while (it.hasNext()) { final ImageReaderWriterSpi spi = it.next(); final String name = spi.getDescription(locale); final String[] mimeTypes = patchMimes(spi.getMIMETypes()); for (final String mimeType : mimeTypes) { DefaultMutableTreeNode[] childs = mimes.get(mimeType); if (childs == null) { childs = new DefaultMutableTreeNode[2]; mimes.put(mimeType, childs); } DefaultMutableTreeNode child = childs[index]; if (child == null) { child = new DefaultMutableTreeNode(title); childs[index] = child; } child.add(new DefaultMutableTreeNode(name, false)); } } if (rootName.length() != 0) { rootName.append(" / "); } rootName.append(title); } final DefaultMutableTreeNode root = new DefaultMutableTreeNode(rootName.toString()); for (final Map.Entry<String, DefaultMutableTreeNode[]> entry : mimes.entrySet()) { final DefaultMutableTreeNode node = new DefaultMutableTreeNode(entry.getKey()); root.add(node); final DefaultMutableTreeNode[] childs = entry.getValue(); for (int i=0; i<childs.length; i++) { if (childs[i] != null) { node.add(childs[i]); } } } JComponent tree = new JTree(root); tree.setBorder(BorderFactory.createEmptyBorder(6,6,0,0)); tree = new JScrollPane(tree); tabs.addTab(resources.getString(Vocabulary.Keys.Images), setup(tree)); } /* * JAI OPERATIONS TAB */ if (true) { final JComponent tree = new RegisteredOperationBrowser(); tabs.addTab(resources.getString(Vocabulary.Keys.Operations), setup(tree)); } } /** * Setup the border for the specified component. */ private static JComponent setup(JComponent component) { component.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEmptyBorder(3,3,3,3), component.getBorder())); component.setPreferredSize(new Dimension(200, 200)); component.setOpaque(false); return component; } /** * Patch the mime type, replacing "" by "(untitled)" for JAI I/O codec. * This happen mostly for RAW format, but we have no guaranteed that it * doesn't happen for other format. */ private String[] patchMimes(String[] mimes) { if (mimes == null) { mimes = new String[] {""}; } for (int i=0; i<mimes.length; i++) { String name = mimes[i].trim(); if (name.isEmpty()) { name = resources.getString(Vocabulary.Keys.Untitled); } mimes[i] = name; } return mimes; } /** * Returns attribute for the specified class. */ private static Attributes getAttributes(final Class<?> classe) { InputStream stream = classe.getClassLoader().getResourceAsStream("META-INF/Manifest.mf"); if (stream != null) try { final Manifest manifest = new Manifest(stream); stream.close(); String name = classe.getName().replace('.','/'); int index; while ((index=name.lastIndexOf('/'))>=0) { final Attributes attributes = manifest.getAttributes(name.substring(0, index+1)); if (attributes!=null) return attributes; name = name.substring(0, index); } return manifest.getMainAttributes(); } catch (IOException e) { Logging.unexpectedException(null, About.class, "getAttributes", e); } // Use empty manifest attributes. return new Attributes(); } /** * Returns a neutral date format for timestamp. */ private static DateFormat getDateFormat() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CANADA); } /** * List of active thread in a given thread group. * This list will update itself in a background thread. * * @author Martin Desruisseaux (IRD, Geomatys) * @version 3.09 * * @since 2.0 * @module */ @SuppressWarnings("serial") private static final class ThreadList extends AbstractListModel<String> implements Runnable { /** * The thread which update {@code ThreadList}, or {@code null} if none. * Setting this field to {@code null} will stop any current process. */ transient DaemonThread worker; /** * The group of threads to display. */ private final ThreadGroup tasks; /** * The list of threads to display. This list will be updated in a background * thread (the {@linkplain #worker}) on a regular basis. */ private String[] names = CharSequences.EMPTY_ARRAY; /** * The label where to write the total amount of memory. */ private final JLabel totalMemory; /** * The laber where to write the percentage of memory currently in use. */ private final JLabel percentUsed; /** * The localized resources to use. */ private final Vocabulary resources; /** * Creates a list of threads declared in the given thread group. */ public ThreadList(final ThreadGroup tasks, final JLabel totalMemory, final JLabel percentUsed, final Vocabulary resources) { this.tasks = tasks; this.totalMemory = totalMemory; this.percentUsed = percentUsed; this.resources = resources; } /** * Returns the number of elements currently in this list. */ @Override public int getSize() { // NO synchronized here return names.length; } /** * Returns the element at the given index in this list. */ @Override public String getElementAt(final int index) { // NO synchronized here return names[index]; } /** * Starts the thread, if not already running. */ public synchronized void start() { if (worker == null) { worker = new DaemonThread(SwingUtilities.THREAD_GROUP, this, resources.getString(Vocabulary.Keys.About)); worker.setPriority(Thread.NORM_PRIORITY - 1); worker.start(); } } /** * Updates the content of the "About" pane on a regular basis. The loop can * be interrupted by setting the {@link #tasks} field to {@code null}. */ @Override public synchronized void run() { String oldTotalMemory = null; String oldPercentUsed = null; while (worker == Thread.currentThread() && !worker.isKillRequested() && listenerList.getListenerCount() != 0) { final Runtime system = Runtime.getRuntime(); final float freeMemoryN = system.freeMemory() / HEAP_SIZE_UNIT; final float totalMemoryN = system.totalMemory() / HEAP_SIZE_UNIT; String totalMemoryText = resources.getString(Vocabulary.Keys.MemoryHeapSize_1, totalMemoryN); String percentUsedText = resources.getString(Vocabulary.Keys.MemoryHeapUsage_1, 1 - freeMemoryN/totalMemoryN); Thread[] threadArray = new Thread[tasks.activeCount()]; String[] threadNames = new String[tasks.enumerate(threadArray)]; int c=0; for (int i=0; i<threadNames.length; i++) { if (threadArray[i] != worker) { threadNames[c++] = threadArray[i].getName(); } } threadNames = ArraysExt.resize(threadNames, c); if (Arrays.equals(names, threadNames)) { threadNames = null; } if (totalMemoryText.equals(oldTotalMemory)) { totalMemoryText = null; } else { oldTotalMemory = totalMemoryText; } if (percentUsedText.equals(oldPercentUsed)) { percentUsedText = null; } else { oldPercentUsed = percentUsedText; } if (threadNames != null || totalMemoryText != null || percentUsedText != null) { final String[] names = threadNames; final String totalMemory = totalMemoryText; final String percentUsed = percentUsedText; EventQueue.invokeLater(new Runnable() { @Override public void run() { update(names, totalMemory, percentUsed); } }); } try { wait(4000); } catch (InterruptedException exception) { // Someone asked for interruption. The panel will not be updated anymore. break; } } worker = null; } /** * Updates the content of the thread list. This method shall be * invoked on the Swing thread only. */ private synchronized void update(final String[] newNames, final String totalMemory, final String percentUsed) { if (newNames != null) { final int count = Math.max(names.length, newNames.length); names = newNames; fireContentsChanged(this, 0, count-1); } if (totalMemory != null) this.totalMemory.setText(totalMemory); if (percentUsed != null) this.percentUsed.setText(percentUsed); } } /** * Forces the current values to be taken from the editable fields and set them as the * current values. The default implementation does nothing since there is no editable * fields in this widget. * * @since 3.12 */ @Override public void commitEdit() throws ParseException { } /** * Popups the dialog box and waits for the user. This method always invoke {@link #start} * before showing the dialog, and {@link #stop} after disposing it. * * @param owner The component which will be the owner of this component. */ public void showDialog(final Component owner) { showDialog(owner, resources.getMenuLabel(Vocabulary.Keys.About)); } /** * Popups the dialog box and waits for the user. This method always invoke {@link #start} * before showing the dialog, and {@link #stop} after disposing it. * * @param owner The component which will be the owner of this component. * @param title The title to write in the window bar. * @return Always {@code false} for this component. * * @since 3.00 */ @Override public boolean showDialog(final Component owner, final String title) { try { start(); SwingUtilities.showMessageDialog(owner, this, title, JOptionPane.PLAIN_MESSAGE); } finally { stop(); } return false; } /** * Starts a daemon thread updating the information shown in this {@code About} widget. Updated * information include available memory and the list of running tasks. <strong>You <u>must</u> * invoke the {@link #stop} method after {@code start()}</strong> (typically in a {@code try * ... finally} construct) in order to free resources. {@code stop()} will not be automatically * invoked by the garbage collector. */ protected void start() { final ThreadList updater = this.updater; if (updater != null) { updater.start(); } } /** * Frees any resources used by this dialog box. <strong>This method must be invoked after * {@link #start}</strong> in order to free resources, since {@code stop()} is not invoked * automatically by the garbage collector. */ protected void stop() { final ThreadList updater = this.updater; if (updater != null) { updater.worker = null; // Stop the thread. } } }