package com.ca.directory.jxplorer.viewer;
import com.ca.commons.cbutil.*;
import com.ca.commons.naming.*;
import com.ca.directory.jxplorer.*;
import com.ca.directory.jxplorer.tree.NewEntryWin;
import com.ca.directory.jxplorer.viewer.tableviewer.*;
import com.ca.directory.jxplorer.viewer.tableviewer.AttributeValue;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.logging.Logger;
import java.util.logging.Level;
/**
* This class displays attributes in a table (currently string attributes only). The user can modify the table values,
* and submit the results, which are passed to the registered DataSource (obtained from the registered DataSource)...
*/
/* PROGRAMMING NOTE:
*
* Some rather unpleasent stuff happens with object class changing. The state
* of the unmodified entry is maintained between displayEntry() calls using
* the classChangedOriginalEntry variable.
*/
public class TableAttributeEditor extends JPanel
implements DataSink, PluggableEditor //, TreeEntryCreator
{
private static Logger log = Logger.getLogger(TableAttributeEditor.class.getName());
JTable attributeTable;
AttributeTableModel tableData;
JScrollPane tableScroller;
CBButton submit, reset, changeClass, opAttrs; //, help;
JFrame owner;
JDialog virtualEntryDialog = null;
/**
* Flag for a virtual entry.
*/
boolean virtualEntry = false;
/**
* Copy of the current entry.
*/
DXEntry currentEntry = null;
/**
* Copy of the current DN.
*/
DN currentDN = null;
/**
* The data source directory data is read from.
*/
public DataSource dataSource;
/**
* A rare operation is for the user to change the classes of an entry. This backs up the original state of that
* entry.
*/
DXEntry classChangedOriginalEntry = null;
SmartPopupTableTool popupTableTool;
ClassLoader myLoader;
final AttributeValueCellEditor myEditor;
/**
* Constructor initialises the table and a popup tool, as well as initialising the required GUI elements. It adds
* action listeners for the three main buttons, which include basic user input validation checking.
*/
public TableAttributeEditor(JFrame MyOwner)
{
// As usual, it is insanely hard to get the swing components to display
// and work properly. If JTable is not displayed in a scroll pane no headers are
// displayed, and you have to do it manually. (If you *do* display it
// in a scrollbar, in this instance, it screws up sizing)
// The broken header mis-feature is only mentioned in the tutorial,
// not in the api doco - go figure.
super();
owner = MyOwner;
// final JPanel mainPanel = (JPanel)this;
tableData = new AttributeTableModel();
attributeTable = new JTable(tableData);
//attributeTable.setRowHeight(20); // This may be needed, depends on how fussy people get about the bottom of letters like 'y' getting cut off when the cell is selected - bug 3013.
popupTableTool = new SmartPopupTableTool(attributeTable, tableData, (JXplorer) owner);
// Set the renderer for the attribute type...
final AttributeTypeCellRenderer typeRenderer = new AttributeTypeCellRenderer();
attributeTable.setDefaultRenderer(AttributeType.class, typeRenderer);
// Set the renderer for the attribute value...
final AttributeValueCellRenderer valueRenderer = new AttributeValueCellRenderer();
attributeTable.setDefaultRenderer(AttributeValue.class, valueRenderer);
// Set the editor for the attribute value...
myEditor = new AttributeValueCellEditor(owner);
attributeTable.setDefaultEditor(AttributeValue.class, myEditor);
attributeTable.getTableHeader().setReorderingAllowed(false);
currentDN = null;
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
buttonPanel.add(submit = new CBButton(CBIntText.get("Submit"), CBIntText.get("Submit your changes to the Directory.")));
buttonPanel.add(reset = new CBButton(CBIntText.get("Reset"), CBIntText.get("Reset this entry i.e. cancels any changes.")));
buttonPanel.add(changeClass = new CBButton(CBIntText.get("Change Classes"), CBIntText.get("Change the Object Class of this entry.")));
buttonPanel.add(opAttrs = new CBButton(CBIntText.get("Properties"), CBIntText.get("View the Operational Attributes of this entry.")));
// I don't really understand why we have to do this...
// but without it these buttons over ride the default
// button (Search Bar's search button), if they have
// been clicked and the user hits the enter key?
opAttrs.setDefaultCapable(false);
submit.setDefaultCapable(false);
reset.setDefaultCapable(false);
changeClass.setDefaultCapable(false);
setLayout(new BorderLayout(10, 10));
tableScroller = new JScrollPane();
attributeTable.setBackground(Color.white);
tableScroller.setPreferredSize(new Dimension(300, 285));
tableScroller.setViewportView(attributeTable);
add(tableScroller, BorderLayout.CENTER);
add(buttonPanel, BorderLayout.SOUTH);
setVisible(true);
// Opens a dialog that displays any operational attributes of the current entry.
opAttrs.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
displayOperationalAttributes();
}
});
reset.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
myEditor.stopCellEditing();
//XXX??? if (attributeTable.isEditing()) myEditor.stopCellEditing();
tableData.reset();
}
});
submit.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
doSubmit();
}
});
// This allows the user to change the objectclass attribute.
// This is pretty tricky, because it changes what attributes are available.
changeClass.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
changeClass();
}
});
attributeTable.addMouseListener(new MouseAdapter()
{
public void mousePressed(MouseEvent e)
{
if (!doPopupStuff(e)) super.mousePressed(e);
}
public void mouseReleased(MouseEvent e)
{
if (!doPopupStuff(e)) super.mouseReleased(e);
}
//TODO need to have a way to call this from a keystroke...
public boolean doPopupStuff(MouseEvent e)
{
if (e.isPopupTrigger() == false) return false;
int row = attributeTable.rowAtPoint(new Point(e.getX(), e.getY()));
attributeTable.clearSelection();
attributeTable.addRowSelectionInterval(row, row);
attributeTable.repaint();
popupTableTool.registerCurrentRow((AttributeType) attributeTable.getValueAt(row, 0), (AttributeValue) attributeTable.getValueAt(row, 1), row, tableData.getRDN()); // active path also set by valueChanged
popupTableTool.show(attributeTable, e.getX(), e.getY());
popupTableTool.registerCellEditor(myEditor); //TE: for bug fix 3107.
return true;
}
});
}
/**
* Opens the change class dialog.
*/
public void changeClass()
{ //JPanel mainPanel
/*
* MINOR MAGIC
*
* This code reuses the 'new entry window'. In order to make things
* sane, we prompt the user to save any serious changes before continuing.
* (Things can get really wierd if the user changes the name and then
* tries to change the objectclass - best to avoid the whole issue.)
*/
myEditor.stopCellEditing();
if (virtualEntry)
{
doVirtualEntryDisplay();
return;
}
/*
* classChangedOriginalEntry saves the original state of the entry
* between visits to NewEntryWin. (- I wonder if it would be neater
* to just reset the 'oldEntry' state of the table every time? ).
* Check it's not been set already (i.e. Pathological User is paying
* multiple visits to the NewEntryWin.)
*/
if (classChangedOriginalEntry == null)
classChangedOriginalEntry = tableData.getOldEntry();
DXEntry newEntry = tableData.getNewEntry();
DN newDN = newEntry.getDN();
/*
* Pathalogical user has messed with the name, *and* wants to
* change the object classes...
*/
if (newDN.equals(classChangedOriginalEntry.getDN()) == false)
{
if (promptForSave(false) == false) // we may need to reset the 'newEntry' data
{ // if the user discards their changes.
tableData.reset(); // resets the table before going on.
newEntry = tableData.getNewEntry();
newDN = newEntry.getDN();
}
else // user has saved data - so now we need to reset the 'classChangedOriginalEntry'
{ // to the changed (and hopefully saved!) data.
// NB: If the directory write fails, then the change classes will also fail...
classChangedOriginalEntry = tableData.getNewEntry();
}
}
/*
* Open NewEntryWin, allowing the user to reset the objectclass attribute.
*/
/*
NewEntryWin userData = new NewEntryWin(newDN.parentDN(), newDN,
dataSource,
newEntry.getAsNonNullAttributes(),
newDN.getLowestRDN().toString(), TableAttributeEditor.this,
CBUtility.getParentFrame(mainPanel));
*/
if (dataSource.getSchemaOps() == null)
{
JOptionPane.showMessageDialog(owner, CBIntText.get("Because there is no schema currently published by the\ndirectory, changing an entry's object class is unavailable."), CBIntText.get("No Schema"), JOptionPane.INFORMATION_MESSAGE);
return;
}
else
{
NewEntryWin userData = new NewEntryWin(dataSource, newDN, newEntry.getAsNonNullAttributes(),
this, CBUtility.getParentFrame(this));
userData.setSize(400, 250);
CBUtility.center(userData, owner); // TE: centres window.
userData.setVisible(true);
}
}
/**
* Kicks off the entry modify/update & checks for manditory attributes.
*/
public void doSubmit()
{
if (dataSource == null)
{
CBUtility.error("No dataSource available to write changes to in Table Attribute Editor");
return;
}
myEditor.stopCellEditing();
// If schema checking is on, make sure that all mandatory attributes are filled in.
if ("false".equalsIgnoreCase(JXplorer.getProperty("option.ignoreSchemaOnSubmission"))
&& (tableData.checkMandatoryAttributesSet() == false))
{
CBUtility.error(TableAttributeEditor.this, CBIntText.get("All Mandatory Attributes must have values!"), null);
return;
}
writeTableData();
}
/**
* Opens a dialog that displays the operational attributes of the current entry.
*/
public void displayOperationalAttributes()
{
JXplorer jx = null;
if (owner instanceof JXplorer)
jx = (JXplorer) owner;
else
return;
String[] opAttrs = new String[]{"createTimestamp", "modifyTimestamp", "creatorsName", "modifiersName", "subschemaSubentry"};
int size = opAttrs.length;
DXEntry entry = null;
try
{
entry = (jx.getSearchBroker()).unthreadedReadEntry(currentDN, opAttrs);
}
catch (NamingException e)
{
CBUtility.error(TableAttributeEditor.this, CBIntText.get("Unable to read entry") + " " + currentDN, e);
}
StringBuffer buffy = new StringBuffer("DN: " + currentDN.toString() + "\n\n");
// Get the attribute values...
for (int i = 0; i < size; i++)
{
DXAttribute att = (DXAttribute) entry.get(opAttrs[i]);
try
{
if (att != null)
{
buffy.append(opAttrs[i] + ": " + att.get().toString() + "\n\n");
}
}
catch (NamingException ee)
{
log.log(Level.WARNING, "Problem accessing Operational Attributes via Table Editor\n", ee);
}
}
// Dialog setup...
JTextArea area = new JTextArea(buffy.toString());
area.setFont(new Font("SansSerif", Font.PLAIN, 11));
area.setLineWrap(true);
area.setWrapStyleWord(true);
JScrollPane scrollPane = new JScrollPane(area);
scrollPane.setPreferredSize(new Dimension(300, 125));
scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
JOptionPane.showMessageDialog(jx, scrollPane, CBIntText.get("Properties (Operational Attributes)"), JOptionPane.INFORMATION_MESSAGE);
}
/**
* This notifies the user that they are about to lose entered data (i.e. they've made changes and are about to a)
* change classes or b) go to another entry).
* @param reset usually prompt for save keeps an internal check to prevent the user being prompted twice for the
* same entry. If this parameter is true, that prompt is reset.
* @return true if data is saved, false if discarded.
*/
public boolean promptForSave(boolean reset)
{
return false;
/* TEMPORARY REMOVAL
if (dataSource == null || dataSource.isActive() == false)
{
return false; // no point prompting - nothing to save with!
}
*/
/*
* Only ever check the entry once (sometimes promptForSave can be called
* multiple time - remember that the 'save' function gets called by a
* separate thread).
*/
/* TEMPORARY REMOVAL
if (reset)
checkedDN = null; // force the prompt to be used.
if (checkedDN == null || checkedDN.equals(tableData.getOldEntry().getDN()) == false)
{
checkedDN = tableData.getOldEntry().getDN();
//Thread.currentThread().dumpStack();
String save = CBIntText.get("Save");
String discard = CBIntText.get("Discard");
int result = JOptionPane.showOptionDialog(CBUtility.getDefaultDisplay(),
CBIntText.get("Submit changes to the Directory?"),
CBIntText.get("Save Data"), JOptionPane.DEFAULT_OPTION,
JOptionPane.QUESTION_MESSAGE, null,
new Object[] {save, discard}, save);
if (result == 0)
{
writeTableData(); // nb - this queues a request to the directory
return true;
}
}
else
{
// do nothing - don't prompt, don't save...
}
return false;
*/
}
/**
* Opens a dialog that asks the user if they want to make a virtual entry a non virtual entry. If the user clicks
* 'Yes' the 'change class' dialog opens.
*/
public void doVirtualEntryDisplay()
{
virtualEntryDialog = new JDialog(owner, CBIntText.get("Virtual Entry"), true);
CBButton btnYes = new CBButton(CBIntText.get("Yes"), CBIntText.get("Click yes to make a Virtual Entry."));
CBButton btnNo = new CBButton(CBIntText.get("No"), CBIntText.get("Click no to cancel without making a Virtual Entry."));
//TE: layout stuff...
Container pane = virtualEntryDialog.getContentPane();
pane.setLayout(new BorderLayout());
CBPanel panel1 = new CBPanel();
CBPanel panel2 = new CBPanel();
CBPanel panel3 = new CBPanel();
panel1.add(new JLabel(CBIntText.get("This entry is a Virtual Entry. Are you sure you want to give this entry an object class?")));
panel2.add(btnYes);
panel2.add(btnNo);
panel3.makeWide();
panel3.addln(panel1);
panel3.addln(panel2);
pane.add(panel3);
btnYes.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
processVirtualEntry();
}
});
btnNo.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
shutVirtualEntryDialog();
}
});
virtualEntryDialog.setSize(475, 125);
CBUtility.center(virtualEntryDialog, owner);
virtualEntryDialog.setVisible(true);
}
/**
* Normally called by the 'Yes' button listener of the virtual entry dialog. This method opens the New Entry dialog
* in simple mode (or Change Classes dialog). If the user selects one or more object classes they are added to the
* entry and displayed in the table editor.
*/
public void processVirtualEntry()
{
NewEntryWin userData = null;
if (dataSource.getSchemaOps() == null)
{
JOptionPane.showMessageDialog(owner, CBIntText.get("Because there is no schema currently published by the\ndirectory, changing an entry's object class is unavailable."), CBIntText.get("No Schema"), JOptionPane.INFORMATION_MESSAGE);
return;
}
else
{
shutVirtualEntryDialog(); //TE: kill the prompt window.
userData = new NewEntryWin(dataSource, currentEntry.getDN(), this, owner, true);
userData.setSize(400, 250);
CBUtility.center(userData, owner); //TE: centres window.
userData.setVisible(true);
while (userData.isVisible()) //TE: don't do anything until the New Entry window is closed.
{
try
{
wait();
}
catch (Exception e)
{
userData.dispose();
}
}
}
if (userData.newObjectClasses != null) //TE: if the user has selected one or more object classes - add them to the entry in the directory.
{
try
{
DXOps dxOps = new DXOps(dataSource.getDirContext());
dxOps.addAttribute(currentEntry.getDN(), userData.newObjectClasses);
dataSource.getEntry(currentEntry.getDN()); //TE: hack?? forces the entry to be read again - otherwise we don't display the naming value.
}
catch (NamingException e)
{
CBUtility.error(TableAttributeEditor.this, CBIntText.get("Unable to add new object classes to {0}.", new String[]{currentEntry.getDN().toString()}), e);
}
}
}
/**
* Disposes of the virtual entry dialog that is opened as a prompt when the user may want to edit a virtual entry.
*/
public void shutVirtualEntryDialog()
{
if (virtualEntryDialog != null)
{
virtualEntryDialog.setVisible(false);
virtualEntryDialog.dispose();
}
}
//
// DN checkedDN;
/**
* <p>Displays data that can be modified by the user in a table.</p>
* @param entry the entry to be displayed by all the editors
* @param ds the datasource the editors may use for more info
*/
public void displayEntry(DXEntry entry, DataSource ds)
{
// checkedDN = null; // hack - resets promptForSave.
// Set the globals...
currentEntry = entry;
dataSource = ds;
if (entry != null && entry.size() == 0)
{
// If there is an entry and it's size is zero - it's probably is a virtual entry.
// We need to give the user the option of adding an object class to it i.e. so that
// it can be added to the directory as a real entry.
//
// Disable all the buttons except the 'Change Class' button - but rename this button
// to 'Add Class' so the user hopefully has a bit more of an idea about what is going on.
// Sets editor to a blank screen...
tableData.clear();
// Disable all buttons except the 'Change Class' button - rename this one...
submit.setEnabled(false);
reset.setEnabled(false);
changeClass.setText(CBIntText.get("Add Class"));
changeClass.setEnabled(true);
opAttrs.setEnabled(false);
virtualEntry = true;
return;
}
virtualEntry = false;
// Isn't a virtual entry...
if (entry != null)
currentDN = entry.getDN();
// May have been changed to 'Add Class'...
changeClass.setText(CBIntText.get("Change Class"));
// Some quick faffing around, to see if we're coming back from a
// change classes operation.
if (classChangedOriginalEntry != null)
{
// If they have the same name, then we're reviewing the same entry - otherwise we've moved on
if (entry == null || entry.getDN().equals(classChangedOriginalEntry.getDN()) == false)
classChangedOriginalEntry = null;
}
/*
* Check that we're not displaying a new entry, and leaving unsaved changes
* behind.
*
* This turns out to be quite tricky, and involves a bunch 'o special cases.
*
* First check whether the table data has changed (if not, do nothing)
* -> if the new entry is null, prompt user to save
* -> OR if the DN has changed, and it wasn't due to a rename, prompt user to save
*
*/
if (tableData.changedByUser()) // user made changes - were they saved? (i.e., are we
{ // displaying the result of those changes?)
boolean prompt = false;
DXEntry oldEntry = tableData.getOldEntry();
if (oldEntry != null)
{
/*
* The code below is simply checking to see if the name of the
* new entry is different from the old entry, and if it is,
* whether that's due to the old entry being renamed.
*/
if (entry == null)
{
prompt = true;
}
//TE: added the isEmpty check see bug: 3194.
else if (!oldEntry.getDN().isEmpty() && entry.getDN().equals(oldEntry.getDN()) == false)
{
DN oldParent = oldEntry.getDN().parentDN();
DN newParent = entry.getDN().parentDN();
if (oldParent.equals(newParent) == false)
{
prompt = true;
}
else
{
if (entry.getDN().getLowestRDN().equals(tableData.getRDN()) == false)
{
prompt = true;
}
}
}
if (prompt) // yes, there is a risk of data loss - prompt the user.
{
if (promptForSave(false)) // see if the user wants to save their data
{
//dataSource.getEntry(entry.getDN()); // queue a request to redisplay
return; // this entry (but don't show it this time around).
}
}
}
}
myEditor.setDataSource(ds); // Sets the DataSource in AttributeValueCellEditor used to get the syntax of attributes.
// only enable buttons if DataSource
// is valid *and* we can modify data...
if (dataSource == null || entry == null)
{
submit.setEnabled(false);
reset.setEnabled(false);
changeClass.setEnabled(false);
opAttrs.setEnabled(false);
}
else
{
submit.setEnabled(true);
reset.setEnabled(true);
opAttrs.setEnabled(true);
if (entry.get("objectclass") != null) // only allow class changes if we can find
changeClass.setEnabled(true); // some to start with!
}
myEditor.stopCellEditing();
if (entry != null)
{
entry.expandAllAttributes();
currentDN = entry.getDN();
tableData.insertAttributes(entry);
popupTableTool.setDN(currentDN); // Sets the DN in SmartPopupTableTool.
myEditor.setDN(currentDN); // Sets the DN in the attributeValueCellEditor which can be used to identify the entry that is being modified/
}
else
{
tableData.clear(); // Sets editor to a blank screen.
}
tableScroller.getVerticalScrollBar().setValue(0); // Sets the scroll bar back to the top.
}
public JComponent getDisplayComponent()
{
validate();
repaint();
return this;
}
public String[] getAttributeValuesAsStringArray(Attribute a)
throws NamingException
{
if (a == null) return new String[0];
DXNamingEnumeration e = new DXNamingEnumeration(a.getAll());
if (e == null) return new String[0];
return e.toStringArray();
}
/**
* Test whether the (unordered) object class lists of two attributes contain the same
*/
public boolean objectClassesChanged(DXAttributes a, DXAttributes b)
{
boolean result = false;
try
{
String[] A = getAttributeValuesAsStringArray(a.getAllObjectClasses());
String[] B = getAttributeValuesAsStringArray(b.getAllObjectClasses());
Object[] test = CBArray.difference(A, B);
if (test.length > 0) result = true;
test = CBArray.difference(B, A);
if (test.length > 0) result = true;
return result;
}
catch (NamingException e)
{
log.log(Level.WARNING, "Error in TableAttributeEditor:objectClassesChanged ", e);
return true;
}
}
/**
* Writes the data currently in the table editor to the directory.
*/
public void writeTableData()
{
myEditor.stopCellEditing();
if (dataSource == null) // if ds is null, data is not modifiable...
{
CBUtility.error("no datasource to write data to in writeTableData()");
return;
} // shouldn't happen
DXEntry oldEntry = tableData.getOldEntry();
DXEntry newEntry = tableData.getNewEntry();
/* Check to see if major surgery is needed - whether the user has been
* messing with the object class list. */
if (classChangedOriginalEntry != null)
{
// use the saved state of the pre-class-changed entry as the 'old entry'
// state.
oldEntry = classChangedOriginalEntry;
classChangedOriginalEntry = null; // this is only used once! (either the object class change will
// now succeed, or fail - either way, the entry state is reset to
// match what's in the directory.)
if (objectClassesChanged(oldEntry, newEntry))
{
oldEntry.removeEmptyAttributes();
newEntry.setStatus(oldEntry.getStatus());
Object[] delSet = CBArray.difference(oldEntry.toIDStringArray(), newEntry.toIDStringArray());
/* if there *are* attributes that should no longer exist, delete them by adding them (blanked)
* to the complete 'newAtts' set of *all* known attributes. */
if ((delSet != null) && (delSet.length > 0))
{
for (int i = 0; i < delSet.length; i++)
{
newEntry.put(new DXAttribute(delSet[i].toString())); // overwrite old values with an empty attribute
}
}
}
}
dataSource.modifyEntry(oldEntry, newEntry);
}
/**
* Return the thingumy that should be printed.
*/
public Component getPrintComponent()
{
return attributeTable;
}
/**
* This editor is happy to be used in conjunction with other editors...
*/
public boolean isUnique()
{
return false;
}
public String getName()
{
return CBIntText.get("Table Editor");
}
public ImageIcon getIcon()
{
return new ImageIcon(Theme.getInstance().getDirImages() + "table.gif");
} //TE: returns an icon.
public String getToolTip()
{
return CBIntText.get("The table editor is generally used for editing data, it also functions perfectly well as a simple, but robust, entry viewer.");
} //TE: returns a tool tip.
public DataSink getDataSink()
{
return this;
}
public boolean canCreateEntry()
{
return true;
}
public void registerComponents(JMenuBar menu, JToolBar buttons, JTree tree, JPopupMenu treeMenu, JFrame jx)
{
}
public void unload()
{
}
/**
* Use the default tree icon system based on naming value or object class.
*/
public ImageIcon getTreeIcon(String rdn)
{
return null;
}
/**
* Use the default popupmenu.
*/
public JPopupMenu getPopupMenu(String rdn)
{
return null;
}
/**
* Don't hide sub entries.
*/
public boolean hideSubEntries(String rdn)
{
return false;
}
/**
* Optionally register a new class loader for atribute value viewers to use.
*/
public void registerClassLoader(ClassLoader loader)
{
myLoader = loader;
myEditor.registerClassLoader(loader);
}
public void setVisible(boolean state)
{
super.setVisible(state);
// has to be *after* previous call for SwingMagic reasons.
if (state == false && tableData.changedByUser()) // user made changes - were they saved? (i.e., are we
{
/*
* The setVisible() method may be called multiple time. Only prompt
* the user the first time.
*/
promptForSave(false);
}
}
}