/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2003-2008, Open Source Geospatial Foundation (OSGeo)
*
* 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.geotools.gui.swing;
// User interface
import java.awt.Dimension;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.GridLayout;
import java.awt.BorderLayout;
import java.awt.GridBagLayout;
import java.awt.GridBagConstraints;
import javax.swing.event.ListDataListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.MutableTreeNode;
import javax.swing.AbstractListModel;
import javax.swing.BorderFactory;
import javax.swing.JOptionPane;
import javax.swing.JTabbedPane;
import javax.swing.JScrollPane;
import javax.swing.JComponent;
import javax.swing.ImageIcon;
import javax.swing.JPanel;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JTree;
import javax.swing.Icon;
// Input/output
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;
// Manifest
import java.util.jar.Manifest;
import java.util.jar.Attributes;
// Formatting
import java.text.DateFormat;
import java.text.FieldPosition;
import java.text.ParseException;
import java.text.SimpleDateFormat;
// Miscellaneous
import java.util.Map;
import java.util.Date;
import java.util.Locale;
import java.util.Arrays;
import java.util.TreeMap;
import java.util.Iterator;
// Java Advanced Imaging
import javax.media.jai.JAI;
// Geotools dependencies
import org.geotools.util.logging.Logging;
import org.geotools.resources.XArray;
import org.geotools.resources.Arguments;
import org.geotools.resources.SwingUtilities;
import org.geotools.resources.i18n.Vocabulary;
import org.geotools.resources.i18n.VocabularyKeys;
import org.geotools.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>.
* 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> patter. If presents, this date will be localized
* according user's locale and appended to the version number.
*
* <p> </p>
* <p align="center"><img src="doc-files/About.png"></p>
* <p> </p>
*
* @since 2.0
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (IRD)
*/
public class About extends JPanel {
/**
* 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;
/**
* Construct a new dialog box with the Geotools's logo.
*/
public About() {
this("org/geotools/resources/logo/Geotools.png", About.class, null);
}
/**
* Constructs a new dialog box for the specified application class. This constructor
* uses the class loader for loading the manifest file. It also use 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)
{
super(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().length()!=0) {
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(About.class, "<init>", exception);
}
/*
* 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(VocabularyKeys.MEMORY_HEAP_SIZE_$1, new Float(totalMemory)));
final JLabel percentUsedLabel = new JLabel(resources.getString(VocabularyKeys.MEMORY_HEAP_USAGE_$1, new Float(1-freeMemory/totalMemory)));
gc.gridx=0; gc.gridy=1; gc.weightx=1; gc.weighty=1; gc.fill=gc.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(VocabularyKeys.VERSION_$1, version)), c);
c.gridy++;
pane.add(new JLabel(vendor), c);
c.gridy++; c.insets.top=6;
pane.add(new JLabel(resources.getString(VocabularyKeys.JAVA_VERSION_$1,
System.getProperty("java.version"))), c);
c.gridy++; c.insets.top=0;
pane.add(new JLabel(resources.getString(VocabularyKeys.JAVA_VENDOR_$1,
System.getProperty("java.vendor" ))), c);
c.gridy++; c.insets.top=6;
pane.add(new JLabel(resources.getString(VocabularyKeys.OS_NAME_$1,
System.getProperty("os.name"))), c);
c.gridy++; c.insets.top=0;
pane.add(new JLabel(resources.getString(VocabularyKeys.OS_VERSION_$2,
System.getProperty("os.version"),
System.getProperty("os.arch"))), c);
c.gridy++; c.insets.top=12;
pane.add(new JLabel(resources.getString(VocabularyKeys.TILE_CACHE_CAPACITY_$1, new Float(
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);
tabs.addTab(resources.getString(VocabularyKeys.SYSTEM), pane);
}
/*
* RUNNING TASKS TAB
*/
if (tasks != null) {
updater = new ThreadList(tasks, totalMemoryLabel, percentUsedLabel, resources);
final JPanel pane = new JPanel(new BorderLayout());
final JList list = new JList(updater);
pane.add(new JLabel(resources.getString(VocabularyKeys.RUNNING_TASKS)), BorderLayout.NORTH);
pane.add(new JScrollPane(list), BorderLayout.CENTER);
pane.setBorder(BorderFactory.createEmptyBorder(9,9,9,9));
tabs.addTab(resources.getString(VocabularyKeys.TASKS), pane);
} else {
updater = null;
}
/*
* IMAGE ENCODERS/DECODERS TAB
*/
if (true) {
final StringBuffer buffer = new StringBuffer();
final Map mimes = new TreeMap();
boolean writer = false;
do {
final int titleKey;
final Class category;
if (writer) {
titleKey = VocabularyKeys.ENCODERS;
category = ImageWriterSpi.class;
} else {
titleKey = VocabularyKeys.DECODERS;
category = ImageReaderSpi.class;
}
String title = resources.getString(titleKey);
Iterator it = IIORegistry.getDefaultInstance().getServiceProviders(category, true);
while (it.hasNext()) {
final ImageReaderWriterSpi spi = (ImageReaderWriterSpi) it.next();
final String name = spi.getDescription(locale);
final String[] mimeTypes = spi.getMIMETypes();
patchMimes(mimeTypes);
for (int i=0; i<mimeTypes.length; i++) {
final String mimeType = mimeTypes[i];
DefaultMutableTreeNode child = (DefaultMutableTreeNode)mimes.get(mimeType);
if (child == null) {
child = new DefaultMutableTreeNode(mimeType);
mimes.put(mimeType, child);
}
child.add(new DefaultMutableTreeNode(name, false));
}
if (title!=null && mimeTypes.length!=0) {
if (buffer.length() != 0) {
buffer.append(" / ");
}
buffer.append(title);
title = null;
}
}
} while ((writer = !writer) == true);
final String title = buffer.toString();
final DefaultMutableTreeNode root = new DefaultMutableTreeNode(title);
for (final Iterator it=mimes.values().iterator(); it.hasNext();) {
root.add((DefaultMutableTreeNode) it.next());
}
JComponent tree = new JTree(root);
tree.setBorder(BorderFactory.createEmptyBorder(6,6,0,0));
tree = new JScrollPane(tree);
tabs.addTab(resources.getString(VocabularyKeys.IMAGES), setup(tree));
}
/*
* JAI OPERATIONS TAB
*/
if (true) {
final JComponent tree = new RegisteredOperationBrowser();
tabs.addTab(resources.getString(VocabularyKeys.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));
return component;
}
/**
* Patch the mime type, replacing "" by "(raw)" for JAI I/O codec.
*/
private static void patchMimes(String[] mimes) {
for (int i=0; i<mimes.length; i++) {
String name = mimes[i].trim();
if (name.length() == 0) {
name = "(raw)";
}
mimes[i] = name;
}
}
/**
* 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(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);
}
/**
* Modèle représentant la liste des processus actif dans un {@link ThreadGroup}.
* Cette liste se mettre automatiquement à jour de façon périodique.
*
* @version $Id$
* @author Martin Desruisseaux (IRD)
*/
private static final class ThreadList extends AbstractListModel implements Runnable {
/**
* Processus qui met à jour {@code ThreadList}, ou {@code null} s'il n'y en a pas.
* On peut tuer le processus actif en donnant la valeur {@code null} à cette variable.
*/
public transient Thread worker;
/**
* Liste des processus en cours.
*/
private final ThreadGroup tasks;
/**
* Liste des noms des processus en cours. Cette liste sera mises à jour périodiquement.
*/
private String[] names=new String[0];
/**
* Texte dans lequel écrire la mémoire totale réservée.
*/
private final JLabel totalMemory;
/**
* Texte dans lequel écrire le pourcentage de mémoire utilisée.
*/
private final JLabel percentUsed;
/**
* The localized resources to use.
*/
private final Vocabulary resources;
/**
* Construit une liste des processus actifs dans le groupe {@code tasks} spécifié.
*/
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;
}
/**
* Retourne le nombre d'éléments dans la liste.
*/
public int getSize() { // NO synchronized here
return names.length;
}
/**
* Retourne un des éléments de la liste.
*/
public Object getElementAt(final int index) { // NO synchronized here
return names[index];
}
/**
* Ajoute un objet à la liste des objets intéressés
* à être informé des changements apportés à la liste.
*/
public synchronized void addListDataListener(final ListDataListener listener) {
super.addListDataListener(listener);
}
/**
* Démarre le thread.
*/
public synchronized void start() {
if (worker == null) {
worker = new Thread(this, resources.getString(VocabularyKeys.ABOUT));
worker.setPriority(Thread.MIN_PRIORITY);
worker.setDaemon(true);
worker.start();
}
}
/**
* Met à jour le contenu de la liste à interval régulier.
* Cette méthode est exécutée dans une boucle jusqu'à ce
* qu'elle soit interrompue en donnant la valeur nulle à
* {@link #tasks}.
*/
public synchronized void run() {
String oldTotalMemory = null;
String oldPercentUsed = null;
while (worker==Thread.currentThread() && 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(VocabularyKeys.MEMORY_HEAP_SIZE_$1,
new Float(totalMemoryN));
String percentUsedText = resources.getString(VocabularyKeys.MEMORY_HEAP_USAGE_$1,
new Float(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 = (String[])XArray.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() {
public void run() {
update(names, totalMemory, percentUsed);
}
});
}
try {
wait(4000);
} catch (InterruptedException exception) {
// Quelqu'un a réveillé ce thread. Retourne au travail.
}
}
worker = null;
}
/**
* Met à jour le contenu de la liste. Cette méthode
* est appelée périodiquement dans le thread de Swing.
*/
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);
}
}
/**
* Popups the dialog box and wait for the user. This method
* always invoke {@link #start} before showing the dialog,
* and {@link #stop} after disposing it.
*/
public void showDialog(final Component owner) {
try {
start();
SwingUtilities.showMessageDialog(owner, this,
resources.getMenuLabel(VocabularyKeys.ABOUT), JOptionPane.PLAIN_MESSAGE);
} finally {
stop();
}
}
/**
* Start a daemon thread updating dialog box information. 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()} is not automatically
* invoked by the garbage collector.
*/
protected void start() {
final ThreadList updater = this.updater;
if (updater != null) {
updater.start();
}
}
/**
* Free any resources used by this dialog box. <strong>This method must be invoked
* after {@link #start}</strong> in order to free resources. {@code stop()} is
* not automatically invoked by the garbage collector.
*/
protected void stop() {
final ThreadList updater = this.updater;
if (updater != null) {
updater.worker = null; // Stop the thread.
}
// Le thread avait une référence indirecte vers 'this' via 'ListDataListener'
}
/**
* Convenience method for setting the {@code Implementation-Date}
* attributes to the current date.
*
* @param attributes Attributes in which setting the compilation date.
*/
public static void touch(final Attributes attributes) {
attributes.putValue(TIMESTAMP, getDateFormat().format(new Date()));
}
/**
* Display the default "About" dialog box. This method is usefull
* for testing the widget appareance and for checking system informations.
*
* @param args the command line arguments
*/
public static void main(final String[] args) {
final Arguments arguments = new Arguments(args);
Locale.setDefault(arguments.locale);
new About().showDialog(null);
System.exit(0);
}
}