/*******************************************************************************
* Copyright (c) 2004, 2015 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;
import java.util.Collections;
import java.util.Dictionary;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IProduct;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.jobs.MultiRule;
import org.eclipse.ui.plugin.AbstractUIPlugin;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.BundleListener;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceRegistration;
import org.osgi.framework.Version;
import org.osgi.service.url.URLConstants;
import org.osgi.service.url.URLStreamHandlerService;
import org.springframework.beans.factory.xml.NamespaceHandlerResolver;
import org.springframework.ide.eclipse.beans.core.internal.model.BeansModel;
import org.springframework.ide.eclipse.beans.core.internal.model.namespaces.NamespaceManager;
import org.springframework.ide.eclipse.beans.core.internal.model.namespaces.ProjectClasspathNamespaceDefinitionResolverCache;
import org.springframework.ide.eclipse.beans.core.model.IBeansModel;
import org.springframework.ide.eclipse.beans.core.model.INamespaceDefinitionListener;
import org.springframework.ide.eclipse.beans.core.model.INamespaceDefinitionResolver;
import org.springframework.ide.eclipse.core.MessageUtils;
/**
* Central access point for the Spring Framework Core plug-in (id
* <code>"org.springframework.ide.eclipse.beans.core"</code>).
*
* @author Torsten Juergeleit
* @author Christian Dupuis
* @author Tomasz Zarna
* @author Martin Lippert
*/
public class BeansCorePlugin extends AbstractUIPlugin {
/**
* Plugin identifier for Spring Beans Core (value <code>org.springframework.ide.eclipse.beans.core</code>).
*/
public static final String PLUGIN_ID = "org.springframework.ide.eclipse.beans.core";
private static final String RESOURCE_NAME = PLUGIN_ID + ".messages";
/** preference key to suppress missing namespace handler warnings */
public static final String IGNORE_MISSING_NAMESPACEHANDLER_PROPERTY = "ignoreMissingNamespaceHandler";
public static final boolean IGNORE_MISSING_NAMESPACEHANDLER_PROPERTY_DEFAULT = false;
/** preference key to load namespace handler from classpath */
public static final String LOAD_NAMESPACEHANDLER_FROM_CLASSPATH_ID = "loadNamespaceHandlerFromClasspath";
/** preference key to load namespace handler by searching source folders */
public static final String DISABLE_CACHING_FOR_NAMESPACE_LOADING_ID = "disableCachingForNamespaceLoadingFromClasspath";
/** preference key for defining the parsing timeout */
public static final String TIMEOUT_CONFIG_LOADING_PREFERENCE_ID = PLUGIN_ID + ".timeoutConfigLoading";
/** preference key to enable namespace versions per namespace */
public static final String PROJECT_PROPERTY_ID = "enable.project.preferences";
/** preference key to specify the default namespace version */
public static final String NAMESPACE_DEFAULT_VERSION_PREFERENCE_ID = "default.version.";
/** preference key to specify the default namespace version */
public static final String NAMESPACE_PREFIX_PREFERENCE_ID = "prefix.";
/** preference key to specify if versions should be taken from the classpath */
public static final String NAMESPACE_DEFAULT_FROM_CLASSPATH_ID = "default.version.check.classpath";
/** The shared instance */
private static BeansCorePlugin plugin;
/** The singleton beans model */
private BeansModel model;
/** Spring namespace/resolver manager */
private NamespaceManager nsManager;
private NamespaceBundleLister nsListener;
private ServiceRegistration<?> projectAwareUrlService = null;
/** Internal executor service */
private ExecutorService executorService;
private AtomicInteger threadCount = new AtomicInteger(0);
private static final String THREAD_NAME_TEMPLATE = "Background Thread-%s (%s/%s.%s.%s)";
/**
* Preference ID to globally disable any beans auto detection scanning.
*/
public static final String DISABLE_AUTO_DETECTION = BeansCorePlugin.class.getName()+".DISABLE_AUTO_DETECTION";
/** Resource bundle */
private ResourceBundle resourceBundle;
/** Listeners to inform about namespace changes */
private volatile Set<INamespaceDefinitionListener> namespaceDefinitionListeners = Collections
.synchronizedSet(new HashSet<INamespaceDefinitionListener>());
/**
* flag indicating whether the context is down or not - useful during shutdown
*/
private volatile boolean isClosed = false;
/**
* Monitor used for dealing with the bundle activator and synchronous bundle threads
*/
private transient final Object monitor = new Object();
/**
* Creates the Spring Beans Core plug-in.
* <p>
* The plug-in instance is created automatically by the Eclipse platform. Clients must not call.
*/
public BeansCorePlugin() {
plugin = this;
model = new BeansModel();
try {
resourceBundle = ResourceBundle.getBundle(RESOURCE_NAME);
}
catch (MissingResourceException e) {
resourceBundle = null;
}
}
@Override
public void start(final BundleContext context) throws Exception {
super.start(context);
Hashtable<String, String> properties = new Hashtable<String, String>();
properties.put(URLConstants.URL_HANDLER_PROTOCOL,
ProjectAwareUrlStreamHandlerService.PROJECT_AWARE_PROTOCOL);
projectAwareUrlService = context.registerService(
URLStreamHandlerService.class.getName(),
new ProjectAwareUrlStreamHandlerService(), properties);
executorService = Executors.newCachedThreadPool(new ThreadFactory() {
public Thread newThread(Runnable runnable) {
Version version = Version.parseVersion(getPluginVersion());
String productId = "Spring IDE";
IProduct product = Platform.getProduct();
if (product != null && "com.springsource.sts".equals(product.getId()))
productId = "STS";
Thread reportingThread = new Thread(runnable, String.format(THREAD_NAME_TEMPLATE, threadCount.incrementAndGet(),
productId, version.getMajor(), version.getMinor(), version.getMicro()));
reportingThread.setDaemon(true);
return reportingThread;
}
});
nsManager = new NamespaceManager(context);
getPreferenceStore().setDefault(TIMEOUT_CONFIG_LOADING_PREFERENCE_ID, 60);
getPreferenceStore().setDefault(NAMESPACE_DEFAULT_FROM_CLASSPATH_ID, true);
getPreferenceStore().setDefault(LOAD_NAMESPACEHANDLER_FROM_CLASSPATH_ID, true);
Job modelJob = new Job("Initializing Spring Tooling") {
@Override
protected IStatus run(IProgressMonitor monitor) {
initNamespaceHandlers(context);
model.start();
return Status.OK_STATUS;
}
};
modelJob.setRule(MultiRule.combine(ResourcesPlugin.getWorkspace().getRoot(), BeansCoreUtils.BEANS_MODEL_INIT_RULE));
// modelJob.setRule(ResourcesPlugin.getWorkspace().getRuleFactory().buildRule());
// modelJob.setSystem(true);
modelJob.setPriority(Job.DECORATE);
modelJob.schedule();
}
@Override
public void stop(BundleContext context) throws Exception {
synchronized (monitor) {
// if already closed, bail out
if (isClosed) {
return;
}
isClosed = true;
}
model.stop();
if (projectAwareUrlService != null) {
projectAwareUrlService.unregister();
}
super.stop(context);
}
/**
* Returns the shared instance.
*/
public static BeansCorePlugin getDefault() {
return plugin;
}
/**
* Returns the singleton {@link IBeansModel}.
*/
public static IBeansModel getModel() {
return getDefault().model;
}
/**
* only for internal testing purposes
*/
public static void setModel(BeansModel model) {
getDefault().model = model;
}
public static NamespaceHandlerResolver getNamespaceHandlerResolver() {
return getDefault().nsManager.getNamespacePlugins();
}
public static INamespaceDefinitionResolver getNamespaceDefinitionResolver() {
return getDefault().nsManager.getNamespacePlugins();
}
public static INamespaceDefinitionResolver getNamespaceDefinitionResolver(IProject project) {
if (project != null) {
return ProjectClasspathNamespaceDefinitionResolverCache.getResolver(project);
}
return getDefault().nsManager.getNamespacePlugins();
}
public static ExecutorService getExecutorService() {
return getDefault().executorService;
}
public static void notifyNamespaceDefinitionListeners(IProject project) {
for (INamespaceDefinitionListener listener : getDefault().namespaceDefinitionListeners) {
listener.onNamespaceDefinitionRegistered(new INamespaceDefinitionListener.NamespaceDefinitionChangeEvent(
null, project));
}
}
public static void registerNamespaceDefinitionListener(INamespaceDefinitionListener listener) {
getDefault().nsManager.getNamespacePlugins().registerNamespaceDefinitionListener(listener);
getDefault().namespaceDefinitionListeners.add(listener);
}
public static void unregisterNamespaceDefinitionListener(INamespaceDefinitionListener listener) {
getDefault().nsManager.getNamespacePlugins().unregisterNamespaceDefinitionListener(listener);
getDefault().namespaceDefinitionListeners.remove(listener);
}
/**
* Returns the {@link IWorkspace} instance.
*/
public static IWorkspace getWorkspace() {
return ResourcesPlugin.getWorkspace();
}
/**
* Returns the string from the plugin's resource bundle, or 'key' if not found.
*/
public static String getResourceString(String key) {
String bundleString;
ResourceBundle bundle = getDefault().getResourceBundle();
if (bundle != null) {
try {
bundleString = bundle.getString(key);
}
catch (MissingResourceException e) {
log(e);
bundleString = "!" + key + "!";
}
}
else {
bundleString = "!" + key + "!";
}
return bundleString;
}
public static ClassLoader getClassLoader() {
return BeansCorePlugin.class.getClassLoader();
}
/**
* Returns the plugin's resource bundle,
*/
public ResourceBundle getResourceBundle() {
return resourceBundle;
}
public static void log(IStatus status) {
getDefault().getLog().log(status);
}
/**
* Writes the message to the plug-in's log
*
* @param message the text to write to the log
*/
public static void log(String message, Throwable exception) {
IStatus status = createErrorStatus(message, exception);
getDefault().getLog().log(status);
}
public static void log(Throwable exception) {
getDefault().getLog().log(createErrorStatus(getResourceString("Plugin.internal_error"), exception));
}
public static void logAsWarning(Throwable exception) {
getDefault().getLog().log(createWarningStatus(getResourceString("Plugin.internal_warning"), exception));
}
public static IStatus createErrorStatus(String message, Throwable exception) {
return createStatus(IStatus.ERROR, message, exception);
}
public static IStatus createWarningStatus(String message, Throwable exception) {
return createStatus(IStatus.WARNING, message, exception);
}
public static IStatus createStatus(int severity, String message, Throwable exception) {
return new Status(severity, PLUGIN_ID, 0, message == null ? "" : message, exception);
}
public static String getFormattedMessage(String key, Object... args) {
return MessageUtils.format(getResourceString(key), args);
}
public static String getPluginVersion() {
Bundle bundle = getDefault().getBundle();
return bundle.getHeaders().get(Constants.BUNDLE_VERSION);
}
protected void maybeAddNamespaceHandlerFor(Bundle bundle, boolean isLazy) {
nsManager.maybeAddNamespaceHandlerFor(bundle, isLazy);
}
protected void maybeRemoveNameSpaceHandlerFor(Bundle bundle) {
nsManager.maybeRemoveNameSpaceHandlerFor(bundle);
}
protected void initNamespaceHandlers(BundleContext context) {
// register listener first to make sure any bundles in INSTALLED state
// are not lost
nsListener = new NamespaceBundleLister();
context.addBundleListener(nsListener);
Bundle[] previousBundles = context.getBundles();
for (int i = 0; i < previousBundles.length; i++) {
Bundle bundle = previousBundles[i];
if (isBundleResolved(bundle)) {
nsManager.maybeAddNamespaceHandlerFor(bundle, false);
}
else if (isBundleLazyActivated(bundle)) {
nsManager.maybeAddNamespaceHandlerFor(bundle, true);
}
}
// discovery finished, publish the resolvers/parsers in the OSGi space
nsManager.afterPropertiesSet();
}
public boolean isBundleResolved(Bundle bundle) {
return (bundle.getState() >= Bundle.RESOLVED);
}
public boolean isBundleLazyActivated(Bundle bundle) {
if (bundle.getState() == Bundle.STARTING) {
Dictionary<String, String> headers = bundle.getHeaders();
if (headers != null && headers.get(Constants.BUNDLE_ACTIVATIONPOLICY) != null) {
String value = headers.get(Constants.BUNDLE_ACTIVATIONPOLICY).trim();
return (value.startsWith(Constants.ACTIVATION_LAZY));
}
}
return false;
}
/**
* Common base class for {@link ContextLoaderListener} listeners.
*/
private abstract class BaseListener implements BundleListener {
/**
* common cache used for tracking down bundles started lazily so they don't get processed twice (once when
* started lazy, once when started fully)
*/
protected Map<Bundle, Object> lazyBundleCache = new WeakHashMap<Bundle, Object>();
/** dummy value for the bundle cache */
private final Object VALUE = new Object();
// caches the bundle
protected void push(Bundle bundle) {
synchronized (lazyBundleCache) {
lazyBundleCache.put(bundle, VALUE);
}
}
// checks the presence of the bundle as well as removing it
protected boolean pop(Bundle bundle) {
synchronized (lazyBundleCache) {
return (lazyBundleCache.remove(bundle) != null);
}
}
/**
* A bundle has been started, stopped, resolved, or unresolved. This method is a synchronous callback, do not do
* any long-running work in this thread.
*
* @see org.osgi.framework.SynchronousBundleListener#bundleChanged
*/
public void bundleChanged(BundleEvent event) {
// check if the listener is still alive
if (isClosed) {
return;
}
try {
handleEvent(event);
}
catch (Exception ex) {
log(ex);
}
}
protected abstract void handleEvent(BundleEvent event);
}
/**
* Bundle listener used for detecting namespace handler/resolvers. Exists as a separate listener so that it can be
* registered early to avoid race conditions with bundles in INSTALLING state but still to avoid premature context
* creation before the Spring {@link ContextLoaderListener} is not fully initialized.
*/
private class NamespaceBundleLister extends BaseListener {
@Override
protected void handleEvent(final BundleEvent event) {
Bundle bundle = event.getBundle();
switch (event.getType()) {
case BundleEvent.LAZY_ACTIVATION: {
push(bundle);
maybeAddNamespaceHandlerFor(bundle, true);
break;
}
case BundleEvent.RESOLVED: {
if (!pop(bundle)) {
maybeAddNamespaceHandlerFor(bundle, false);
}
break;
}
case BundleEvent.STOPPED: {
pop(bundle);
maybeRemoveNameSpaceHandlerFor(bundle);
break;
}
default:
break;
}
}
}
public boolean isAutoDetectionEnabled() {
return !getPreferenceStore().getBoolean(DISABLE_AUTO_DETECTION);
}
}