/*******************************************************************************
* 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.internal;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
import javax.swing.JOptionPane;
import javax.swing.filechooser.FileSystemView;
import org.eclipse.persistence.tools.workbench.framework.NodeManager;
import org.eclipse.persistence.tools.workbench.framework.OpenException;
import org.eclipse.persistence.tools.workbench.framework.Plugin;
import org.eclipse.persistence.tools.workbench.framework.UnsupportedFileException;
import org.eclipse.persistence.tools.workbench.framework.app.ApplicationNode;
import org.eclipse.persistence.tools.workbench.framework.context.WorkbenchContext;
import org.eclipse.persistence.tools.workbench.uitools.CancelException;
import org.eclipse.persistence.tools.workbench.uitools.PreferencesRecentFilesManager;
import org.eclipse.persistence.tools.workbench.uitools.RecentFilesManager;
import org.eclipse.persistence.tools.workbench.uitools.app.TreeNodeValueModel;
import org.eclipse.persistence.tools.workbench.utility.ClassTools;
import org.eclipse.persistence.tools.workbench.utility.HashBag;
import org.eclipse.persistence.tools.workbench.utility.SynchronizedBoolean;
import org.eclipse.persistence.tools.workbench.utility.events.AWTChangeNotifier;
import org.eclipse.persistence.tools.workbench.utility.events.ChangeNotifier;
import org.eclipse.persistence.tools.workbench.utility.events.DefaultChangeNotifier;
import org.eclipse.persistence.tools.workbench.utility.node.AbstractNodeModel;
import org.eclipse.persistence.tools.workbench.utility.node.Node;
import org.eclipse.persistence.tools.workbench.utility.node.PluggableValidator;
import org.eclipse.persistence.tools.workbench.utility.node.RunnableValidation;
/**
* This is the model that corresponds to the root node in all the
* navigator trees. It's a little different than a typical model, in that it
* holds on to application nodes directly, as opposed to holding on
* to the models corresponding to those application nodes.
*/
final class FrameworkNodeManager
extends AbstractNodeModel
implements NodeManager
{
/** Backpointer to the application. */
private FrameworkApplication application;
private TreeNodeValueModel rootNode;
private Collection projectNodes;
public static final String PROJECT_NODES_COLLECTION = "projectNodes";
/** The validation threads and flags, keyed by the project nodes. */
private Map validationThreads;
private Map continueValidationThreadFlags;
/** The project nodes with synchronous validators - used only at development time. */
private Collection synchronousProjectNodes;
private boolean projectNodesAreAddedWithSynchronousValidators;
/** Maintain a list of the recently-opened files - save as a preference. */
private RecentFilesManager recentFilesManager;
private static final String RECENT_FILES_PREFERENCES_NODE = "recent files";
static final String RECENT_FILES_MAX_SIZE_PREFERENCE = "recent files max size";
static final int RECENT_FILES_MAX_SIZE_PREFERENCE_DEFAULT = RecentFilesManager.DEFAULT_MAX_SIZE;
private static final String MOST_RECENT_SAVE_LOCATION_PREFERENCE = "recent save location";
private static final String MOST_RECENT_SAVE_LOCATION_PREFERENCE_DEFAULT = FileSystemView.getFileSystemView().getDefaultDirectory().getAbsolutePath();
// ********** constructors/initialization **********
FrameworkNodeManager(FrameworkApplication application) {
super(null); // this node never has a parent
this.application = application;
this.recentFilesManager = this.buildRecentFilesManager();
}
protected void initialize() {
super.initialize();
this.rootNode = new FrameworkRootNode(this);
this.projectNodes = new ArrayList();
this.validationThreads = new HashMap();
this.continueValidationThreadFlags = new HashMap();
this.synchronousProjectNodes = new HashBag();
this.projectNodesAreAddedWithSynchronousValidators = false;
}
/**
* Build a recent files manager that stores the recently-opened
* files in a preferences node.
*/
private RecentFilesManager buildRecentFilesManager() {
Preferences baseNode = this.application.generalPreferences();
Preferences recentFilesNode = baseNode.node(RECENT_FILES_PREFERENCES_NODE);
return new PreferencesRecentFilesManager(recentFilesNode, baseNode, RECENT_FILES_MAX_SIZE_PREFERENCE);
}
protected void checkParent(Node parent) {
if (parent != null) {
throw new IllegalArgumentException(ClassTools.shortClassNameForObject(this) + " should not have a parent");
}
}
// ********** NodeManager implementation **********
/**
* @see org.eclipse.persistence.tools.workbench.framework.NodeManager#addProjectNode(org.eclipse.persistence.tools.workbench.framework.app.ApplicationNode)
* Install the appropriate validator on the "project-level" node
* and add it to our collection of "project-level" nodes.
*/
public void addProjectNode(ApplicationNode projectNode) {
// adding the project node to the tree will trigger the building
// of all the application nodes;
// so do this *before* kicking off the validation thread, so
// that all the nodes' "application" problems are in synch with and
// listening to the "model" problems
this.addItemToCollection(projectNode, this.projectNodes, PROJECT_NODES_COLLECTION);
((Node) projectNode.getValue()).setChangeNotifier(AWTChangeNotifier.instance());
if (this.projectNodesAreAddedWithSynchronousValidators) {
this.installSynchronousValidatorOn(projectNode);
} else {
this.installAsynchronousValidatorOn(projectNode);
}
}
/**
* Build, cache, and start a validation thread for the specified "project-level" node.
* Hook up the thread to the node via an asynchronous validator.
*/
private void installAsynchronousValidatorOn(ApplicationNode projectNode) {
Node node = (Node) projectNode.getValue();
// the "validate" flag is shared by the node's validator and the validation thread;
// initialize it to true so the the node is immediately validated
SynchronizedBoolean validateFlag = new SynchronizedBoolean(true);
node.setValidator(PluggableValidator.buildAsynchronousValidator(validateFlag));
// the "continue" flag is shared by this node manager and the validation thread
SynchronizedBoolean continueFlag = new SynchronizedBoolean(true);
this.continueValidationThreadFlags.put(projectNode, continueFlag);
Thread validationThread =
new Thread(
new RunnableValidation(
node,
validateFlag,
continueFlag,
this.logger(),
Level.WARNING,
"VALIDATION_EXCEPTION"
),
"Validation Thread : " + node.displayString()
);
validationThread.setPriority(Thread.MIN_PRIORITY);
this.validationThreads.put(projectNode, validationThread);
validationThread.start();
}
/**
* Install a "synchronous" validator on the specified "project-level" node.
* This simplifies debugging of model validation.
* This should only be used during development.
*/
private void installSynchronousValidatorOn(ApplicationNode projectNode) {
Node node = (Node) projectNode.getValue();
node.setValidator(PluggableValidator.buildSynchronousValidator(node));
this.synchronousProjectNodes.add(projectNode);
}
/**
* @see org.eclipse.persistence.tools.workbench.framework.NodeManager#removeProjectNode(org.eclipse.persistence.tools.workbench.framework.app.ApplicationNode)
*/
public void removeProjectNode(ApplicationNode projectNode) {
this.removeItemFromCollection(projectNode, this.projectNodes, PROJECT_NODES_COLLECTION);
if ( ! this.synchronousProjectNodes.remove(projectNode)) {
SynchronizedBoolean continueFlag = (SynchronizedBoolean) this.continueValidationThreadFlags.remove(projectNode);
continueFlag.setFalse();
Thread validationThread = (Thread) this.validationThreads.remove(projectNode);
validationThread.interrupt();
}
}
/**
* @see org.eclipse.persistence.tools.workbench.framework.NodeManager#projectNodesFor(org.eclipse.persistence.tools.workbench.framework.Plugin)
*/
public ApplicationNode[] projectNodesFor(Plugin plugin) {
Collection nodes = new ArrayList();
for (Iterator stream = this.projectNodes(); stream.hasNext(); ) {
ApplicationNode node = ((ApplicationNode) stream.next());
if (node.getPlugin() == plugin) {
nodes.add(node);
}
}
return (ApplicationNode[]) nodes.toArray(new ApplicationNode[nodes.size()]);
}
/**
* @see org.eclipse.persistence.tools.workbench.framework.NodeManager#getRootNode()
*/
public TreeNodeValueModel getRootNode() {
return this.rootNode;
}
// ********** AbstractNodeModel implementation **********
/**
* @see org.eclipse.persistence.tools.workbench.utility.node.Node#displayString()
*/
public String displayString() {
// this node should never be visible
return null;
}
/**
* @see org.eclipse.persistence.tools.workbench.utility.node.AbstractNodeModel#getChangeNotifier()
*/
public ChangeNotifier getChangeNotifier() {
return DefaultChangeNotifier.instance();
}
/**
* @see org.eclipse.persistence.tools.workbench.utility.node.AbstractNodeModel#getValidator()
*/
public Validator getValidator() {
return Node.NULL_VALIDATOR;
}
// ********** queries **********
/**
* Return the "project-level" nodes held by the node manager.
*/
Iterator projectNodes() {
return this.projectNodes.iterator();
}
/**
* Return the number of "project-level" nodes held by the node manager.
*/
int projectNodesSize() {
return this.projectNodes.size();
}
/**
* Return the recent files manager that maintains a list
* of the recently-opened files.
*/
RecentFilesManager getRecentFilesManager() {
return this.recentFilesManager;
}
boolean projectNodesAreAddedWithSynchronousValidators() {
return this.projectNodesAreAddedWithSynchronousValidators;
}
/**
* Return the new setting.
*/
boolean toggleAddProjectNodesWithSynchronousValidators() {
this.projectNodesAreAddedWithSynchronousValidators = ! this.projectNodesAreAddedWithSynchronousValidators;
return this.projectNodesAreAddedWithSynchronousValidators;
}
private Logger logger() {
return this.application.getLogger();
}
// ********** opening nodes **********
/**
* Prevent the user from the opening the same file twice.
* Ask the user to revert to saved if the file is already open and modified.
*/
void open(File file, WorkbenchContext context) {
ApplicationNode projectNode = this.projectNodeFor(file);
if (projectNode == null) {
this.openNew(file, context);
return;
}
if (projectNode.isDirty() && this.userWantsToRevert(file, context)) {
// remove the node and re-read the file
context.getNavigatorSelectionModel().pushExpansionState();
this.removeProjectNode(projectNode);
this.openNew(file, context);
context.getNavigatorSelectionModel().popAndRestoreExpansionState();
} else {
// select the node corresponding to the selected file
context.getNavigatorSelectionModel().setSelectedNode(projectNode);
}
}
/**
* Return the "project-level" node corresponding to the specified file.
* Return null if there is no corresponding node.
*/
private ApplicationNode projectNodeFor(File file) {
for (Iterator stream = this.projectNodes(); stream.hasNext(); ) {
ApplicationNode projectNode = (ApplicationNode) stream.next();
File saveLocation = projectNode.saveFile();
if ((saveLocation != null) && saveLocation.equals(file)) {
return projectNode;
}
}
return null;
}
/**
* Fork off a thread to load the project for the specified file,
* allowing us to return control to the caller (typically an action).
*/
private void openNew(File file, WorkbenchContext context) {
Thread thread = new Thread(new RunnableProjectLoader(this, file, context), "Project Loader");
thread.setPriority(Thread.NORM_PRIORITY);
thread.start();
}
/**
* This is called by the ProjectLoader once the "Wait..." dialog has
* been launched. Synchronize this method so multiple
* ProjectLoaders will not interfere with each other.
*/
synchronized ApplicationNode openCallback(File file, WorkbenchContext context) throws UnsupportedFileException, OpenException {
return this.application.open(file, context);
}
/**
* Open the specified file and add the new node to the node manager's
* "project-level" nodes. Also select the new node in the current navigator
* and add the file to the recent files list.
* This is called by the ProjectLoader once a project has
* been successfully read in. This method should not need
* to be synchronized since it should always be called in
* the AWT event dispatcher thread.
*/
void addProjectNodeCallback(ApplicationNode node, File file, WorkbenchContext context) {
this.addProjectNode(node);
context.getNavigatorSelectionModel().expandNode(node);
context.getNavigatorSelectionModel().setSelectedNode(node);
this.recentFilesManager.setMostRecentFile(file);
}
/**
* Ask the user whether she wants to revert to the saved file.
* Return whether she answers yes.
*/
private boolean userWantsToRevert(File file, WorkbenchContext context) {
int option = JOptionPane.showConfirmDialog(
context.getCurrentWindow(),
context.getApplicationContext().getResourceRepository().getString("REVERT_TO_SAVED.message", file),
context.getApplicationContext().getResourceRepository().getString("REVERT_TO_SAVED.title"),
JOptionPane.YES_NO_OPTION
);
return (option == JOptionPane.YES_OPTION);
}
// ********** closing nodes **********
/**
* First check for any dirty projects; if there are any, prompt the
* user to save them. Then exit the application.
* This method is called in 2 situations:
* - when the user selects File -> Exit
* - when the user closes the last window
*/
void exit(WorkbenchContext context) {
Collection nodes = new ArrayList(this.projectNodes);
this.application.saveTreeExpansionStates();
if (this.closeAll(context)) {
// TODO - something else once we have multi-window support ~kfm
saveProjectsState(nodes.iterator());
nodes = null;
this.application.exit();
}
}
//TODO possibly maintain this as we go along?
private void saveProjectsState(Iterator nodes) {
Preferences projectPreferences = this.application.generalPreferences().node("projects");
try {
projectPreferences.clear();
} catch (BackingStoreException e) {
//do nothing if this occurs
}
int i = 0;
while (nodes.hasNext()) {
ApplicationNode projectNode = (ApplicationNode) nodes.next();
if (projectNode.saveFile() != null) {
projectPreferences.put(String.valueOf(i++), projectNode.saveFile().getAbsolutePath());
}
}
}
protected void restoreProjectsState(WorkbenchWindow window, Preferences windowPreferences) {
Preferences projectPreferences = this.application.generalPreferences().node("projects");
String[] keys;
try {
keys = projectPreferences.keys();
} catch (BackingStoreException e) {
return;
}
for (int i = 0; i < keys.length; i++) {
String projectLocation = projectPreferences.get(keys[i], null);
if (projectLocation != null) {
File projectFile = new File(projectLocation);
if (projectFile.exists()) {
open(projectFile, window.getContext());
}
}
}
window.restoreTreeExpansionState(windowPreferences);
}
/**
* Close all the nodes currently held by the node manager.
* Return whether the nodes were (saved and) closed successfully.
*/
boolean closeAll(WorkbenchContext context) {
return close((ApplicationNode[]) this.projectNodes.toArray(new ApplicationNode[this.projectNodes.size()]), context);
}
/**
* Close all the nodes currently held by the node manager.
* Return whether the nodes were (saved and) closed successfully.
*/
boolean close(ApplicationNode[] nodes, WorkbenchContext context) {
Collection dirtyNodesToSave;
try {
dirtyNodesToSave = this.promptToSave(this.dirtyNodesFrom(nodes), context);
} catch (CancelException e) {
// if the user cancels the save dialog, cancel the entire close process
return false;
}
for (Iterator stream = dirtyNodesToSave.iterator(); stream.hasNext(); ) {
if ( ! this.save((ApplicationNode) stream.next(), context)) {
// if any of the saves fail, cancel the entire close process
return false;
}
}
for (int i = nodes.length; i-- > 0; ) {
this.removeProjectNode(nodes[i]);
}
return true;
}
/**
* Extract a collection of the dirty nodes from the specified
* collection of nodes.
*/
private Collection dirtyNodesFrom(ApplicationNode[] nodes) {
Collection dirtyNodes = new ArrayList(nodes.length);
for (int i = nodes.length; i-- > 0; ) {
ApplicationNode node = nodes[i];
if (node.isDirty()) {
dirtyNodes.add(node);
}
}
return dirtyNodes;
}
/**
* Display a dialog to the user with a list of the "projects" that are dirty
* and need to be saved. Return a collection of the dirty "projects"
* selected by the user to be saved.
*/
private Collection promptToSave(Collection dirtyNodes, WorkbenchContext context) {
if (dirtyNodes.isEmpty()) {
return Collections.EMPTY_SET;
}
SaveModifiedProjectsDialog dialog = new SaveModifiedProjectsDialog(context, dirtyNodes);
dialog.show();
if (dialog.wasCanceled()) {
throw new CancelException();
}
return dialog.selectedNodes();
}
// ********** saving nodes **********
/**
* Save the specified node and, if it was saved successfully,
* add it to the recent files list. Return whether the node was saved.
*/
public boolean save(ApplicationNode node, WorkbenchContext workbenchContext) {
boolean saved = node.save(getMostRecentSaveDirectory(), workbenchContext);
if (saved) {
this.recentFilesManager.setMostRecentFile(node.getProjectRoot().saveFile());
setMostRecentSaveDirectory(node.getProjectRoot().saveFile());
}
return saved;
}
private File getMostRecentSaveDirectory() {
return new File(this.application.generalPreferences().get(MOST_RECENT_SAVE_LOCATION_PREFERENCE, MOST_RECENT_SAVE_LOCATION_PREFERENCE_DEFAULT));
}
private void setMostRecentSaveDirectory(File saveLocation) {
this.application.generalPreferences().put(MOST_RECENT_SAVE_LOCATION_PREFERENCE, saveLocation.getParentFile().getAbsolutePath());
}
/**
* Save the specified node in a new location and, if it was saved successfully,
* add it to the recent files list. Return whether the node was saved.
*/
boolean saveAs(ApplicationNode node, WorkbenchContext context) {
boolean saved = node.saveAs(getMostRecentSaveDirectory(), context);
if (saved) {
this.recentFilesManager.setMostRecentFile(node.getProjectRoot().saveFile());
setMostRecentSaveDirectory(node.getProjectRoot().saveFile());
}
return saved;
}
/**
* Save all the dirty nodes and, if they were saved successfully,
* add them to the recent files list.
*/
void saveAll(WorkbenchContext workbenchContext) {
for (Iterator stream = this.projectNodes(); stream.hasNext(); ) {
ApplicationNode node = (ApplicationNode) stream.next();
if (node.isDirty()) {
this.save(node, workbenchContext);
}
}
}
}