/*******************************************************************************
* Copyright (c) 1998, 2015 Oracle and/or its affiliates. All rights reserved.
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0
* which accompanies this distribution.
* The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
* and the Eclipse Distribution License is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* Contributors:
* Oracle - initial API and implementation from Oracle TopLink
******************************************************************************/
package org.eclipse.persistence.tools.workbench.framework.ui.chooser;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dialog;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Window;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.text.CollationKey;
import java.text.Collator;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ListCellRenderer;
import javax.swing.ListSelectionModel;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import org.eclipse.persistence.tools.workbench.framework.action.AbstractFrameworkAction;
import org.eclipse.persistence.tools.workbench.framework.context.ApplicationContext;
import org.eclipse.persistence.tools.workbench.framework.context.WorkbenchContext;
import org.eclipse.persistence.tools.workbench.framework.ui.dialog.AbstractDialog;
import org.eclipse.persistence.tools.workbench.framework.uitools.SwingComponentFactory;
import org.eclipse.persistence.tools.workbench.uitools.FilteringListPanel;
import org.eclipse.persistence.tools.workbench.uitools.cell.SimpleListCellRenderer;
import org.eclipse.persistence.tools.workbench.utility.iterators.SingleElementIterator;
import org.eclipse.persistence.tools.workbench.utility.string.StringConverter;
import org.eclipse.persistence.tools.workbench.utility.string.StringTools;
/**
* This dialog presents a list of short class names to the user that
* can be filtered down by typing in the text field.
* When the user selects one of the short class names, we
* present a list of packages that contain a class with the
* chosen short class name. The user selects a package
* and there you go [gathering nuts in May].
*
* Once the user presses the OK button, clients of this dialog
* can retrieve the selected class by calling #selection().
*
* A "class" can be anything the resembles a Class and can
* be adapted with a ClassDescriptionAdapter (e.g. java.lang.Class,
* java.lang.String, org.eclipse.persistence.tools.workbench.mappingsmodel.spi.meta.ClassDescription).
*/
// TODO maybe filter inner classes based on their "local" names,
// then display their "outer" class in the "package" list alongside any packages
// e.g. "Entry" would have "java.util.Map" in the "package" list,
// possibly with a different icon alongside it
public class ClassChooserDialog extends AbstractDialog {
/** The repository holding the list of classes. */
private ClassDescriptionRepository repository;
private ClassDescriptionAdapter adapter;
private ShortClassNameEntry[] shortClassNameEntries;
/** Hold this so we can give it the initial focus. */
private FilteringListPanel filteringPanel;
/** Hold this so we can manipulate it some. */
private JList packageListBox;
/** This listens to both list boxes. */
private MouseListener doubleClickMouseListener;
/** Cache the icons. */
Icon classIcon;
Icon packageIcon;
/** Optional initial selection. */
private Object initialSelection;
/** Whether the user is allowed to clear the selection - default is false. */
private boolean allowNullSelection;
/** An empty package list for when no class is selected. */
private static final Object[] EMPTY_PACKAGE_LIST = new Object[0];
// ********** static methods **********
/** Factory method - "class descriptions" are strings */
public static ClassChooserDialog createDialog(ClassDescriptionRepository repository, WorkbenchContext context) {
return createDialog(repository, DefaultClassDescriptionAdapter.instance(), context);
}
/** Factory method. */
public static ClassChooserDialog createDialog(ClassDescriptionRepository repository, ClassDescriptionAdapter adapter, WorkbenchContext context) {
Window window = context.getCurrentWindow();
if (window instanceof Dialog) {
return new ClassChooserDialog(repository, adapter, context, (Dialog) window);
}
return new ClassChooserDialog(repository, adapter, context);
}
private static String title(ApplicationContext context) {
return context.getResourceRepository().getString("CLASS_CHOOSER_DIALOG.TITLE");
}
// ********** constructors **********
private ClassChooserDialog(ClassDescriptionRepository repository, ClassDescriptionAdapter adapter, WorkbenchContext context) {
super(context, title(context.getApplicationContext()));
this.initialize(repository, adapter);
}
private ClassChooserDialog(ClassDescriptionRepository repository, ClassDescriptionAdapter adapter, WorkbenchContext context, Dialog owner) {
super(context, title(context.getApplicationContext()), owner);
this.initialize(repository, adapter);
}
// ********** initialization **********
private void initialize(ClassDescriptionRepository cdr, ClassDescriptionAdapter cda) {
this.repository = cdr;
this.adapter = cda;
this.shortClassNameEntries = this.buildShortClassNameEntries();
this.doubleClickMouseListener = this.buildDoubleClickMouseListener();
this.classIcon = this.resourceRepository().getIcon("class.public");
this.packageIcon = this.resourceRepository().getIcon("package");
this.allowNullSelection = false;
}
/**
* build a temporary map of the short class name entries keyed by
* short class name (this speeds up the population of the entries); but
* then return only the entries themselves
*/
private ShortClassNameEntry[] buildShortClassNameEntries() {
Collator collator = Collator.getInstance();
// short class name entries keyed by short class name
Map shortClassNameEntryMap = new HashMap(20000); // start big
for (Iterator stream = this.repository.classDescriptions(); stream.hasNext(); ) {
Object classDescription = stream.next();
this.shortClassNameEntry(classDescription, shortClassNameEntryMap, collator).addPackageEntry(new PackageEntry(classDescription, this.adapter, collator));
}
Collection values = shortClassNameEntryMap.values();
ShortClassNameEntry[] entries = (ShortClassNameEntry[]) values.toArray(new ShortClassNameEntry[values.size()]);
Arrays.sort(entries);
// this.reportMultiPackageClasses(entries);
return entries;
}
/**
* get the short class name entry for the specified type from the specified map,
* creating it and adding it to the map if necessary
*/
private ShortClassNameEntry shortClassNameEntry(Object classDescription, Map shortClassNameEntryMap, Collator collator) {
String shortClassName = this.adapter.shortClassName(classDescription).replace('$', '.');
ShortClassNameEntry shortClassNameEntry = (ShortClassNameEntry) shortClassNameEntryMap.get(shortClassName);
if (shortClassNameEntry == null) {
shortClassNameEntry = new ShortClassNameEntry(shortClassName, collator);
shortClassNameEntryMap.put(shortClassName, shortClassNameEntry);
}
return shortClassNameEntry;
}
/* use this method to find out just how many
* classes belong to multiple packages
* (~3% in JDK 1.4.2)
*/
// private void reportMultiPackageClasses(ShortClassNameEntry[] entries) {
// System.out.println("total classes: " + entries.length);
// float multiPkgClasses = 0;
// for (int i = entries.length; i-- > 0; ) {
// if (entries[i].getPackageEntries().length > 1) {
// multiPkgClasses++;
// }
// }
// float percent = multiPkgClasses / entries.length * 100;
// java.text.NumberFormat format = java.text.NumberFormat.getNumberInstance();
// format.setMaximumFractionDigits(1);
// System.out.println("multi-package classes: " + (int) multiPkgClasses + " (" + format.format(percent) + "%)");
// }
//
/**
* Build a listener that makes a double-click equivalent to
* clicking the OK button. This will listen to both the short class
* name list and the package list.
*/
private MouseListener buildDoubleClickMouseListener() {
return new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) {
ClassChooserDialog.this.clickOK();
}
}
};
}
private Action buildRefreshAction() {
return new AbstractFrameworkAction(this.getWorkbenchContext()) {
protected void initialize() {
this.initializeTextAndMnemonic("CLASS_CHOOSER_DIALOG.REFRESH_BUTTON");
}
protected void execute() {
ClassChooserDialog.this.refresh();
}
};
}
// ********** main panel **********
protected Component buildMainPanel() {
JPanel mainPanel = new JPanel(new GridBagLayout());
mainPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 0, 5));
GridBagConstraints constraints = new GridBagConstraints();
// filtering panel
this.filteringPanel = new FilteringListPanel(this.shortClassNameEntries, null, new ShortClassNameEntryStringConverter());
this.configureLabel(this.filteringPanel.getTextFieldLabel(), "CLASS_CHOOSER_DIALOG.TEXT_FIELD_LABEL");
this.configureLabel(this.filteringPanel.getListBoxLabel(), "CLASS_CHOOSER_DIALOG.CLASS_LIST_BOX_LABEL");
this.filteringPanel.setListBoxCellRenderer(this.buildClassListCellRenderer());
this.filteringPanel.getListBox().getSelectionModel().addListSelectionListener(this.buildClassListSelectionListener());
this.filteringPanel.getListBox().addMouseListener(this.doubleClickMouseListener);
constraints.gridx = 0;
constraints.gridy = 0;
constraints.gridwidth = 1;
constraints.gridheight = 1;
constraints.weightx = 1;
constraints.weighty = 3;
constraints.anchor = GridBagConstraints.PAGE_START;
constraints.fill = GridBagConstraints.BOTH;
constraints.insets = new Insets(0, 0, 0, 0);
mainPanel.add(this.filteringPanel, constraints);
// package list box
JPanel packageListPanel = new JPanel(new BorderLayout());
JLabel packageListLabel = new JLabel();
this.configureLabel(packageListLabel, "CLASS_CHOOSER_DIALOG.PACKAGE_LIST_BOX_LABEL");
packageListLabel.setBorder(BorderFactory.createEmptyBorder(5, 0, 5, 0));
packageListPanel.add(packageListLabel, BorderLayout.PAGE_START);
this.packageListBox = SwingComponentFactory.buildList();
this.packageListBox.setDoubleBuffered(true);
this.packageListBox.setCellRenderer(this.buildPackageListCellRenderer());
this.packageListBox.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
this.packageListBox.getSelectionModel().addListSelectionListener(this.buildPackageListSelectionListener());
this.packageListBox.addMouseListener(this.doubleClickMouseListener);
packageListLabel.setLabelFor(this.packageListBox);
packageListPanel.add(new JScrollPane(this.packageListBox), BorderLayout.CENTER);
constraints.gridx = 0;
constraints.gridy = 1;
constraints.gridwidth = 1;
constraints.gridheight = 1;
constraints.weightx = 1;
constraints.weighty = 1;
constraints.anchor = GridBagConstraints.PAGE_END;
constraints.fill = GridBagConstraints.BOTH;
constraints.insets = new Insets(0, 0, 0, 0);
mainPanel.add(packageListPanel, constraints);
return mainPanel;
}
/**
* Configure the specified label's text and mnemonic.
*/
private void configureLabel(JLabel label, String key) {
label.setText(this.resourceRepository().getString(key));
label.setDisplayedMnemonic(this.resourceRepository().getMnemonic(key));
}
private ListCellRenderer buildClassListCellRenderer() {
return new SimpleListCellRenderer() {
protected Icon buildIcon(Object value) {
return ClassChooserDialog.this.classIcon;
}
protected String buildText(Object value) {
return ((ShortClassNameEntry) value).getName();
}
};
}
private ListSelectionListener buildClassListSelectionListener() {
return new ListSelectionListener() {
public void valueChanged(ListSelectionEvent e) {
if ( ! e.getValueIsAdjusting()) {
ClassChooserDialog.this.classSelectionChanged(e);
}
}
};
}
private ListCellRenderer buildPackageListCellRenderer() {
return new SimpleListCellRenderer() {
protected Icon buildIcon(Object value) {
return ClassChooserDialog.this.packageIcon;
}
protected String buildText(Object value) {
return ((PackageEntry) value).getDisplayString();
}
};
}
private ListSelectionListener buildPackageListSelectionListener() {
return new ListSelectionListener() {
public void valueChanged(ListSelectionEvent e) {
if ( ! e.getValueIsAdjusting()) {
ClassChooserDialog.this.packageSelectionChanged(e);
}
}
};
}
// ********** AbstractDialog implementation/overrides **********
/**
* nothing is selected initially - so disable the OK button
*/
protected Action buildOKAction() {
Action action = super.buildOKAction();
action.setEnabled(false);
return action;
}
protected Iterator buildCustomActions() {
return new SingleElementIterator(this.buildRefreshAction());
}
protected String helpTopicId() {
return "dialog.classChooser";
}
protected Component initialFocusComponent() {
return this.filteringPanel.getTextField();
}
protected void prepareToShow() {
pack();
Dimension size = getPreferredSize();
size.width = Math.max(size.width, 350);
size.height = Math.max(size.height, 566);
setSize(size);
this.setLocationRelativeTo(this.getParent());
if (this.initialSelection != null) {
this.setInitialSelectionInternal(this.initialSelection);
}
}
/**
* increase visibility slightly for inner class
*/
protected void clickOK() {
super.clickOK();
}
// ********** behavior **********
private void setInitialSelectionInternal(Object classDescription) {
ShortClassNameEntry[] scnEntries = this.shortClassNameEntries;
for (int i = scnEntries.length; i-- > 0; ) {
ShortClassNameEntry scnEntry = scnEntries[i];
PackageEntry pEntry = scnEntry.packageEntryFor(classDescription);
if (pEntry != null) {
this.filteringPanel.setSelection(scnEntry);
this.packageListBox.setSelectedValue(pEntry, true);
break;
}
}
}
void classSelectionChanged(ListSelectionEvent e) {
Object sel = this.filteringPanel.getSelection();
if (sel == null) {
this.packageListBox.setListData(EMPTY_PACKAGE_LIST);
} else {
this.packageListBox.setListData(((ShortClassNameEntry) sel).getPackageEntries());
this.packageListBox.getSelectionModel().setAnchorSelectionIndex(0);
this.packageListBox.getSelectionModel().setLeadSelectionIndex(0);
}
}
void packageSelectionChanged(ListSelectionEvent e) {
this.packageListBox.ensureIndexIsVisible(e.getFirstIndex());
// enable the OK button only when the user has selected a package
this.getOKAction().setEnabled(this.allowNullSelection || (this.packageListBox.getSelectedValue() != null));
}
void refresh() {
this.repository.refreshClassDescriptions();
this.shortClassNameEntries = this.buildShortClassNameEntries();
this.filteringPanel.setCompleteList(this.shortClassNameEntries);
}
// ********** public API **********
public void setAllowNullSelection(boolean allowNullSelection) {
this.allowNullSelection = allowNullSelection;
}
public void setInitialSelection(Object classDescription) {
this.initialSelection = classDescription;
}
/**
* Return the type selected by the user.
*/
public Object selection() {
if ( ! this.wasConfirmed()) {
throw new IllegalStateException();
}
// the package entry should only be null if allowNullSelection is true
PackageEntry packageEntry = (PackageEntry) this.packageListBox.getSelectedValue();
return (packageEntry == null) ? null : packageEntry.getClassDescription();
}
// ******************** inner classes ********************
/**
* Associate a short class name with its packages.
*/
private static class ShortClassNameEntry implements Comparable {
/** The type's short name. */
private String name;
private CollationKey collationKey;
/** The packages containing a type the short name referenced above. */
private PackageEntry[] packageEntries;
ShortClassNameEntry(String name, Collator collator) {
super();
this.name = name;
this.collationKey = collator.getCollationKey(name);
// all classes will belong to at least one package; and very
// few belong to more than one (~3% of the classes in JDK 1.4.2)
this.packageEntries = new PackageEntry[1];
}
String getName() {
return this.name;
}
PackageEntry[] getPackageEntries() {
return this.packageEntries;
}
void addPackageEntry(PackageEntry packageEntry) {
if (this.packageEntries[0] == null) {
this.packageEntries[0] = packageEntry;
} else {
this.addSubsequentPackageEntry(packageEntry);
}
}
private void addSubsequentPackageEntry(PackageEntry packageEntry) {
String packageName = packageEntry.getName();
PackageEntry[] oldEntries = this.packageEntries;
int len = oldEntries.length;
// if we have 2 packages with the same name, configure them to display their descriptions
for (int i = len; i-- > 0; ) {
if (oldEntries[i].getName().equals(packageName)) {
oldEntries[i].setDisplaysAdditionalInfo(true);
packageEntry.setDisplaysAdditionalInfo(true);
}
}
PackageEntry[] newEntries = new PackageEntry[len + 1];
System.arraycopy(oldEntries, 0, newEntries, 0, len);
newEntries[len] = packageEntry;
Arrays.sort(newEntries);
this.packageEntries = newEntries;
}
PackageEntry packageEntryFor(Object classDescription) {
PackageEntry[] entries = this.packageEntries;
int len = entries.length;
for (int i = len; i-- > 0; ) {
if (entries[i].isEntryFor(classDescription)) {
return entries[i];
}
}
return null;
}
public int compareTo(Object o) {
return this.collationKey.compareTo(((ShortClassNameEntry) o).collationKey);
}
public String toString() {
return StringTools.buildToStringFor(this, this.name);
}
}
/**
* Converts a short class name entry to a string in an obvious fashion.
*/
private class ShortClassNameEntryStringConverter implements StringConverter {
public String convertToString(Object o) {
return (o == null) ? null : ((ShortClassNameEntry) o).getName();
}
}
/**
* Wrap a user-supplied "class description".
* This feels a little weird; but it *is* the selection of a package that
* determines which "class description" is to be returned to the client.
*/
private static class PackageEntry implements Comparable {
private Object classDescription;
private String name;
private String additionalInfo;
private boolean displaysAdditionalInfo;
private String displayString;
private Collator collator;
PackageEntry(Object classDescription, ClassDescriptionAdapter adapter, Collator collator) {
super();
this.classDescription = classDescription;
this.name = adapter.packageName(classDescription);
this.additionalInfo = adapter.additionalInfo(this.classDescription);
this.displaysAdditionalInfo = false;
this.displayString = this.buildDisplayString();
this.collator = collator;
}
private String buildDisplayString() {
if (( ! this.displaysAdditionalInfo) || (this.additionalInfo == null) || (this.additionalInfo.length() == 0)) {
return this.name;
}
return this.name + " - " + this.additionalInfo;
}
Object getClassDescription() {
return this.classDescription;
}
String getName() {
return this.name;
}
String getAdditionalInfo() {
return this.additionalInfo;
}
boolean displaysAdditionalInfo() {
return this.displaysAdditionalInfo;
}
void setDisplaysAdditionalInfo(boolean displaysAdditionalInfo) {
if (this.displaysAdditionalInfo == displaysAdditionalInfo) {
return;
}
this.displaysAdditionalInfo = displaysAdditionalInfo;
// rebuild the display string
this.displayString = this.buildDisplayString();
}
String getDisplayString() {
return this.displayString;
}
boolean isEntryFor(Object otherClassDescription) {
return this.classDescription == otherClassDescription;
}
public int compareTo(Object o) {
// sort by name first...
int result = this.collator.compare(this.name, ((PackageEntry) o).name);
if (result != 0) {
return result;
}
// ...then description
return this.collator.compare(this.displayString, ((PackageEntry) o).displayString);
}
public String toString() {
return StringTools.buildToStringFor(this, this.displayString);
}
}
// ******************** static helper method ********************
public static void gc() {
Thread t = new Thread() {
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException ex) {
return;
}
System.gc();
}
};
t.start();
}
}