blob: cfb5e42a446419587da8389d9e5ecbef3baa9a42 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.j2ee.deployment.impl;
import java.io.File;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.api.java.source.BuildArtifactMapper;
import org.netbeans.api.java.source.BuildArtifactMapper.ArtifactsUpdated;
import org.netbeans.modules.j2ee.deployment.common.api.ConfigurationException;
import org.netbeans.modules.j2ee.deployment.common.api.DatasourceAlreadyExistsException;
import org.netbeans.modules.j2ee.deployment.config.ConfigSupportImpl;
import org.netbeans.modules.j2ee.deployment.devmodules.spi.ArtifactListener;
import org.netbeans.modules.j2ee.deployment.devmodules.spi.ArtifactListener.Artifact;
import org.netbeans.modules.j2ee.deployment.devmodules.spi.J2eeApplicationProvider;
import org.netbeans.modules.j2ee.deployment.devmodules.spi.J2eeModuleProvider;
import org.netbeans.modules.j2ee.deployment.impl.projects.DeploymentTarget;
import org.netbeans.modules.j2ee.deployment.impl.ui.ProgressUI;
import org.openide.awt.StatusDisplayer;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileSystem;
import org.openide.filesystems.FileUtil;
import org.openide.filesystems.URLMapper;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
/**
*
* @author Petr Hejl
*/
public final class DeployOnSaveManager {
public static enum DeploymentState {
MODULE_NOT_DEPLOYED,
MODULE_UPDATED,
DEPLOYMENT_FAILED,
SERVER_STATE_UNSUPPORTED
}
private static final Logger LOGGER = Logger.getLogger(DeployOnSaveManager.class.getName());
private static final int DELAY = 300;
private static final int PROGRESS_DELAY = 200;
private static DeployOnSaveManager instance;
private final WeakHashMap<J2eeModuleProvider, CompileOnSaveListener> compileListeners = new WeakHashMap<J2eeModuleProvider, CompileOnSaveListener>();
private final WeakHashMap<J2eeModuleProvider, CopyOnSaveListener> copyListeners = new WeakHashMap<J2eeModuleProvider, CopyOnSaveListener>();
private final WeakHashMap<J2eeModuleProvider, Object> suspended = new WeakHashMap<J2eeModuleProvider, Object>();
private final WeakHashMap<J2eeModuleProvider, List<ConfigSupportImpl.DeployOnSaveListener>> projectListeners = new
WeakHashMap<J2eeModuleProvider, List<ConfigSupportImpl.DeployOnSaveListener>>();
/**
* We need a custom thread factory because the default one stores the
* ThreadGroup in constructor. If the group is destroyed in between
* the submit throws IllegalThreadStateException.
*/
private final ExecutorService EXECUTOR = Executors.newFixedThreadPool(1, new ThreadFactory() {
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
if (t.isDaemon()) {
t.setDaemon(false);
}
if (t.getPriority() != Thread.NORM_PRIORITY) {
t.setPriority(Thread.NORM_PRIORITY);
}
return t;
}
});
//private final ExecutorService EXECUTOR = Executors.newFixedThreadPool(1);
/**<i>GuardedBy("this")</i>*/
private Map<J2eeModuleProvider, Set<Artifact>> toDeploy = new HashMap<J2eeModuleProvider, Set<Artifact>>();
/**<i>GuardedBy("this")</i>*/
private Map<J2eeModuleProvider, DeploymentState> lastDeploymentStates = new HashMap<J2eeModuleProvider, DeploymentState>();
/**<i>GuardedBy("this")</i>*/
private Future<?> current;
private DeployOnSaveManager() {
super();
}
public static synchronized DeployOnSaveManager getDefault() {
if (instance == null) {
instance = new DeployOnSaveManager();
}
return instance;
}
public void startListening(J2eeModuleProvider j2eeProvider) {
synchronized (this) {
if (compileListeners.containsKey(j2eeProvider)) {
// this is due to EAR childs :(
if (j2eeProvider instanceof J2eeApplicationProvider) {
stopListening(j2eeProvider);
} else {
LOGGER.log(Level.FINE, "Already listening on {0}", j2eeProvider);
return;
}
}
List<J2eeModuleProvider> providers = new ArrayList<J2eeModuleProvider>(4);
providers.add(j2eeProvider);
if (j2eeProvider instanceof J2eeApplicationProvider) {
Collections.addAll(providers,
((J2eeApplicationProvider) j2eeProvider).getChildModuleProviders());
}
// get all binary urls
List<URL> urls = new ArrayList<URL>();
for (J2eeModuleProvider provider : providers) {
for (FileObject file : provider.getSourceFileMap().getSourceRoots()) {
URL url = URLMapper.findURL(file, URLMapper.EXTERNAL);
if (url != null) {
urls.add(url);
}
}
}
// register CLASS listener
CompileOnSaveListener listener = new CompileOnSaveListener(j2eeProvider, urls);
for (URL url :urls) {
BuildArtifactMapper.addArtifactsUpdatedListener(url, listener);
}
compileListeners.put(j2eeProvider, listener);
// register WEB listener
J2eeModuleProvider.DeployOnSaveSupport support = j2eeProvider.getDeployOnSaveSupport();
if (support != null) {
CopyOnSaveListener copyListener = new CopyOnSaveListener(j2eeProvider);
support.addArtifactListener(copyListener);
copyListeners.put(j2eeProvider, copyListener);
}
}
}
public void stopListening(J2eeModuleProvider j2eeProvider) {
synchronized (this) {
CompileOnSaveListener removed = compileListeners.remove(j2eeProvider);
if (removed == null) {
LOGGER.log(Level.FINE, "Not compile-listening on {0}", j2eeProvider);
} else {
for (URL url : removed.getRegistered()) {
BuildArtifactMapper.removeArtifactsUpdatedListener(url, removed);
}
}
CopyOnSaveListener copyRemoved = copyListeners.remove(j2eeProvider);
if (removed == null) {
LOGGER.log(Level.FINE, "Not copy-listening on {0}", j2eeProvider);
} else {
J2eeModuleProvider.DeployOnSaveSupport support = j2eeProvider.getDeployOnSaveSupport();
if (support != null) {
support.removeArtifactListener(copyRemoved);
}
}
}
}
public void suspendListening(J2eeModuleProvider provider) {
synchronized (this) {
suspended.put(provider, new Object());
LOGGER.log(Level.FINE, "Listening suspended for {0}", provider);
}
}
public void resumeListening(final J2eeModuleProvider provider) {
boolean resume = false;
synchronized (this) {
resume = suspended.containsKey(provider);
}
// don't do resume unless it is really needed
if (resume) {
FileObject fo = ((ConfigSupportImpl) provider.getConfigSupport()).getProjectDirectory();
FileUtil.refreshAll();
try {
FileSystem fs = (fo != null) ? fo.getFileSystem() : FileUtil.getConfigRoot().getFileSystem();
fs.runAtomicAction(new FileSystem.AtomicAction() {
@Override
public void run() throws IOException {
clearSuspended(provider);
}
});
} catch (IOException ex) {
LOGGER.log(Level.INFO, null, ex);
clearSuspended(provider);
}
}
}
private void clearSuspended(J2eeModuleProvider provider) {
Object prev = null;
synchronized (this) {
prev = suspended.remove(provider);
}
if (LOGGER.isLoggable(Level.FINE) && prev != null) {
LOGGER.log(Level.FINE, "Resuming listening for {0}", provider);
}
}
public void addDeployOnSaveListener( J2eeModuleProvider provider, ConfigSupportImpl.DeployOnSaveListener listener )
{
synchronized (this) {
List<ConfigSupportImpl.DeployOnSaveListener> listeners = projectListeners.get(provider);
if (listeners == null) {
listeners = new ArrayList<ConfigSupportImpl.DeployOnSaveListener>();
projectListeners.put(provider, listeners);
}
listeners.add(listener);
}
}
public void removeDeployOnSaveListener( J2eeModuleProvider provider, ConfigSupportImpl.DeployOnSaveListener listener )
{
synchronized (this) {
List<ConfigSupportImpl.DeployOnSaveListener> listeners = projectListeners.get(provider);
if (listeners == null) {
return;
}
listeners.remove(listener);
if (listeners.isEmpty()) {
projectListeners.remove(provider);
}
}
}
public static boolean isServerStateSupported(ServerInstance si) {
return si.isRunning() && !si.isSuspended();
}
public void notifyInitialDeployment(J2eeModuleProvider provider) {
synchronized (this) {
if (compileListeners.containsKey(provider)) {
// this is due to EAR childs :(
if (provider instanceof J2eeApplicationProvider) {
startListening(provider);
}
}
if (!lastDeploymentStates.containsKey(provider)) {
lastDeploymentStates.put(provider, DeploymentState.MODULE_UPDATED);
}
}
}
public void submitChangedArtifacts(J2eeModuleProvider provider, Iterable<Artifact> artifacts) {
assert provider != null;
assert artifacts != null;
synchronized (this) {
// TODO should go through deploy task and return from the notification task ?
if (suspended.containsKey(provider)) {
return;
}
Set<Artifact> preparedArtifacts = toDeploy.get(provider);
if (preparedArtifacts == null) {
preparedArtifacts = new HashSet<Artifact>();
toDeploy.put(provider, preparedArtifacts);
}
for (Artifact artifact : artifacts) {
preparedArtifacts.add(artifact);
}
boolean delayed = false;
if (current != null && !current.isDone()) {
// TODO interruption throws exception to user from lower levels :((
// this is dummy interruption signal handling :(
current.cancel(false);
delayed = true;
}
current = EXECUTOR.submit(new DeployTask(delayed));
}
}
private static final class CompileOnSaveListener implements ArtifactsUpdated {
private final WeakReference<J2eeModuleProvider> provider;
private final List<URL> registered;
public CompileOnSaveListener(J2eeModuleProvider provider, List<URL> registered) {
this.provider = new WeakReference<J2eeModuleProvider>(provider);
this.registered = registered;
}
public List<URL> getRegistered() {
return registered;
}
public void artifactsUpdated(Iterable<File> artifacts) {
J2eeModuleProvider realProvider = provider.get();
if (realProvider == null) {
return;
}
J2eeModuleProvider.DeployOnSaveClassInterceptor interceptor = realProvider.getDeployOnSaveClassInterceptor();
Set<Artifact> realArtifacts = new HashSet<Artifact>();
for (File file : artifacts) {
if (file != null) {
Artifact a = Artifact.forFile(file);
if (interceptor != null) {
a = interceptor.convert(a);
}
realArtifacts.add(a);
}
}
if (LOGGER.isLoggable(Level.FINE)) {
for (Artifact artifact : realArtifacts) {
LOGGER.log(Level.FINE, "Delivered compile artifact: {0}", artifact);
}
}
DeployOnSaveManager.getDefault().submitChangedArtifacts(realProvider, realArtifacts);
}
}
private static final class CopyOnSaveListener implements ArtifactListener {
private final WeakReference<J2eeModuleProvider> provider;
public CopyOnSaveListener(J2eeModuleProvider provider) {
this.provider = new WeakReference<J2eeModuleProvider>(provider);
}
@Override
public void artifactsUpdated(Iterable<Artifact> artifacts) {
if (LOGGER.isLoggable(Level.FINE)) {
for (Artifact artifact : artifacts) {
LOGGER.log(Level.FINE, "Delivered copy artifact: {0}", artifact);
}
}
J2eeModuleProvider realProvider = provider.get();
if (realProvider != null) {
DeployOnSaveManager.getDefault().submitChangedArtifacts(realProvider, artifacts);
}
}
}
private class DeployTask implements Runnable {
private final boolean delayed;
public DeployTask(boolean delayed) {
this.delayed = delayed;
}
public void run() {
if (delayed) {
try {
Thread.sleep(DELAY);
} catch (InterruptedException ex) {
LOGGER.log(Level.INFO, null, ex);
return;
}
}
LOGGER.log(Level.FINE, "Performing pending deployments");
Map<J2eeModuleProvider, Set<Artifact>> deployNow;
Map<J2eeModuleProvider, List<ConfigSupportImpl.DeployOnSaveListener>> listeners = new HashMap<J2eeModuleProvider, List<ConfigSupportImpl.DeployOnSaveListener>>();
synchronized (DeployOnSaveManager.this) {
if (toDeploy.isEmpty()) {
return;
}
deployNow = toDeploy;
toDeploy = new HashMap<J2eeModuleProvider, Set<Artifact>>();
// copy the listeners
for (Map.Entry<J2eeModuleProvider, List<ConfigSupportImpl.DeployOnSaveListener>> entry : projectListeners.entrySet()) {
if (!deployNow.containsKey(entry.getKey())) {
continue;
}
listeners.put(entry.getKey(), new ArrayList<ConfigSupportImpl.DeployOnSaveListener>(entry.getValue()));
}
}
for (Map.Entry<J2eeModuleProvider, Set<Artifact>> entry : deployNow.entrySet()) {
if (entry.getValue().isEmpty()) {
continue;
}
try {
boolean updated = notifyServer(entry.getKey(), entry.getValue());
if (updated) {
// run nbjpdaapprealoaded task.
runJPDAAppReloaded();
List<ConfigSupportImpl.DeployOnSaveListener> toFire = listeners.get(entry.getKey());
if (toFire != null) {
for (ConfigSupportImpl.DeployOnSaveListener listener : toFire) {
listener.deployed(entry.getValue());
}
}
}
} catch (Throwable t) {
// do not throw away any exception:
LOGGER.log(Level.SEVERE, null, t);
}
}
}
private boolean notifyServer(J2eeModuleProvider provider, Iterable<Artifact> artifacts) {
if (LOGGER.isLoggable(Level.FINEST)) {
StringBuilder builder = new StringBuilder("Artifacts updated: [");
for (Artifact artifact : artifacts) {
builder.append(artifact.getFile().getAbsolutePath()).append(",");
}
builder.setLength(builder.length() - 1);
builder.append("]");
LOGGER.log(Level.FINEST, builder.toString());
}
String instanceID = provider.getServerInstanceID ();
ServerInstance inst = ServerRegistry.getInstance ().getServerInstance (instanceID);
if (inst == null && "DEV-NULL".equals(instanceID)) { // NOI18N
LOGGER.log(Level.INFO, "No server set for Maven project - Deploy on Save will not be performed"); // NOI18N
return false;
} else if (null == inst) {
// the server is not in the registry... so we should not try to deploy
LOGGER.log(Level.INFO, "Project''s server {0} is not registered - Deploy on Save will not be performed", instanceID); // NOI18N
return false;
}
DeploymentState lastState;
synchronized (this) {
lastState = lastDeploymentStates.get(provider);
if (lastState == null) {
lastState = DeploymentState.MODULE_NOT_DEPLOYED;
}
}
DeploymentTarget deploymentTarget = new DeploymentTarget(provider, null);
TargetServer server = new TargetServer(deploymentTarget);
DeploymentState state;
// DEPLOYMENT_FAILED - this can happen when metadata are invalid for example
// SERVER_STATE_UNSUPPORTED - this can happen when server in suspended mode
// null - app has not been deployed so far
if (lastState == null || lastState == DeploymentState.DEPLOYMENT_FAILED
|| (lastState == DeploymentState.SERVER_STATE_UNSUPPORTED
&& isServerStateSupported(deploymentTarget.getServer().getServerInstance()))) {
ProgressUI ui = new ProgressUI(NbBundle.getMessage(TargetServer.class,
"MSG_DeployOnSave", provider.getDeploymentName()), false);
ui.start(PROGRESS_DELAY);
try {
DeploymentHelper.deployServerLibraries(provider);
DeploymentHelper.deployDatasources(provider);
DeploymentHelper.deployMessageDestinations(provider);
TargetModule[] modules = server.deploy(ui, true);
if (modules == null || modules.length <= 0) {
state = DeploymentState.DEPLOYMENT_FAILED;
} else {
state = DeploymentState.MODULE_UPDATED;
}
// TODO start listening ?
} catch (IOException ex) {
LOGGER.log(Level.INFO, null, ex);
state = DeploymentState.DEPLOYMENT_FAILED;
} catch (ServerException ex) {
LOGGER.log(Level.INFO, null, ex);
state = DeploymentState.DEPLOYMENT_FAILED;
} catch (ConfigurationException ex) {
LOGGER.log(Level.INFO, null, ex);
state = DeploymentState.DEPLOYMENT_FAILED;
} catch (DatasourceAlreadyExistsException ex) {
LOGGER.log(Level.INFO, null, ex);
state = DeploymentState.DEPLOYMENT_FAILED;
} catch (TargetServer.NoArchiveException ex) {
LOGGER.log(Level.INFO, null, ex);
state = DeploymentState.DEPLOYMENT_FAILED;
} finally {
ui.finish();
}
// standard incremental deploy
} else {
state = server.notifyArtifactsUpdated(provider, artifacts);
if (state == DeploymentState.SERVER_STATE_UNSUPPORTED
&& lastState == DeploymentState.MODULE_NOT_DEPLOYED) {
state = DeploymentState.MODULE_NOT_DEPLOYED;
}
}
if (state == DeploymentState.MODULE_UPDATED) {
deploymentTarget.getServer().getServerInstance().notifyUpdated(artifacts);
}
String message = null;
switch (state) {
case MODULE_UPDATED:
message = NbBundle.getMessage(DeployOnSaveManager.class,
"MSG_DeployOnSave_Deployed", provider.getDeploymentName());
break;
case DEPLOYMENT_FAILED:
message = NbBundle.getMessage(DeployOnSaveManager.class,
"MSG_DeployOnSave_Failed", provider.getDeploymentName());
break;
case SERVER_STATE_UNSUPPORTED:
message = NbBundle.getMessage(DeployOnSaveManager.class,
"MSG_DeployOnSave_Unsupported", provider.getDeploymentName());
break;
default:
message = null;
}
if (message != null) {
StatusDisplayer.getDefault().setStatusText(message);
}
LOGGER.log(Level.FINE, "Deployment state {0}", state);
synchronized (this) {
lastDeploymentStates.put(provider, state);
}
return state == DeploymentState.MODULE_UPDATED;
}
private void runJPDAAppReloaded() {
// Hack: run nbjpdaappreloaded ANT task after deploy to fix breakpoints.
String reloadedClassName = org.apache.tools.ant.module.api.IntrospectedInfo.getKnownInfo().getDefs("task").get("nbjpdaappreloaded"); // NOI18N
if (reloadedClassName == null) {
// seems to be null during some unit tests
return;
}
String reloadedPackageName = reloadedClassName.substring(0, reloadedClassName.lastIndexOf('.'));
try {
Map<String, ClassLoader> customDefClassLoaders = (Map<String, ClassLoader>)
Lookup.getDefault().lookup(ClassLoader.class).
loadClass("org.apache.tools.ant.module.bridge.AntBridge"). // NOI18N
getMethod("getCustomDefClassLoaders").invoke(null); // NOI18N
//Class reloadedClass = org.apache.tools.ant.module.bridge.AntBridge.getCustomDefClassLoaders().get(reloadedPackageName).loadClass(reloadedClassName);
ClassLoader reloadedClassLoader = customDefClassLoaders.get(reloadedPackageName);
if (reloadedClassLoader != null) {
Class reloadedClass = reloadedClassLoader.loadClass(reloadedClassName);
reloadedClass.getMethod("execute").invoke(reloadedClass.newInstance());
}
} catch (Exception ex) {
Exceptions.printStackTrace(ex);
}
}
}
}