/*******************************************************************************
* Copyright (c) 2012 Pivotal Software, Inc.
* 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:
* Pivotal Software, Inc. - initial API and implementation
*******************************************************************************/
package org.grails.ide.eclipse.core.internal.plugins;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
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.IResourceDelta;
import org.eclipse.core.resources.IResourceDeltaVisitor;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.grails.ide.eclipse.core.GrailsCoreActivator;
import org.grails.ide.eclipse.core.internal.GrailsNature;
/**
* Resource change listening for Grails projects and resources.
* @author Nieraj Singh
* @author Andrew Eisenberg
*/
public class GrailsCore implements IResourceChangeListener {
private class PerProjectCacheManager {
private final Map<IProject, Map<Class<? extends IGrailsProjectInfo>, IGrailsProjectInfo>> projectInfoMap =
new HashMap<IProject, Map<Class<? extends IGrailsProjectInfo>,IGrailsProjectInfo>>();
synchronized <T extends IGrailsProjectInfo> T get(
IProject project, Class<T> infoClass, boolean create) throws InstantiationException, IllegalAccessException {
Map<Class<? extends IGrailsProjectInfo>, IGrailsProjectInfo> infoList = projectInfoMap.get(project);
if (infoList == null) {
infoList = new HashMap<Class<? extends IGrailsProjectInfo>, IGrailsProjectInfo>();
projectInfoMap.put(project, infoList);
}
T info = (T) infoList.get(infoClass);
if (info == null && create) {
info = infoClass.newInstance();
info.setProject(project);
infoList.put(infoClass, info);
}
return info;
}
synchronized Map<Class<? extends IGrailsProjectInfo>, IGrailsProjectInfo> removeProject(IProject project) {
return projectInfoMap.remove(project);
}
synchronized Map<Class<? extends IGrailsProjectInfo>, IGrailsProjectInfo> getAll(IProject project) {
return projectInfoMap.get(project);
}
synchronized boolean isEmpty() {
return projectInfoMap.isEmpty();
}
}
// The singleton instance
private final static GrailsCore INSTANCE = new GrailsCore();
private PerProjectCacheManager cacheManager = new PerProjectCacheManager();
private GrailsCore() {
// Singleton
}
private final Map<String, Object> projectLocks = new HashMap<String, Object>();
// Access to the projectLocks map must be synchronized
private final Object projectLocksLock = new Object();
/**
* Retrieves a lock for synchronizing on Cache operations for this particular project.
* Note that this map only grows and old projects are not removed. I think that this is
* fine since we are not expecting an overwhelming number of projects
* @param project the project to lock on
* @return a lock to synchronize on that is specific for the current project
*/
public Object getLockForProject(IProject project) {
if (project == null) {
return this;
}
synchronized (projectLocksLock) {
Object lock = projectLocks.get(project.getName());
if (lock == null) {
lock = new Object();
projectLocks.put(project.getName(), lock);
}
return lock;
}
}
private IProject[] getAllProjects() {
synchronized (projectLocksLock) {
IProject[] allProjects = new IProject[projectLocks.size()];
int i = 0;
IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
for (String name : projectLocks.keySet()) {
allProjects[i++] = root.getProject(name);
}
return allProjects;
}
}
public static GrailsCore get() {
return INSTANCE;
}
/**
* Not API. Only for internal use within the plugin
*/
public void initialize() {
cacheManager = new PerProjectCacheManager();
ResourcesPlugin.getWorkspace().addResourceChangeListener(this);
}
/**
* Not API. Only for internal use within the plugin
*/
public void dispose() {
ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
IProject[] allProjects = getAllProjects();
for (IProject project : allProjects) {
disconnectProject(project);
}
cacheManager = null;
}
/**
* Notifies all connected {@link IGrailsProjectInfo}s that a change to one
* of the grails elements has occurred. Currently, not all grails changes
* are interesting. And so, some changes are ignored.
* <p>
* Here is the list of grails changes that we keep track of:
* <ul>
* <li>Changes to custom taglibs</li>
* <li>Changes to plugins and the grails classpath</li>
* <li>Project close and deletions</li>
* <li>Removal of grails nature</li>
* </ul>
* This list may grow over time.
* <p>
* This method grabs a lock on the project.
*
* @param project
* The affected project
* @param changeKinds
* the list of {@link GrailsElementKind}s affected by this change
* @param change
* the delta
*/
private void notifyGrailsProjectInfos(IProject project,
GrailsElementKind[] changeKinds, IResourceDelta change) {
synchronized (getLockForProject(project)) {
Map<Class<? extends IGrailsProjectInfo>, IGrailsProjectInfo> infos = cacheManager.getAll(project);
if (infos != null) {
for (IGrailsProjectInfo info : infos.values()) {
info.projectChanged(changeKinds, change);
}
}
}
}
/**
* Connect an info of a particular class to the specified project. If the
* class is already connected, The object of that class is returned instead.
*
* This method grabs a lock on the project.
*
* @param infoClass
* @param project
* @param infoClass
* @return the associated {@link IGrailsProjectInfo} of the specified class
* connected to the passed in project, or null if there was an
* exception or if not a Grails project
*/
public <T extends IGrailsProjectInfo> T connect(
IProject project, Class<T> infoClass) {
try {
if (GrailsNature.isGrailsProject(project)) {
synchronized (getLockForProject(project)) {
return cacheManager.get(project, infoClass, true);
}
}
} catch (Exception e) {
GrailsCoreActivator
.log("Problem creating IGrailsProjectInfo of type " + infoClass.getCanonicalName() + //$NON-NLS-1$
" for project " + project.getName(), e); //$NON-NLS-1$
}
return null;
}
/**
* Gets an info of a particular class that is already connected to the
* specified project.
*
* This method grabs a lock on the project.
*
* @param project
* @param infoClass
* @return the associated {@link IGrailsProjectInfo} of the specified class
* that is already connected to the project, or null if none are
* connected.
*/
public <T extends IGrailsProjectInfo> T getInfo(
IProject project, Class<T> infoClass) {
try {
if (GrailsNature.isGrailsProject(project)) {
synchronized (getLockForProject(project)) {
return cacheManager.get(project, infoClass, false);
}
}
} catch (Exception e) {
GrailsCoreActivator
.log("Problem getting IGrailsProjectInfo of type " + infoClass.getCanonicalName() + //$NON-NLS-1$
" for project " + project.getName() + ". Was the project deleted or closed?", e); //$NON-NLS-1$ //$NON-NLS-2$
}
return null;
}
/**
* Disconnects all {@link IGrailsProjectInfo} for the given {@link IProject}.
* Grabs a lock on the given project.
* @param project
*/
public void disconnectProject(IProject project) {
synchronized (getLockForProject(project)) {
Map<Class<? extends IGrailsProjectInfo>, IGrailsProjectInfo> infos = cacheManager.removeProject(project);
if (infos != null) {
for (IGrailsProjectInfo info : infos.values()) {
info.dispose();
}
}
}
}
public void resourceChanged(IResourceChangeEvent event) {
if (cacheManager.isEmpty()) {
return;
}
switch (event.getType()) {
case IResourceChangeEvent.PRE_CLOSE:
case IResourceChangeEvent.PRE_DELETE:
IResource resource = event.getResource();
// don't need to check nature here
if (resource != null && resource.getType() == IResource.PROJECT) {
disconnectProject((IProject) resource);
}
break;
case IResourceChangeEvent.POST_CHANGE:
lookForChanges(event.getDelta());
break;
default:
break;
}
}
private void lookForChanges(IResourceDelta delta) {
LookForChangeVisitor visitor = new LookForChangeVisitor();
try {
delta.accept(visitor);
} catch (CoreException e) {
GrailsCoreActivator.log(e);
}
Map<IProject, Set<GrailsElementKind>> projectKindMap = visitor.projectKindMap;
for (Entry<IProject, Set<GrailsElementKind>> entry : projectKindMap
.entrySet()) {
notifyGrailsProjectInfos(entry.getKey(),
entry.getValue().toArray(new GrailsElementKind[0]), delta);
}
}
/**
* Walks the resource delta and calculates all the kinds of changes for each
* project
*
* @author Andrew Eisenberg
* @created Jan 14, 2010
*/
@SuppressWarnings("nls")
private class LookForChangeVisitor implements IResourceDeltaVisitor {
private Map<IProject, Set<GrailsElementKind>> projectKindMap = new HashMap<IProject, Set<GrailsElementKind>>();
public boolean visit(IResourceDelta delta) throws CoreException {
IResource resource = delta.getResource();
switch (resource.getType()) {
case IResource.PROJECT:
// short circuit non-grails projects
IProject project = (IProject) resource;
return project.isAccessible()
&& project.hasNature(GrailsNature.NATURE_ID);
case IResource.FILE:
if (resource.getParent().getType() == IResource.PROJECT) {
if (resource.getName().equals(".classpath")) {
addChange(resource, GrailsElementKind.CLASSPATH);
} else if (resource.getName().equals(".project")) {
addChange(resource, GrailsElementKind.PROJECT);
}
} else {
// check for various kinds of groovy files
String name = resource.getName();
if (isGroovyLikeFile(resource)) {
if (isTagLibName(name) && isInTagLibFolder(resource)) {
addChange(resource, GrailsElementKind.TAGLIB_CLASS);
} else if (isServiceName(name) && isInServiceFolder(resource)) {
addChange(resource, GrailsElementKind.SERVICE_CLASS);
} else if (isControllerName(name) && isInControllerFolder(resource)) {
addChange(resource, GrailsElementKind.CONTROLLER_CLASS);
} else if (isInDomainFolder(resource)) {
addChange(resource, GrailsElementKind.DOMAIN_CLASS);
}
}
}
case IResource.ROOT:
case IResource.FOLDER:
return true;
}
return false;
}
protected boolean isGroovyLikeFile(IResource resource) {
String fileExtension = resource.getFileExtension();
if (fileExtension != null
&& (fileExtension.equals("groovy") || fileExtension
.equals("java"))) {
return true;
}
return false;
}
/**
* returns true if this resource is in a tag lib folder
*
* @param resource
* @return
*/
private boolean isInTagLibFolder(IResource resource) {
// remove project path
IPath fullPath = resource.getFullPath().removeFirstSegments(1);
return fullPath.segmentCount() > 2
&& fullPath.segment(0).equals("grails-app")
&& fullPath.segment(1).equals("taglib");
}
/**
* Does this resource's name (minus file extension) end in TagLib?
*
* @param resource
* @return
*/
private boolean isTagLibName(String name) {
int lastDot = name.lastIndexOf('.');
if (lastDot > 0) {
int tagLibEnd = name.lastIndexOf("TagLib.") + "TagLib".length();
return tagLibEnd == lastDot;
}
return false;
}
/**
* returns true if this resource is in a services folder
*
* @param resource
* @return
*/
private boolean isInServiceFolder(IResource resource) {
// remove project path
IPath fullPath = resource.getFullPath().removeFirstSegments(1);
return fullPath.segmentCount() > 2
&& fullPath.segment(0).equals("grails-app")
&& fullPath.segment(1).equals("services");
}
/**
* Does this resource's name (minus file extension) end in Service?
*
* @param resource
* @return
*/
private boolean isServiceName(String name) {
int lastDot = name.lastIndexOf('.');
if (lastDot > 0) {
int tagLibEnd = name.lastIndexOf("Service.") + "Service".length();
return tagLibEnd == lastDot;
}
return false;
}
/**
* returns true if this resource is in a controllers folder
*
* @param resource
* @return
*/
private boolean isInControllerFolder(IResource resource) {
// remove project path
IPath fullPath = resource.getFullPath().removeFirstSegments(1);
return fullPath.segmentCount() > 2
&& fullPath.segment(0).equals("grails-app")
&& fullPath.segment(1).equals("controllers");
}
/**
* Does this resource's name (minus file extension) end in Controller?
*
* @param resource
* @return
*/
private boolean isControllerName(String name) {
int lastDot = name.lastIndexOf('.');
if (lastDot > 0) {
int tagLibEnd = name.lastIndexOf("Controller.") + "Controller".length();
return tagLibEnd == lastDot;
}
return false;
}
/**
* returns true if this resource is in a domain class folder
*
* @param resource
* @return
*/
private boolean isInDomainFolder(IResource resource) {
// remove project path
IPath fullPath = resource.getFullPath().removeFirstSegments(1);
return fullPath.segmentCount() > 2
&& fullPath.segment(0).equals("grails-app")
&& fullPath.segment(1).equals("domain");
}
private void addChange(IResource resource, GrailsElementKind kind) {
IProject affectedProject = resource.getProject();
Set<GrailsElementKind> kindSet = projectKindMap
.get(affectedProject);
if (kindSet == null) {
kindSet = new HashSet<GrailsElementKind>(3);
projectKindMap.put(affectedProject, kindSet);
}
kindSet.add(kind);
}
}
}