/*******************************************************************************
* Copyright (c) 2012 - 2013 VMWare, 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:
* VMWare, Inc. - initial API and implementation
*******************************************************************************/
package org.grails.ide.eclipse.runonserver;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
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.IWorkspace;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.resources.WorkspaceJob;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.wst.common.project.facet.core.internal.FacetedProjectNature;
import org.eclipse.wst.server.core.IModule;
import org.eclipse.wst.server.core.IServer;
import org.eclipse.wst.server.core.IServerWorkingCopy;
import org.eclipse.wst.server.core.ServerCore;
import org.eclipse.wst.server.core.model.ModuleDelegate;
import org.eclipse.wst.server.core.util.ProjectModuleFactoryDelegate;
import org.grails.ide.eclipse.core.GrailsCoreActivator;
import org.grails.ide.eclipse.core.internal.GrailsNature;
import org.grails.ide.eclipse.runonserver.RunOnServerProperties.RunOnServerPropertiesListener;
/**
* @author Kris De Volder
* @author Andrew Eisenberg
* @author Andy Clement
* @author Christian Dupuis
* @since 2.5.1
*/
public class GrailsAppModuleFactoryDelegate extends ProjectModuleFactoryDelegate implements RunOnServerPropertiesListener {
/**
* This is here mostly to make testing easier...
*/
public static GrailsAppModuleFactoryDelegate instance = null;
public static final String TYPE = "grails.app";
public static final String VERSION = "1.0";
/*
* Notes:
* 1) the module type and version here should correspond to what is defined in our plugin.xml
* contribution to the moduleFactories extension point.
*
* 2) for the module to be useful, some server needs to declare that it can accept this module
* type and version.
*
* 3) Deployment to a server is also Governed by facets applied to the project
* from whence a module came. We apply the "grails.app" facet to any Grails
* App project for which we create a module.
*
* => Modules created by this factory will only be accepted by server runtimes
* that declare they accept the BOTH grails.app facet and grails.app module type.
*/
private static final boolean DEBUG = false;
private static void debug(String msg) {
if (DEBUG) {
System.out.println("GrailsAppModuleFactoryDelegate:" + msg);
}
}
public GrailsAppModuleFactoryDelegate() {
instance = this;
RunOnServerProperties.addEnvChangeListener(this);
}
// /**
// * @param c
// * @throws CoreException
// */
// private static void debug(ILaunchConfiguration c) {
// try {
// @SuppressWarnings("unchecked")
// Map<String, Object> attribs = c.getAttributes();
// for (Entry<String, Object> e : attribs.entrySet()) {
// if (e.getKey().contains("source_locator")) {
// debug(e.getKey() + " = " + e.getValue());
// }
// }
// } catch (CoreException e1) {
// e1.printStackTrace();
// }
// }
/**
* Keeps track of associations between grails projects and their delegates.
*/
private Map<IProject, GrailsAppModuleDelegate> delegates = new WeakHashMap<IProject, GrailsAppModuleDelegate>();
/**
* Get or create the corresponding GrailsAppModuleDelegate for a (Grails app) module.
*/
@Override
public ModuleDelegate getModuleDelegate(IModule module) {
ensureChangeListener();
synchronized (delegates) {
GrailsAppModuleDelegate delegate = delegates.get(module.getProject());
if (delegate==null) {
delegate = new GrailsAppModuleDelegate(module);
delegates.put(module.getProject(), delegate);
}
return delegate;
}
}
/**
* Returns the corresponding GrailsAppModuleDelegate for a project. May return null if either
* - the project is not a GrailsApp project
* - the module delegate has not yet been created.
*/
public GrailsAppModuleDelegate getDelegate(IProject project) {
synchronized (delegates) {
return delegates.get(project);
}
}
@Override
protected IModule createModule(IProject project) {
IModule created;
if (GrailsNature.isGrailsAppProject(project)) {
try {
ensureFacetsAndNatures(project);
} catch (CoreException e) {
GrailsCoreActivator.log("Couldn't make '"+project+"' Faceted, deploying it may not work", e);
}
created = createModule(project.getName(), project.getName(), TYPE, VERSION, project);
} else {
created = null;
}
return created;
}
/**
* If the project associated with a deployable module isn't faceted, and has certain
* natures there's going to be some trouble. Here we check for and add the necessary natures
* and facets.
*
* @param project
* @throws CoreException
*/
private void ensureFacetsAndNatures(IProject project) throws CoreException {
if (!project.hasNature(FacetedProjectNature.NATURE_ID)) {
//Turn into a faceted project
IProjectDescription description = project.getDescription();
String[] natures = description.getNatureIds();
String[] newNatures = RunOnServerPlugin.copyOf(natures, natures.length+1);
newNatures[natures.length] = FacetedProjectNature.NATURE_ID;
description.setNatureIds(newNatures);
project.setDescription(description, null);
}
//Ensure we have the GrailApp facet (This facet marks the project as "TcServer Deployable")
GrailsAppFacet.addFacetIfNeeded(project);
}
///////////////////////////////////////////////////////////////////////////////////////////////
// Responding to changes in the workspace
//
private IResourceChangeListener changeListener;
synchronized void ensureChangeListener() {
if (changeListener==null) {
IWorkspace workspace = ResourcesPlugin.getWorkspace();
changeListener = new IResourceChangeListener() {
public void resourceChanged(IResourceChangeEvent event) {
if ((event.getType() & IResourceChangeEvent.POST_CHANGE) != 0) {
try {
resourcesChanged(event.getDelta());
} catch (CoreException e) {
GrailsCoreActivator.log("Error processing resource change event, event ignored:", e);
}
}
}
};
workspace.addResourceChangeListener(changeListener);
}
}
@Override
protected IPath[] getListenerPaths() {
return new IPath[] {
new Path(".project") // When natures change should clear module cache in superclass.
};
}
/**
* Called whenever some resources are changed in the workspace. Needs to delegate to resourcesChanged method of
* any corresponding GrailsAppModuleDelegates.
* @throws CoreException
*/
private void resourcesChanged(IResourceDelta delta) throws CoreException {
delta.accept(new IResourceDeltaVisitor() {
public boolean visit(IResourceDelta delta) throws CoreException {
// debug("visitDelta: " + delta.getResource());
switch (delta.getResource().getType()) {
case IResource.ROOT:
// debug("visitDelta => TRUE ");
return true;
case IResource.PROJECT:
// debug("visitDelta => delegate to GrailsApp?");
IProject project = (IProject) delta.getResource();
if (GrailsNature.isGrailsAppProject(project)) {
// debug("visitDelta => delegate to GrailsApp? YES");
GrailsAppModuleDelegate grailsApp = getDelegate(project);
if (grailsApp!=null) {
//If the delegate hasn't been created yet, we don't need to watch for changes.
//TODO: But... possible race condition? What if Delegate is in the process of being created?
grailsApp.projectChanged(delta);
}
return false; // Don't go deeper, we have delegate the handling to the grailsApp
} else {
// Project may have been removed or is not a grails app, or no longer a grails app.
// Whatever the case may be, it is certain we should not keep any module delegates for this project anymore
// and if any modules existed for it, they are no longer valid.
removeDelegate(project);
}
return false;
default:
// debug("visitDelta => FALSE");
return false;
}
}
});
}
private void removeModulesFromServer(IProject project) {
IProgressMonitor mon = new NullProgressMonitor();
IServer[] allServers = ServerCore.getServers();
for (IServer server : allServers) {
IServerWorkingCopy wc = server.createWorkingCopy();
IModule[] ms = wc.getModules();
if (ms!=null && ms.length>0) {
List<IModule> toRemove = new ArrayList<IModule>();
for (IModule m : ms) {
if (project.equals(m.getProject())) {
toRemove.add(m);
}
}
if (toRemove.size()>0) {
try {
wc.modifyModules(new IModule[0], toRemove.toArray(new IModule[toRemove.size()]), mon);
saveLater("Removing '"+project.getName()+"' from server", wc);
} catch (CoreException e) {
GrailsCoreActivator.log("Problem removing deleted project's modules from server", e);
}
}
}
}
}
private void saveLater(String description, final IServerWorkingCopy wc) {
WorkspaceJob job = new WorkspaceJob(description) {
@Override
public IStatus runInWorkspace(IProgressMonitor monitor) {
try {
wc.save(true, monitor);
return Status.OK_STATUS;
} catch (CoreException e) {
return e.getStatus();
}
}
};
job.setPriority(Job.INTERACTIVE);
job.schedule();
}
/**
* This is called when a module becomes invalid (most likely this is because the project was deleted).
*/
private void removeDelegate(IProject project) {
synchronized (delegates) {
GrailsAppModuleDelegate delegate = delegates.get(project);
if (delegate!=null) {
// STS-3162: Commented out because it causes apps in Cloud Foundry to be automatically removed.
// Ensure that any module associated with deleted project are removed from server.
// removeModulesFromServer(project);
// Remove any delegates from our map
delegates.remove(project);
//Clear module caches in super class or references to the delegate remain in there!
super.clearCache(project);
// Following is probably not necessary anymore (if we got all references to delegate)
// but is safer to do this anyway.
delegate.clearCaches();
}
}
}
/**
* Called when the 'env' property in {@link RunOnServerProperties} changes for any project.
*/
public synchronized void envChanged(IProject project, String oldEnv, String newEnv) {
GrailsAppModuleDelegate delegate = getDelegate(project);
if (delegate!=null) {
delegate.envChanged(oldEnv, newEnv);
}
}
public void incrementalChanged(IProject project, boolean old, boolean isIncremental) {
GrailsAppModuleDelegate delegate = getDelegate(project);
if (delegate!=null) {
delegate.incrementalChanged(old, isIncremental);
}
}
}