/*******************************************************************************
* Copyright (c) 2004, 2013 Spring IDE Developers
* 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:
* Spring IDE Developers - initial API and implementation
*******************************************************************************/
package org.springframework.ide.eclipse.beans.core.internal.model;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.wst.common.project.facet.core.FacetedProjectFramework;
import org.eclipse.wst.common.project.facet.core.IFacetedProject;
import org.eclipse.wst.common.project.facet.core.events.IFacetedProjectEvent;
import org.eclipse.wst.common.project.facet.core.events.IFacetedProjectListener;
import org.springframework.ide.eclipse.beans.core.BeansCorePlugin;
import org.springframework.ide.eclipse.beans.core.internal.model.resources.BeansResourceChangeListener;
import org.springframework.ide.eclipse.beans.core.internal.model.resources.IBeansResourceChangeEvents;
import org.springframework.ide.eclipse.beans.core.internal.model.update.BeansModelUpdater;
import org.springframework.ide.eclipse.beans.core.model.IBean;
import org.springframework.ide.eclipse.beans.core.model.IBeansConfig;
import org.springframework.ide.eclipse.beans.core.model.IBeansConfigSet;
import org.springframework.ide.eclipse.beans.core.model.IBeansModel;
import org.springframework.ide.eclipse.beans.core.model.IBeansProject;
import org.springframework.ide.eclipse.beans.core.model.IImportedBeansConfig;
import org.springframework.ide.eclipse.beans.core.model.IReloadableBeansConfig;
import org.springframework.ide.eclipse.core.SpringCore;
import org.springframework.ide.eclipse.core.SpringCoreUtils;
import org.springframework.ide.eclipse.core.io.ExternalFile;
import org.springframework.ide.eclipse.core.model.AbstractModel;
import org.springframework.ide.eclipse.core.model.IModelElement;
import org.springframework.ide.eclipse.core.model.IModelElementVisitor;
import org.springframework.ide.eclipse.core.model.ModelChangeEvent;
import org.springframework.ide.eclipse.core.model.ModelChangeEvent.Type;
import org.springframework.util.ObjectUtils;
/**
* This model manages instances of {@link IBeansProject}s. It's populated from Eclipse's current workspace and receives
* {@link IResourceChangeEvent}s for workspaces changes.
* <p>
* The single instance of {@link IBeansModel} is available from the static method {@link BeansCorePlugin#getModel()}.
* @author Torsten Juergeleit
* @author Christian Dupuis
* @author Martin Lippert
*/
public class BeansModel extends AbstractModel implements IBeansModel {
public static final String DEBUG_OPTION = BeansCorePlugin.PLUGIN_ID + "/model/debug";
public static final boolean DEBUG = SpringCore.isDebug(DEBUG_OPTION);
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
protected volatile boolean modelPopulated = false;
/**
* The table of Spring Beans projects
*/
private volatile Map<IProject, IBeansProject> projects = new HashMap<IProject, IBeansProject>();
private IResourceChangeListener workspaceListener;
private IFacetedProjectListener facetedProjectListener;
public BeansModel() {
super(null, IBeansModel.ELEMENT_NAME);
projects = new ConcurrentHashMap<IProject, IBeansProject>();
BeansCorePlugin.getDefault().getPreferenceStore().addPropertyChangeListener(new IPropertyChangeListener() {
public void propertyChange(PropertyChangeEvent event) {
if (event.getProperty().equals(BeansCorePlugin.DISABLE_AUTO_DETECTION)) {
//boolean enable = BeansCorePlugin.getDefault().isAutoDetectionEnabled();
for (IModelElement me : getProjects()) {
if (me instanceof BeansProject) {
BeansProject p = (BeansProject) me;
p.reset();
((BeansModel) p.getElementParent()).notifyListeners(p,
ModelChangeEvent.Type.CHANGED);
}
}
}
}
});
}
@Override
public IModelElement[] getElementChildren() {
return getProjects().toArray(new IModelElement[getProjects().size()]);
}
@Override
public void accept(IModelElementVisitor visitor, IProgressMonitor monitor) {
// Ask this model's projects
try {
r.lock();
for (IBeansProject project : projects.values()) {
project.accept(visitor, monitor);
if (monitor.isCanceled()) {
return;
}
}
}
finally {
r.unlock();
}
}
public void start() {
if (DEBUG) {
System.out.println("Beans Model startup");
}
try {
w.lock();
projects.clear();
for (IProject project : SpringCoreUtils.getSpringProjects()) {
BeansProject beansProject = new BeansProject(BeansModel.this, project);
addProject(beansProject);
}
// Eagerly populate the internal structure of the beans projects
for (IBeansProject beanProject : projects.values()) {
beanProject.accept(new IModelElementVisitor() {
public boolean visit(IModelElement element, IProgressMonitor monitor) {
return element instanceof IBeansProject;
}
}, new NullProgressMonitor());
}
// Check for update actions
BeansModelUpdater.updateModel(projects.values());
}
finally {
modelPopulated = true;
w.unlock();
}
// Add a ResourceChangeListener to the Eclipse Workspace
workspaceListener = new BeansResourceChangeListener(new ResourceChangeEventHandler());
IWorkspace workspace = ResourcesPlugin.getWorkspace();
workspace.addResourceChangeListener(workspaceListener, BeansResourceChangeListener.LISTENER_FLAGS);
facetedProjectListener = new FacetProjectFrameworkListener();
FacetedProjectFramework.addListener(facetedProjectListener, IFacetedProjectEvent.Type.POST_INSTALL,
IFacetedProjectEvent.Type.POST_UNINSTALL);
}
public void addProject(IBeansProject project) {
projects.put(project.getProject(), project);
}
public void stop() {
if (DEBUG) {
System.out.println("Beans Model shutdown");
}
// Remove the ResourceChangeListener from the Eclipse Workspace
IWorkspace workspace = ResourcesPlugin.getWorkspace();
workspace.removeResourceChangeListener(workspaceListener);
workspaceListener = null;
FacetedProjectFramework.removeListener(facetedProjectListener);
facetedProjectListener = null;
try {
w.lock();
// Remove all projects
projects.clear();
}
finally {
w.unlock();
}
}
public IBeansProject getProject(IProject project) {
try {
r.lock();
return projects.get(project);
}
finally {
r.unlock();
}
}
public IBeansProject getProject(String name) {
if (name == null || name.length() == 0) {
return null;
}
// If a config name given then extract project name
// External config files (with a leading '/') are handled too
int configNamePos = name.indexOf('/', (name.charAt(0) == '/' ? 1 : 0));
if (configNamePos > 0) {
name = name.substring(0, configNamePos);
}
IWorkspace workspace = ResourcesPlugin.getWorkspace();
IProject project = workspace.getRoot().getProject(name);
return getProject(project);
}
/**
* Returns a collection of all projects defined in this model.
*/
public Set<IBeansProject> getProjects() {
try {
r.lock();
return Collections.unmodifiableSet(new HashSet<IBeansProject>(projects.values()));
}
finally {
r.unlock();
}
}
public IBeansConfig getConfig(IFile configFile) {
return getConfig(configFile, true);
}
public IBeansConfig getConfig(IFile configFile, boolean includeImported) {
if (configFile != null) {
IBeansProject project = getProject(configFile.getProject());
if (project != null) {
IBeansConfig bc = project.getConfig(configFile, includeImported);
if (bc != null) {
return bc;
}
}
for (IBeansProject p : getProjects()) {
IBeansConfig bc = p.getConfig(configFile, includeImported);
if (bc != null) {
return bc;
}
}
}
return null;
}
public boolean isConfig(IFile configFile, boolean includeImported) {
if (configFile != null) {
IBeansProject project = getProject(configFile.getProject());
// check the project of the file itself first
String configName = null;
if (project != null) {
if (!(configFile instanceof ExternalFile)) {
configName = configFile.getProjectRelativePath().toString();
}
else {
configName = configFile.getFullPath().toString();
}
if (project.hasConfig(configFile, configName, includeImported)) {
return true;
}
}
// then check all the other projects
configName = configFile.getFullPath().toString();
for (IBeansProject p : getProjects()) {
if (p.hasConfig(configFile, configName, includeImported)) {
return true;
}
}
}
return false;
}
public Set<IBeansConfig> getConfigs(IFile configFile, boolean includeImported) {
Set<IBeansConfig> beansConfigs = new LinkedHashSet<IBeansConfig>();
if (configFile != null) {
for (IBeansProject p : getProjects()) {
beansConfigs.addAll(p.getConfigs(configFile, includeImported));
}
}
return beansConfigs;
}
public IBeansConfig getConfig(String configName) {
// Extract config name from given full-qualified name
// External config files (with a leading '/') are handled too
int configNamePos = configName.indexOf('/', (configName.charAt(0) == '/' ? 1 : 0));
if (configNamePos > 0) {
String projectName = configName.substring(1, configNamePos);
configName = configName.substring(configNamePos + 1);
IBeansProject project = BeansCorePlugin.getModel().getProject(projectName);
if (project != null) {
return project.getConfig(configName);
}
}
return null;
}
/**
* Returns a list of all configs from this model which contain a bean with given bean class.
*/
public Set<IBeansConfig> getConfigs(String className) {
Set<IBeansConfig> configs = new LinkedHashSet<IBeansConfig>();
try {
r.lock();
for (IBeansProject project : projects.values()) {
for (IBeansConfig config : project.getConfigs()) {
if (config.isBeanClass(className)) {
configs.add(config);
}
}
}
}
finally {
r.unlock();
}
return configs;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof BeansModel)) {
return false;
}
try {
r.lock();
BeansModel that = (BeansModel) other;
if (!ObjectUtils.nullSafeEquals(this.projects, that.projects))
return false;
}
finally {
r.unlock();
}
return super.equals(other);
}
@Override
public int hashCode() {
int hashCode = ObjectUtils.nullSafeHashCode(projects);
return getElementType() * hashCode + super.hashCode();
}
@Override
public String toString() {
StringBuffer text = new StringBuffer("Beans model:\n");
try {
r.lock();
for (IBeansProject project : projects.values()) {
text.append(" Configs of project '");
text.append(project.getElementName());
text.append("':\n");
for (IBeansConfig config : project.getConfigs()) {
text.append(" ");
text.append(config);
text.append('\n');
for (IBean bean : config.getBeans()) {
text.append(" ");
text.append(bean);
text.append('\n');
}
}
text.append(" Config sets of project '");
text.append(project.getElementName());
text.append("':\n");
for (IBeansConfigSet configSet : project.getConfigSets()) {
text.append(" ");
text.append(configSet);
text.append('\n');
}
}
}
finally {
r.unlock();
}
return text.toString();
}
/**
* {@inheritDoc}
*/
public boolean isInitialized() {
try {
r.lock();
return modelPopulated;
}
finally {
r.unlock();
}
}
private void buildProject(IResource resource, boolean build) {
BeansProject project = null;
try {
r.lock();
project = (BeansProject) projects.get(resource.getProject());
}
finally {
r.unlock();
}
// project can be null if the model has not been populated
// correctly before updating the project description
if (project != null) {
project.reset();
notifyListeners(project, Type.CHANGED);
if (build) {
// trigger build of project
SpringCoreUtils.buildProject(project.getProject());
}
}
}
/**
* Internal resource change event handler.
*/
public class ResourceChangeEventHandler implements IBeansResourceChangeEvents {
public boolean isSpringProject(IProject project, int eventType) {
return getProject(project) != null;
}
public void springNatureAdded(IProject project, int eventType) {
if (eventType == IResourceChangeEvent.POST_BUILD) {
if (DEBUG) {
System.out.println("Spring beans nature added to project '" + project.getName() + "'");
}
BeansProject proj = new BeansProject(BeansModel.this, project);
try {
w.lock();
projects.put(project, proj);
}
finally {
w.unlock();
}
notifyListeners(proj, Type.CHANGED);
// Nature added -> run builder and validations on this event
SpringCoreUtils.buildProject(project);
}
}
public void springNatureRemoved(IProject project, int eventType) {
if (eventType == IResourceChangeEvent.POST_BUILD) {
if (DEBUG) {
System.out.println("Spring beans nature removed from project '" + project.getName() + "'");
}
IBeansProject proj = null;
try {
w.lock();
proj = projects.remove(project);
}
finally {
w.unlock();
}
if (proj != null) {
notifyListeners(proj, Type.CHANGED);
}
}
}
public void projectAdded(IProject project, int eventType) {
if (eventType == IResourceChangeEvent.POST_BUILD) {
if (DEBUG) {
System.out.println("Project '" + project.getName() + "' added");
}
BeansProject proj = new BeansProject(BeansModel.this, project);
try {
w.lock();
projects.put(project, proj);
BeansModelUpdater.updateProject(proj);
}
finally {
w.unlock();
}
notifyListeners(proj, Type.ADDED);
}
}
public void projectOpened(IProject project, int eventType) {
if (eventType == IResourceChangeEvent.POST_BUILD) {
if (DEBUG) {
System.out.println("Project '" + project.getName() + "' opened");
}
BeansProject proj = new BeansProject(BeansModel.this, project);
try {
w.lock();
projects.put(project, proj);
BeansModelUpdater.updateProject(proj);
}
finally {
w.unlock();
}
notifyListeners(proj, Type.ADDED);
}
}
public void projectClosed(IProject project, int eventType) {
if (DEBUG) {
System.out.println("Project '" + project.getName() + "' closed");
}
IBeansProject proj = null;
try {
w.lock();
proj = projects.remove(project);
}
finally {
w.unlock();
}
if (proj != null) {
notifyListeners(proj, Type.REMOVED);
}
}
public void projectDeleted(IProject project, int eventType) {
if (DEBUG) {
System.out.println("Project '" + project.getName() + "' deleted");
}
IBeansProject proj = null;
try {
w.lock();
proj = projects.remove(project);
}
finally {
w.unlock();
}
if (proj != null) {
notifyListeners(proj, Type.REMOVED);
}
}
public void projectDescriptionChanged(IFile file, int eventType) {
if (eventType == IResourceChangeEvent.POST_BUILD && !SpringCoreUtils.isManifest(file)) {
if (DEBUG) {
System.out.println("Project description '" + file.getFullPath() + "' changed");
}
buildProject(file, true);
}
// Special handling for META-INF/MANIFEST.MF files on PDE
else if (eventType == IResourceChangeEvent.PRE_BUILD && SpringCoreUtils.isManifest(file)) {
if (DEBUG) {
System.out.println("Project manifest '" + file.getFullPath() + "' changed");
}
buildProject(file, true);
}
}
public void configAdded(IFile file, int eventType, IBeansConfig.Type type) {
if (eventType == IResourceChangeEvent.POST_BUILD) {
if (DEBUG) {
System.out.println("Config '" + file.getFullPath() + "' added");
}
BeansProject project = null;
try {
r.lock();
project = (BeansProject) projects.get(file.getProject());
}
finally {
r.unlock();
}
if (!BeansConfigFactory.isJavaConfigFile(file) && project.addConfig(file, type)) {
// In case this is a auto detected config make sure to refresh the
// project too, as the project description file will not change
if (type == IBeansConfig.Type.AUTO_DETECTED) {
buildProject(file, true);
notifyListeners(project, Type.CHANGED);
}
else {
project.saveDescription();
}
}
IBeansConfig config = project.getConfig(file);
notifyListeners(config, Type.ADDED);
}
}
public void configAdded(IFile file, int eventType) {
configAdded(file, eventType, IBeansConfig.Type.MANUAL);
}
public void configChanged(IFile file, int eventType) {
Set<IReloadableBeansConfig> configs = new LinkedHashSet<IReloadableBeansConfig>();
try {
r.lock();
Set<IBeansConfig> bcs = getConfigs(file, true);
for (IBeansConfig bc : bcs) {
if (bc instanceof IImportedBeansConfig) {
configs.add(BeansModelUtils.getParentOfClass(bc, IReloadableBeansConfig.class));
}
else if (bc instanceof IReloadableBeansConfig) {
configs.add((IReloadableBeansConfig) bc);
}
}
}
finally {
r.unlock();
}
if (eventType == IResourceChangeEvent.POST_BUILD) {
if (DEBUG) {
System.out.println("Config '" + file.getFullPath() + "' changed");
}
for (IReloadableBeansConfig config : configs) {
notifyListeners(config, Type.CHANGED);
}
}
else {
// Reset corresponding BeansConfig BEFORE the project builder
// starts validating this BeansConfig
for (IReloadableBeansConfig config : configs) {
config.reload();
}
}
}
public void configRemoved(IFile file, int eventType) {
if (eventType == IResourceChangeEvent.POST_BUILD) {
if (DEBUG) {
System.out.println("Config '" + file.getFullPath() + "' removed");
}
BeansProject project = null;
try {
r.lock();
project = (BeansProject) projects.get(file.getProject());
}
finally {
r.unlock();
}
if (project == null) {
return;
}
// Before removing the config from it's project keep a copy for
// notifying the listeners
IBeansConfig config = project.getConfig(file);
if (project.removeConfig(file)) {
project.saveDescription();
}
// Remove config from config sets where referenced as external
// config
try {
r.lock();
for (IBeansProject proj : projects.values()) {
if (((BeansProject) proj).removeConfig(file)) {
((BeansProject) proj).saveDescription();
}
}
}
finally {
r.unlock();
}
if (config != null) {
notifyListeners(config, Type.REMOVED);
}
}
}
public void listenedFileChanged(IFile file, int eventType) {
if (eventType == IResourceChangeEvent.POST_BUILD) {
if (DEBUG) {
System.out.println("Watched resource '" + file.getFullPath() + "' changed");
}
buildProject(file, false);
}
}
}
/**
* Internal {@link IFacetedProjectListener} that captures changes to project facets.
* @since 2.5.2
*/
private class FacetProjectFrameworkListener implements IFacetedProjectListener {
/**
* {@inheritDoc}
*/
public void handleEvent(IFacetedProjectEvent event) {
IFacetedProject fProject = event.getProject();
if (SpringCoreUtils.isSpringProject(fProject.getProject())) {
if (DEBUG) {
System.out.println(String.format("Project facet on '%s' changed. Triggering re-build.", fProject
.getProject().getName()));
}
buildProject(fProject.getProject(), false);
}
}
}
}