/* $Id: ProjectManager.java 18978 2011-01-24 18:25:30Z linus $
*****************************************************************************
* Copyright (c) 2009 Contributors - see below
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* thn
*****************************************************************************
*
* Some portions of this file was previously release using the BSD License:
*/
// Copyright (c) 1996-2008 The Regents of the University of California. All
// Rights Reserved. Permission to use, copy, modify, and distribute this
// software and its documentation without fee, and without a written
// agreement is hereby granted, provided that the above copyright notice
// and this paragraph appear in all copies. This software program and
// documentation are copyrighted by The Regents of the University of
// California. The software program and documentation are supplied "AS
// IS", without any accompanying services from The Regents. The Regents
// does not warrant that the operation of the program will be
// uninterrupted or error-free. The end-user understands that the program
// was developed for research purposes and is advised not to rely
// exclusively on the program for any reason. IN NO EVENT SHALL THE
// UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
// SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS,
// ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
// THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE. THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY
// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
// PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
// CALIFORNIA HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT,
// UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
package org.argouml.kernel;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import javax.swing.Action;
import javax.swing.event.EventListenerList;
import org.apache.log4j.Logger;
import org.argouml.application.events.ArgoEventPump;
import org.argouml.application.events.ArgoEventTypes;
import org.argouml.application.events.ArgoStatusEvent;
import org.argouml.cognitive.Designer;
import org.argouml.i18n.Translator;
import org.argouml.model.Model;
import org.argouml.model.ModelCommand;
import org.argouml.model.ModelCommandCreationObserver;
import org.argouml.profile.Profile;
import org.argouml.profile.ProfileException;
import org.argouml.uml.cognitive.ProjectMemberTodoList;
import org.argouml.uml.diagram.ArgoDiagram;
import org.argouml.uml.diagram.DiagramFactory;
/**
* This class manages the projects loaded in argouml,
* and what the current project is. <p>
*
* Classes in ArgoUML can ask this class for the current
* project and set the current project. Since we only have one
* project in ArgoUML at the moment, this class does not manage a list
* of projects like one would expect. This could be a nice extension
* for the future of ArgoUML. As soon as the current project is
* changed, a property changed event is fired. <p>
*
* TODO: Move everything related to the creation of a project
* into the ProjectFactory.
*
* @since Nov 17, 2002
* @author jaap.branderhorst@xs4all.nl
* @stereotype singleton
*/
public final class ProjectManager implements ModelCommandCreationObserver {
/**
* The name of the property that defines the current project. The values
* passed are Projects, not Strings. The 'name' here refers to the name
* of this property, not the name of the project.
*
* @deprecated for 0.27.2 by tfmorris. Listeners of this event which expect
* it to indicate a new project being opened should listen for
* {@link #OPEN_PROJECTS_PROPERTY}. Listeners who think
* they need to know a single global current project need
* to be changed to deal with things on a per-project basis.
*/
@Deprecated
public static final String CURRENT_PROJECT_PROPERTY_NAME = "currentProject";
/**
* Property name for list of current open projects. Old and new property
* values are of type Project[] (arrays of Projects).
*/
public static final String OPEN_PROJECTS_PROPERTY = "openProjects";
private static final Logger LOG = Logger.getLogger(ProjectManager.class);
/**
* The singleton instance of this class.
*/
private static ProjectManager instance = new ProjectManager();
/**
* The project that is visible in the projectbrowser.
*/
private static Project currentProject;
private static LinkedList<Project> openProjects = new LinkedList<Project>();
/**
* Flag to indicate we are creating a new current project.
* TODO: This isn't a thread-safe way of doing mutual exclusion.
*/
private boolean creatingCurrentProject;
private Action saveAction;
/**
* The listener list.
*/
private EventListenerList listenerList = new EventListenerList();
/**
* The event to fire.
*
* TODO: Investigate! Is the purpose really to let the next call to
* {@link #firePropertyChanged(String, Object, Object)} fire the old
* event again if the previous invocation resulted in an exception?
* If so, please document why. If not, fix it.
*/
private PropertyChangeEvent event;
/**
* The singleton accessor method of this class.
*
* @return The singleton.
*/
public static ProjectManager getManager() {
return instance;
}
/**
* Constructor for ProjectManager.
*/
private ProjectManager() {
super();
Model.setModelCommandCreationObserver(this);
}
/**
* Adds a listener to the listener list.
*
* @param listener The listener to add.
*/
public void addPropertyChangeListener(PropertyChangeListener listener) {
listenerList.add(PropertyChangeListener.class, listener);
}
/**
* Removes a listener from the listener list.
*
* @param listener The listener to remove.
*/
public void removePropertyChangeListener(PropertyChangeListener listener) {
listenerList.remove(PropertyChangeListener.class, listener);
}
/**
* Fire an event to all members of the listener list.
*
* @param propertyName The name of the event.
* @param oldValue The old value.
* @param newValue The new value.
*/
void firePropertyChanged(String propertyName,
Object oldValue, Object newValue) {
// Guaranteed to return a non-null array
Object[] listeners = listenerList.getListenerList();
// Process the listeners last to first, notifying
// those that are interested in this event
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == PropertyChangeListener.class) {
// Lazily create the event:
if (event == null) {
event =
new PropertyChangeEvent(
this,
propertyName,
oldValue,
newValue);
}
((PropertyChangeListener) listeners[i + 1]).propertyChange(
event);
}
}
event = null;
}
/**
* Sets the current project (the project that is viewable in the
* projectbrowser). Sets the current diagram for the project (if one
* exists). This method fires a propertychanged event.
* <p>
* If the argument is null, then the current project will be forgotten
* about.
*
* @param newProject The new project.
* @deprecated for 0.27.2 by tfmorris. There is no longer the concept of a
* single global "current" project. In the future, multiple
* projects will be able to be open at a time, so all code
* should be prepared to deal with multiple projects and should
* require a Project to be passed as an argument if they need
* access.
*/
public void setCurrentProject(Project newProject) {
Project oldProject = currentProject;
currentProject = newProject;
addProject(newProject);
if (currentProject != null
&& currentProject.getActiveDiagram() == null) {
List<ArgoDiagram> diagrams = currentProject.getDiagramList();
if (diagrams != null && !diagrams.isEmpty()) {
ArgoDiagram activeDiagram = diagrams.get(0);
currentProject.setActiveDiagram(activeDiagram);
}
}
notifyProjectAdded(newProject, oldProject);
}
private void notifyProjectAdded(Project newProject, Project oldProject) {
firePropertyChanged(CURRENT_PROJECT_PROPERTY_NAME,
oldProject, newProject);
// TODO: Tentative implementation. Do we want something that updates
// the list of open projects or just simple open and close events? -tfm
firePropertyChanged(OPEN_PROJECTS_PROPERTY,
new Project[] {oldProject}, new Project[] {newProject});
}
/**
* Returns the current project (ie the project which must recently had the
* user focus) or null if there is no current project.
* <p>
* This should only be used by callers who need to know the global state.
* Most things which need a project want the project that contains them,
* which they can discover by traversing their containing elements (e.g.
* Fig->Diagram->DiagramSettings).
* <p>
*
* @return Project the current project or null if none
* @deprecated for 0.27.2 by tfmorris. There is no longer the concept of a
* single global "current" project. In the future, multiple
* projects will be able to be open at a time, so all code
* should be prepared to deal with multiple projects and should
* require a Project to be passed as an argument if they need
* access. To get a list of all currently open projects, use
* {@link #getOpenProjects()}. For settings which affect
* renderings in diagrams use
* {@link org.argouml.uml.diagram.ui.ArgoFig#getSettings()}.
*/
@Deprecated
public Project getCurrentProject() {
return currentProject;
}
/**
* @return a list of the currently open Projects in the order they were
* opened
*/
public List<Project> getOpenProjects() {
List<Project> result = new ArrayList<Project>();
if (currentProject != null) {
result.add(currentProject);
}
return result;
}
/**
* Makes an empty project.
* @return Project the empty project
*/
public Project makeEmptyProject() {
return makeEmptyProject(true);
}
/**
* Make a new empty project optionally including default diagrams.
* <p>
* Historically new projects have been created with two default diagrams
* (Class and Use Case). NOTE: ArgoUML currently requires at least one
* diagram for proper operation.
*
* @param addDefaultDiagrams
* if true the project will be be created with the two standard
* default diagrams (Class and Use Case)
* @return Project the newly created project
*/
public Project makeEmptyProject(final boolean addDefaultDiagrams) {
final Command cmd = new NonUndoableCommand() {
@Override
public Object execute() {
Model.getPump().stopPumpingEvents();
creatingCurrentProject = true;
LOG.info("making empty project");
Project newProject = new ProjectImpl();
createDefaultModel(newProject);
if (addDefaultDiagrams) {
createDefaultDiagrams(newProject);
}
creatingCurrentProject = false;
setCurrentProject(newProject);
Model.getPump().startPumpingEvents();
return null;
}
};
cmd.execute();
currentProject.getUndoManager().addCommand(cmd);
setSaveEnabled(false);
return currentProject;
}
/**
* Makes an empty profile project.
* @return Project the empty profile project
*/
public Project makeEmptyProfileProject() {
return makeEmptyProfileProject(true);
}
/**
* Make a new empty profile project optionally including default diagrams.
* <p>
* Historically new projects have been created with two default diagrams
* (Class and Use Case). NOTE: ArgoUML currently requires at least one
* diagram for proper operation.
*
* @param addDefaultDiagrams
* if true the project will be be created with the standard
* default diagram (Class)
* @return Project the newly created profile project
*/
public Project makeEmptyProfileProject(final boolean addDefaultDiagrams) {
final Command cmd = new NonUndoableCommand() {
@Override
public Object execute() {
Model.getPump().stopPumpingEvents();
creatingCurrentProject = true;
LOG.info("making empty profile project");
Project newProject = new ProjectImpl(Project.PROFILE_PROJECT);
createDefaultProfile(newProject);
if (addDefaultDiagrams) {
ArgoDiagram d = createClassDiagram(newProject);
createTodoList(newProject);
newProject.setActiveDiagram(d);
}
creatingCurrentProject = false;
setCurrentProject(newProject);
Model.getPump().startPumpingEvents();
return null;
}
};
cmd.execute();
currentProject.getUndoManager().addCommand(cmd);
setSaveEnabled(false);
return currentProject;
}
/**
* Apply all profiles from the profile configuration to a model (can be a
* profile too).
*
* @param project The project with the profile configuration.
* @param model The model to apply the profiles to.
*/
private void applyProfileConfiguration(Project project, Object model) {
Collection<Profile> c =
project.getProfileConfiguration().getProfiles();
if (c != null) {
for (Profile p : c) {
try {
for (Object profile : p.getProfilePackages()) {
Model.getExtensionMechanismsHelper()
.applyProfile(model, profile);
}
} catch (ProfileException pe) {
LOG.warn("Failed to get profile packages from profile "
+ p.getDisplayName());
}
}
}
}
/**
* Create the default diagrams for the project. Currently a Class Diagram
* and a UseCase diagram.
*
* @param project the project to create the diagrams in.
*/
private void createDefaultDiagrams(Project project) {
LOG.debug("Creating default diagrams");
Object model = project.getRoots().iterator().next();
DiagramFactory df = DiagramFactory.getInstance();
ArgoDiagram d = createClassDiagram(project);
LOG.debug("Creating use case diagram");
project.addMember(df.create(
DiagramFactory.DiagramType.UseCase, model,
project.getProjectSettings().getDefaultDiagramSettings()));
project.addMember(new ProjectMemberTodoList("",
project));
createTodoList(project);
project.setActiveDiagram(d);
}
/**
* Create a class diagrams for the project.
*
* @param project the project to create the diagram in.
* @return the created class diagram
*/
private ArgoDiagram createClassDiagram(Project project) {
LOG.debug("Creating class diagram");
Object model = project.getRoots().iterator().next();
DiagramFactory df = DiagramFactory.getInstance();
ArgoDiagram d = df.create(DiagramFactory.DiagramType.Class,
model,
project.getProjectSettings().getDefaultDiagramSettings());
project.addMember(d);
return d;
}
/**
* Create a todo list for the project.
*
* @param project the project to create the todo list in.
*/
private void createTodoList(Project project) {
LOG.debug("Creating todo list");
project.addMember(new ProjectMemberTodoList("",
project));
}
/**
* Create the top level model for the project and set it as a root and the
* current namespace.
*
* @param project the project to create the model in.
*/
private void createDefaultModel(Project project) {
Object model = Model.getModelManagementFactory().createModel();
Model.getCoreHelper().setName(model,
Translator.localize("misc.untitled-model"));
Collection roots = new ArrayList();
roots.add(model);
project.setRoots(roots);
project.setCurrentNamespace(model);
project.addMember(model);
// finally, apply profile configuration to the model
applyProfileConfiguration(project, model);
}
/**
* Create the top level profile for the project and set it as a root and the
* current namespace.
*
* @param project the project to create the model in.
*/
private void createDefaultProfile(Project project) {
Object model = Model.getModelManagementFactory().createProfile();
Model.getCoreHelper().setName(model,
Translator.localize("misc.untitled-profile"));
Collection roots = new ArrayList();
roots.add(model);
project.setRoots(roots);
project.setCurrentNamespace(model);
project.addMember(model);
// finally, apply profile configuration to the model
applyProfileConfiguration(project, model);
}
/**
* Set the save action.
*
* @param save the action to be used
*/
public void setSaveAction(Action save) {
this.saveAction = save;
// Register with the save action with other subsystems so that
// any changes in those subsystems will enable the
// save button/menu item etc.
Designer.setSaveAction(save);
}
/**
* @return true is the save action is currently enabled
* <p>
* @deprecated for 0.27.2 by tfmorris. Use {@link Project#isDirty()}.
*/
public boolean isSaveActionEnabled() {
return this.saveAction.isEnabled();
}
/**
* Notify the gui that the
* current project's save state has changed. There are 2 receivers:
* the SaveProject tool icon and the title bar (for showing a *).
* <p>
* @deprecated for 0.27.2 by tfmorris. Use
* {@link Project#setDirty(boolean)}.
*/
public void setSaveEnabled(boolean newValue) {
if (saveAction != null) {
saveAction.setEnabled(newValue);
}
}
private void addProject(Project newProject) {
openProjects.addLast(newProject);
}
/**
* Remove the project.
*
* @param oldProject The project to be removed.
*/
public void removeProject(Project oldProject) {
openProjects.remove(oldProject);
// TODO: This code can be removed when getCurrentProject is removed
if (currentProject == oldProject) {
if (openProjects.size() > 0) {
currentProject = openProjects.getLast();
} else {
currentProject = null;
}
}
oldProject.remove();
}
/**
* Updates the top level ModelElements for all projects.
*/
public void updateRoots() {
if (Model.getFacade().getUmlVersion().charAt(0) == '1') {
// not needed in UML 1.x
return;
}
for (Project p : getOpenProjects()) {
p.updateRoots();
}
firePropertyChanged(OPEN_PROJECTS_PROPERTY,
new Project[] {currentProject}, new Project[] {currentProject});
}
/**
* Called when the model subsystem creates a command.
* We must add this to the UndoManager.
*
* @param command the command.
* @return result of the command, if any
* @see org.argouml.model.ModelCommandCreationObserver#execute(ModelCommand)
*/
public Object execute(final ModelCommand command) {
setSaveEnabled(true);
AbstractCommand wrappedCommand = new AbstractCommand() {
private ModelCommand modelCommand = command;
public void undo() {
modelCommand.undo();
}
public boolean isUndoable() {
return modelCommand.isUndoable();
}
public boolean isRedoable() {
return modelCommand.isRedoable();
}
public Object execute() {
return modelCommand.execute();
}
public String toString() {
return modelCommand.toString();
}
};
Project p = getCurrentProject();
if (p != null) {
return getCurrentProject().getUndoManager().execute(wrappedCommand);
} else {
return wrappedCommand.execute();
}
}
}