/******************************************************************************* * Copyright (c) 2004, 2017 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.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.eclipse.core.resources.IContainer; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IAdaptable; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.ISafeRunnable; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.SafeRunner; import org.eclipse.core.runtime.Status; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaCore; import org.eclipse.ui.IPersistableElement; import org.springframework.beans.FatalBeanException; import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.parsing.AliasDefinition; import org.springframework.beans.factory.parsing.BeanComponentDefinition; import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; import org.springframework.beans.factory.parsing.ComponentDefinition; import org.springframework.beans.factory.parsing.CompositeComponentDefinition; import org.springframework.beans.factory.parsing.DefaultsDefinition; import org.springframework.beans.factory.parsing.EmptyReaderEventListener; import org.springframework.beans.factory.parsing.ImportDefinition; import org.springframework.beans.factory.parsing.Problem; import org.springframework.beans.factory.parsing.ProblemReporter; import org.springframework.beans.factory.parsing.ReaderEventListener; import org.springframework.beans.factory.parsing.SourceExtractor; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.beans.factory.support.SimpleBeanDefinitionRegistry; import org.springframework.beans.factory.xml.BeanDefinitionDocumentReader; import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate; import org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader; import org.springframework.beans.factory.xml.DocumentDefaultsDefinition; import org.springframework.beans.factory.xml.NamespaceHandler; import org.springframework.beans.factory.xml.NamespaceHandlerResolver; import org.springframework.beans.factory.xml.PluggableSchemaResolver; import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; import org.springframework.beans.factory.xml.XmlReaderContext; import org.springframework.context.annotation.ScannedGenericBeanDefinition; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.EncodedResource; import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.MethodMetadata; import org.springframework.ide.eclipse.beans.core.BeansCorePlugin; import org.springframework.ide.eclipse.beans.core.internal.model.namespaces.DelegatingNamespaceHandlerResolver; import org.springframework.ide.eclipse.beans.core.internal.model.namespaces.DocumentAccessor; import org.springframework.ide.eclipse.beans.core.internal.model.namespaces.XmlCatalogDelegatingEntityResolver; import org.springframework.ide.eclipse.beans.core.internal.model.process.BeansConfigPostProcessorFactory; import org.springframework.ide.eclipse.beans.core.internal.parser.BeansDtdResolver; import org.springframework.ide.eclipse.beans.core.model.IBean; import org.springframework.ide.eclipse.beans.core.model.IBeansComponent; import org.springframework.ide.eclipse.beans.core.model.IBeansConfig; import org.springframework.ide.eclipse.beans.core.model.IBeansConfigEventListener; import org.springframework.ide.eclipse.beans.core.model.IBeansConfigSet; import org.springframework.ide.eclipse.beans.core.model.IBeansImport; import org.springframework.ide.eclipse.beans.core.model.IBeansProject; import org.springframework.ide.eclipse.beans.core.model.IReloadableBeansConfig; import org.springframework.ide.eclipse.beans.core.model.process.IBeansConfigPostProcessor; import org.springframework.ide.eclipse.beans.core.namespaces.IModelElementProvider; import org.springframework.ide.eclipse.beans.core.namespaces.NamespaceUtils; import org.springframework.ide.eclipse.core.SpringCore; import org.springframework.ide.eclipse.core.SpringCoreUtils; import org.springframework.ide.eclipse.core.io.EclipsePathMatchingResourcePatternResolver; import org.springframework.ide.eclipse.core.io.ExternalFile; import org.springframework.ide.eclipse.core.io.FileResource; import org.springframework.ide.eclipse.core.io.StorageResource; import org.springframework.ide.eclipse.core.io.ZipEntryStorage; import org.springframework.ide.eclipse.core.io.xml.LineNumberPreservingDOMParser; import org.springframework.ide.eclipse.core.io.xml.XercesDocumentLoader; import org.springframework.ide.eclipse.core.java.JdtUtils; import org.springframework.ide.eclipse.core.model.DefaultModelSourceLocation; import org.springframework.ide.eclipse.core.model.ILazyInitializedModelElement; import org.springframework.ide.eclipse.core.model.IModelElement; import org.springframework.ide.eclipse.core.model.IModelSourceLocation; import org.springframework.ide.eclipse.core.model.IResourceModelElement; import org.springframework.ide.eclipse.core.model.ISourceModelElement; import org.springframework.ide.eclipse.core.model.validation.ValidationProblem; import org.springframework.ide.eclipse.core.model.xml.XmlSourceLocation; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.xml.sax.EntityResolver; import org.xml.sax.ErrorHandler; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; /** * This class defines a Spring beans configuration. * @author Torsten Juergeleit * @author Dave Watkins * @author Christian Dupuis * @author Martin Lippert */ public class BeansConfig extends AbstractBeansConfig implements IBeansConfig, ILazyInitializedModelElement, IReloadableBeansConfig { private static final String DEBUG_OPTION = BeansCorePlugin.PLUGIN_ID + "/model/loading/debug"; private static final boolean DEBUG = SpringCore.isDebug(DEBUG_OPTION); /** The default element provider used for non-namespaced elements */ public static final IModelElementProvider DEFAULT_ELEMENT_PROVIDER = new DefaultModelElementProvider(); /** The resource that is currently being processed or null if non is processed */ private volatile IResource currentResource = null; /** The resource that is currently being processed or null if non is processed; just a different type then the above */ private volatile EncodedResource currentEncodedResource; /** {@link IBeansConfigPostProcessor}s that are detected in this configs beans and component map */ private volatile Set<IBeansConfigPostProcessor> ownPostProcessors = new HashSet<IBeansConfigPostProcessor>(); /** {@link IBeansConfigPostProcessor}s that have been removed between the two last reads of the backing xml file */ private volatile Set<IBeansConfigPostProcessor> removedPostProcessors = new HashSet<IBeansConfigPostProcessor>(); /** * {@link IBeansConfigPostProcessor}s that have been contributed by external {@link IBeansConfig} from other * {@link IBeansConfigSet} */ private volatile Map<IBeansConfigPostProcessor, Set<IBeansConfig>> externalPostProcessors = new ConcurrentHashMap<IBeansConfigPostProcessor, Set<IBeansConfig>>(); /** {@link Resource} implementation to be passed to Spring core for reading */ private volatile Resource resource; /** {@link ProblemReporter} implementation for later use */ private volatile ProblemReporter problemReporter; /** {@link BeanNameGenerator} implementation for later use */ private volatile BeanNameGenerator beanNameGenerator; /** {@link BeanDefinitionRegistry} implementation for later use */ private volatile SimpleBeanDefinitionRegistry registry; /** Internal cache for all children */ private transient IModelElement[] children; private BeanRegistrationContextTracker beanRegistrationTracker = new BeanRegistrationContextTracker(); /** * Creates a new {@link BeansConfig}. */ public BeansConfig(IBeansProject project, String name, Type type) { super(project, name, type); init(name, project); } /** * {@inheritDoc} */ @Override public IModelElement[] getElementChildren() { // Lazily initialization of this config readConfig(); try { r.lock(); return children; } finally { r.unlock(); } } /** * {@inheritDoc} */ public boolean isInitialized() { return isModelPopulated; } /** * Sets internal list of {@link IBean}s to <code>null</code>. Any further access to the data of this instance of * {@link IBeansConfig} leads to reloading of the corresponding beans config file. */ public void reload() { if (file != null) { try { w.lock(); // System.out.println(String.format("++- resetting config '%s'", file.getFullPath().toString())); isModelPopulated = false; modificationTimestamp = IResource.NULL_STAMP; defaults = null; imports.clear(); aliases.clear(); beans.clear(); components.clear(); isBeanClassesMapPopulated = false; beanClassesMap.clear(); problems.clear(); children = null; beanRegistrationTracker.clear(); } finally { w.unlock(); } // Reset all config sets which contain this config for (IBeansConfigEventListener eventListener : eventListeners) { eventListener.onReset(this); } } } /** * Checks the file for the given name. If the given name defines an external resource (leading '/' -> not part of * the project this config belongs to) get the file from the workspace else from the project. If the name specifies * an entry in an archive then the {@link #isArchived} flag is set. If the corresponding file is not available or * accessible then an entry is added to the config's list of errors. * @param project */ protected void init(String name, IBeansProject project) { IContainer container = null; String fileName = null; String fullPath = null; // At first check for a config file in a JAR int pos = name.indexOf(ZipEntryStorage.DELIMITER); if (pos != -1) { isArchived = true; fileName = name.substring(0, pos); } else { fileName = name; } // Now check if is an workspace external resource if (fileName.startsWith(EXTERNAL_FILE_NAME_PREFIX)) { fileName = fileName.substring(EXTERNAL_FILE_NAME_PREFIX.length()); // Resolve eventual contained classpath variables IPath resolvedPath = JavaCore.getResolvedVariablePath(new Path(fileName)); if (resolvedPath != null) { fileName = resolvedPath.toString(); } // Create an external file instance file = new ExternalFile(new File(fileName), name.substring(pos + 1), project.getProject()); } else { container = (IProject) ((IResourceModelElement) getElementParent()).getElementResource(); fullPath = container.getFullPath().append(fileName).toString(); // Try to find the configuration file in the workspace file = (IFile) container.findMember(fileName); } if (file == null || !file.exists()) { modificationTimestamp = IResource.NULL_STAMP; String msg = "Beans config file '" + fullPath + "' not accessible"; problems = new CopyOnWriteArraySet<ValidationProblem>(); problems.add(new ValidationProblem(IMarker.SEVERITY_ERROR, msg, file, -1)); } else { modificationTimestamp = file.getModificationStamp(); try { file.setSessionProperty(IBeansConfig.CONFIG_FILE_TAG, IBeansConfig.CONFIG_FILE_TAG_VALUE); } catch (CoreException e) { BeansCorePlugin.log(new Status(IStatus.WARNING, BeansCorePlugin.PLUGIN_ID, String.format( "Error occured while tagging config file '%s'", file.getFullPath()), e)); } } } /** * {@inheritDoc} */ @Override protected void readConfig() { if (!this.isModelPopulated) { long start = System.currentTimeMillis(); long count = 0; w.lock(); if (this.isModelPopulated) { w.unlock(); return; } final ClassLoader projectIncludingClassloader = getProjectRelatedClassLoader(); try { // Publish start events for (IBeansConfigEventListener eventListener : eventListeners) { eventListener.onReadStart(this); } if (file != null && file.exists()) { // Only install Eclipse-based resource loader if enabled in project properties // IMPORTANT: the following block needs to stay before the w.lock() // as it could otherwise create a runtime deadlock final ResourceLoader resourceLoader; if (getElementParent() instanceof IBeansProject && ((IBeansProject) getElementParent()).isImportsEnabled()) { resourceLoader = new EclipsePathMatchingResourcePatternResolver(file.getProject(), projectIncludingClassloader); } else { resourceLoader = new ClassResourceFilteringPatternResolver(file.getProject(), projectIncludingClassloader); } modificationTimestamp = file.getModificationStamp(); if (isArchived) { if (file instanceof Resource) { resource = (Resource) file; } else { resource = new StorageResource(new ZipEntryStorage(file.getProject(), getElementName()), file.getProject()); } } else { resource = new FileResource(file); } // Set up classloader to use for NamespaceHandler and XSD loading ClassLoader namespaceResolvingClassloader = projectIncludingClassloader; if (!NamespaceUtils.useNamespacesFromClasspath(file.getProject())) { namespaceResolvingClassloader = BeansCorePlugin.getClassLoader(); } registry = new ScannedGenericBeanDefinitionSuppressingBeanDefinitionRegistry(); EntityResolver resolver = new XmlCatalogDelegatingEntityResolver(new BeansDtdResolver(), new PluggableSchemaResolver(namespaceResolvingClassloader)); final DocumentAccessor documentAccessor = new DocumentAccessor(); final SourceExtractor sourceExtractor = new DelegatingSourceExtractor(file.getProject()); final BeansConfigReaderEventListener eventListener = new BeansConfigReaderEventListener(this, resource, sourceExtractor, documentAccessor); final NamespaceHandlerResolver namespaceHandlerResolver = new DelegatingNamespaceHandlerResolver(namespaceResolvingClassloader, this, documentAccessor); problemReporter = new BeansConfigProblemReporter(); beanNameGenerator = new UniqueBeanNameGenerator(this); final XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(registry) { @Override public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException { // Capture the current resource being processed to handle parsing exceptions correctly and // create the validation error on the correct resource if (encodedResource != null && encodedResource.getResource() instanceof IAdaptable) { currentResource = (IResource) ((IAdaptable) encodedResource.getResource()).getAdapter(IResource.class); currentEncodedResource = encodedResource; } try { // Delegate actual processing to XmlBeanDefinitionReader int loadedBeans = 0; if (encodedResource.getResource().exists()) { loadedBeans = super.loadBeanDefinitions(encodedResource); } return loadedBeans; } finally { // Reset currently processed resource before leaving currentResource = null; currentEncodedResource = null; } } @Override public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException { try { documentAccessor.pushDocument(doc); return super.registerBeanDefinitions(doc, resource); } finally { documentAccessor.popDocument(); } } @Override public XmlReaderContext createReaderContext(Resource resource) { return new ProfileAwareReaderContext(resource, problemReporter, eventListener, sourceExtractor, this, namespaceHandlerResolver); } @Override protected BeanDefinitionDocumentReader createBeanDefinitionDocumentReader() { return new ToolingFriendlyBeanDefinitionDocumentReader(BeansConfig.this); } }; reader.setDocumentLoader(new XercesDocumentLoader()); reader.setResourceLoader(resourceLoader); reader.setEntityResolver(resolver); reader.setSourceExtractor(sourceExtractor); reader.setEventListener(eventListener); reader.setProblemReporter(problemReporter); reader.setErrorHandler(new BeansConfigErrorHandler()); reader.setNamespaceHandlerResolver(namespaceHandlerResolver); reader.setBeanNameGenerator(beanNameGenerator); reader.setEnvironment(new ToolingAwareEnvironment()); final Map<Throwable, Integer> throwables = new HashMap<Throwable, Integer>(); try { Callable<Integer> loadBeanDefinitionOperation = new Callable<Integer>() { public Integer call() { // Obtain thread context classloader and override with the project classloader ClassLoader threadClassLoader = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(resourceLoader.getClassLoader()); try { // Load bean definitions int count = reader.loadBeanDefinitions(resource); // Finally register post processed beans and components eventListener.registerComponents(); // Post process beans config if required postProcess(resourceLoader.getClassLoader()); return count; } catch (Exception e) { // Record the exception to throw it later throwables.put(e, LineNumberPreservingDOMParser.getStartLineNumber(documentAccessor.getLastElement())); } finally { // Reset the context classloader Thread.currentThread().setContextClassLoader(threadClassLoader); } return 0; } }; try { FutureTask<Integer> task = new FutureTask<Integer>(loadBeanDefinitionOperation); BeansCorePlugin.getExecutorService().submit(task); count = task.get(BeansCorePlugin.getDefault().getPreferenceStore().getInt(BeansCorePlugin.TIMEOUT_CONFIG_LOADING_PREFERENCE_ID), TimeUnit.SECONDS); // if we recored an exception use this instead of stupid concurrent exception if (throwables.size() > 0) { throw throwables.keySet().iterator().next(); } } catch (TimeoutException e) { problems.add(new ValidationProblem(IMarker.SEVERITY_ERROR, "Loading of resource '" + resource.getFile().getAbsolutePath() + "' took more than " + BeansCorePlugin.getDefault().getPreferenceStore() .getInt(BeansCorePlugin.TIMEOUT_CONFIG_LOADING_PREFERENCE_ID) + "sec", file, 1)); } } catch (Throwable e) { int line = -1; if (throwables.containsKey(e)) { line = throwables.get(e); } // Skip SAXParseExceptions because they're already handled by the SAX ErrorHandler if (e instanceof BeanDefinitionStoreException) { if (e.getCause() != null) { problems.add(new ValidationProblem(IMarker.SEVERITY_ERROR, String.format( "Error occured processing XML '%s'. See Error Log for more details", e.getCause().getMessage()), file, line)); BeansCorePlugin.log(new Status(IStatus.INFO, BeansCorePlugin.PLUGIN_ID, String.format( "Error occured processing '%s'", file.getFullPath()), e.getCause())); } else { problems.add(new ValidationProblem(IMarker.SEVERITY_ERROR, e.getMessage(), file, line)); BeansCorePlugin.log(new Status(IStatus.INFO, BeansCorePlugin.PLUGIN_ID, String.format( "Error occured processing '%s'", file.getFullPath()), e)); } } else if (!(e.getCause() instanceof SAXParseException) && !(e instanceof BeanDefinitionParsingException)) { problems.add(new ValidationProblem(IMarker.SEVERITY_ERROR, e.getMessage(), file, line)); BeansCorePlugin.log(new Status(IStatus.INFO, BeansCorePlugin.PLUGIN_ID, String.format( "Error occured processing '%s'", file.getFullPath()), e)); } } } } finally { // Prepare the internal cache of all children for faster access List<ISourceModelElement> allChildren = new ArrayList<ISourceModelElement>(imports); allChildren.addAll(aliases.values()); allChildren.addAll(components); allChildren.addAll(beans.values()); Collections.sort(allChildren, new Comparator<ISourceModelElement>() { public int compare(ISourceModelElement element1, ISourceModelElement element2) { return element1.getElementStartLine() - element2.getElementStartLine(); } }); this.children = allChildren.toArray(new IModelElement[allChildren.size()]); this.isModelPopulated = true; w.unlock(); // Run external post processors postProcessExternal(externalPostProcessors.keySet(), projectIncludingClassloader); // Publish events for all existing post processors if (this.ownPostProcessors != null) { for (IBeansConfigPostProcessor postProcessor : ownPostProcessors) { for (IBeansConfigEventListener eventListener : eventListeners) { eventListener.onPostProcessorDetected(this, postProcessor); } } } // Publish events for all removed post processors if (this.removedPostProcessors != null) { for (IBeansConfigPostProcessor postProcessor : removedPostProcessors) { for (IBeansConfigEventListener eventListener : eventListeners) { eventListener.onPostProcessorRemoved(this, postProcessor); } } } for (IBeansConfigEventListener eventListener : eventListeners) { eventListener.onReadEnd(this); } if (DEBUG) { System.out.println(String.format("> loading of %s beans from %s took %sms", count, file .getFullPath().toString(), (System.currentTimeMillis() - start))); } } } } public ClassLoader getProjectRelatedClassLoader() { if (file != null && file.exists()) { return JdtUtils.getClassLoader(file.getProject(), BeansCorePlugin.getClassLoader()); } return null; } /** * {@inheritDoc} */ @Override public Object getAdapter(Class adapter) { if (adapter == IPersistableElement.class) { return new BeansModelElementToPersistableElementAdapter(this); } else if (adapter == IResource.class) { return getElementResource(); } else if (adapter == Resource.class) { return resource; } return super.getAdapter(adapter); } /** * Entry into processing the contributed {@link IBeansConfigPostProcessor}. */ private void postProcess(ClassLoader classloader) { Set<IBeansConfigPostProcessor> detectedPostProcessors = new LinkedHashSet<IBeansConfigPostProcessor>(); removedPostProcessors = new LinkedHashSet<IBeansConfigPostProcessor>(); // Create special ReaderEventListener that essentially just passes through component definitions ReaderEventListener eventListener = new BeansConfigPostProcessorReaderEventListener(); // Collect the beans from this config and all imported configs List<IBean> beansClone = new ArrayList<IBean>(); // Important: don't use getBeans or getComponents on this instance -> will lock beansClone.addAll(beans.values()); for (IBeansComponent component : components) { addBeansFromCompoent(component, beansClone); } // Now collect from all imported configurations as well for (IBeansImport beansImport : imports) { for (IBeansConfig bc : beansImport.getImportedBeansConfigs()) { beansClone.addAll(bc.getBeans()); for (IBeansComponent component : bc.getComponents()) { addBeansFromCompoent(component, beansClone); } } } // Run all generally contributed post processors for (IBeansConfigPostProcessor postProcessor : BeansConfigPostProcessorFactory.createPostProcessor(null)) { executePostProcessor(postProcessor, eventListener, classloader); } // Run all post processors specific to a bean class for (IBean bean : beansClone) { String beanClassName = bean.getClassName(); if (beanClassName != null) { IType type = JdtUtils.getJavaType(getElementResource().getProject(), beanClassName); if (type != null) { for (IBeansConfigPostProcessor postProcessor : BeansConfigPostProcessorFactory .createPostProcessor(beanClassName)) { executePostProcessor(postProcessor, eventListener, classloader); // Keep the detected post processor for later detectedPostProcessors.add(postProcessor); } } } } // Detect deleted post processors for (IBeansConfigPostProcessor postProcessor : ownPostProcessors) { if (!detectedPostProcessors.contains(postProcessor)) { ownPostProcessors.remove(postProcessor); removedPostProcessors.add(postProcessor); } } // Detect newly added post processors for (IBeansConfigPostProcessor postProcessor : detectedPostProcessors) { if (!ownPostProcessors.contains(postProcessor)) { ownPostProcessors.add(postProcessor); } } } private void addBeansFromCompoent(IBeansComponent component, List<IBean> beansClone) { beansClone.addAll(component.getBeans()); for (IBeansComponent bc : component.getComponents()) { addBeansFromCompoent(bc, beansClone); } } /** * Execute the externally added {@link IBeansConfigPostProcessor}s. * <p> * This will only execute the given post processors if this config is already populated. */ private void postProcessExternal(Set<IBeansConfigPostProcessor> postProcessors, ClassLoader classloader) { if (this.isModelPopulated) { try { w.lock(); // Create special ReaderEventListener that essentially just passes through component definitions ReaderEventListener eventListener = new BeansConfigPostProcessorReaderEventListener(); // Run all external found post processor instances for (IBeansConfigPostProcessor postProcessor : postProcessors) { if (!ownPostProcessors.contains(postProcessor)) { executePostProcessor(postProcessor, eventListener, classloader); } } } finally { w.unlock(); } } } /** * Add the given {@link IBeansConfigPostProcessor}. * <p> * If this config has already been populated only the given {@link IBeansConfigPostProcessor} will be executed; * otherwise executing is deferred until the model gets populated * @param classloader */ protected void addExternalPostProcessor(IBeansConfigPostProcessor postProcessor, IBeansConfig config) { try { w.lock(); if (externalPostProcessors.containsKey(postProcessor)) { externalPostProcessors.get(postProcessor).add(config); } else { Set<IBeansConfig> configs = new LinkedHashSet<IBeansConfig>(); configs.add(config); externalPostProcessors.put(postProcessor, configs); if (!ownPostProcessors.contains(postProcessor)) { // Run the external post processors Set<IBeansConfigPostProcessor> postProcessors = new HashSet<IBeansConfigPostProcessor>(); postProcessors.add(postProcessor); postProcessExternal(postProcessors, getProjectRelatedClassLoader()); } } } finally { w.unlock(); } } /** * Remove the given {@link IBeansConfigPostProcessor}. * <p> * This will trigger a reload of this config in order to remove obsolete {@link IBean}s. */ protected void removeExternalPostProcessor(IBeansConfigPostProcessor postProcessor, IBeansConfig config) { try { w.lock(); if (externalPostProcessors.containsKey(postProcessor) && externalPostProcessors.get(postProcessor).remove(config)) { if (externalPostProcessors.get(postProcessor).size() == 0) { externalPostProcessors.remove(postProcessor); } } reload(); } finally { w.unlock(); } } /** * Safely execute the given {@link IBeansConfigPostProcessor}. */ private void executePostProcessor(final IBeansConfigPostProcessor postProcessor, final ReaderEventListener eventListener, final ClassLoader classloader) { SafeRunner.run(new ISafeRunnable() { @Override public void handleException(Throwable exception) { BeansCorePlugin.log(new Status(IStatus.WARNING, BeansCorePlugin.PLUGIN_ID, "Error occured while running bean post processors", exception)); } @Override public void run() throws Exception { ClassLoader contextLoader = Thread.currentThread().getContextClassLoader(); if (classloader != null) { Thread.currentThread().setContextClassLoader(classloader); } try { postProcessor.postProcess(BeansConfigPostProcessorFactory.createPostProcessingContext(BeansConfig.this, beans.values(), eventListener, problemReporter, beanNameGenerator, registry, problems)); } catch (Exception e) { handleException(e); } finally { Thread.currentThread().setContextClassLoader(contextLoader); } } }); } /** * Registers the given component definition with this {@link BeansConfig}'s beans and component storage. */ private void registerComponentDefinition(ComponentDefinition componentDefinition, Map<String, IModelElementProvider> elementProviders) { String uri = NamespaceUtils.getNameSpaceURI(componentDefinition); IModelElementProvider provider = elementProviders.get(uri); if (provider == null) { provider = DEFAULT_ELEMENT_PROVIDER; } ISourceModelElement element = provider.getElement(BeansConfig.this, componentDefinition); if (element instanceof IBean) { beans.put(element.getElementName(), (IBean) element); } else if (element instanceof IBeansComponent) { components.add((IBeansComponent) element); } } /** * Returns the current {@link IResource} that is being processed. */ private IResource getCurrentResource() { if (currentResource != null) { return currentResource; } return file; } /** * Implementation of {@link ReaderEventListener} which populates the current instance of {@link IBeansConfig} with * data from the XML bean definition reader events. */ class BeansConfigReaderEventListener implements ReaderEventListener { private IBeansConfig config; private Resource resource; private Map<String, IModelElementProvider> elementProviders; private Map<Resource, Set<ComponentDefinition>> componentDefinitionsCache; private Map<Resource, Set<ImportDefinition>> importDefinitionsCache; private Map<Resource, Set<AliasDefinition>> aliasDefinitionsCache; private Map<Resource, DocumentDefaultsDefinition> defaultDefinitionsCache; private SourceExtractor sourceExtractor; private DocumentAccessor documentAccessor; public BeansConfigReaderEventListener(IBeansConfig config, Resource resource, SourceExtractor sourceExtractor, DocumentAccessor documentAccessor) { this.config = config; this.resource = resource; this.elementProviders = NamespaceUtils.getElementProviders(); this.componentDefinitionsCache = new HashMap<Resource, Set<ComponentDefinition>>(); this.importDefinitionsCache = new HashMap<Resource, Set<ImportDefinition>>(); this.aliasDefinitionsCache = new HashMap<Resource, Set<AliasDefinition>>(); this.defaultDefinitionsCache = new HashMap<Resource, DocumentDefaultsDefinition>(); this.sourceExtractor = sourceExtractor; this.documentAccessor = documentAccessor; } /** * {@inheritDoc} */ public void defaultsRegistered(DefaultsDefinition defaultsDefinition) { if (defaultsDefinition instanceof DocumentDefaultsDefinition) { Object source = defaultsDefinition.getSource(); if (source instanceof IModelSourceLocation) { Resource resource = ((IModelSourceLocation) source).getResource(); addDefaultToCache((DocumentDefaultsDefinition) defaultsDefinition, resource); } } } /** * {@inheritDoc} */ public void importProcessed(ImportDefinition importDefinition) { Object source = importDefinition.getSource(); if (source instanceof IModelSourceLocation) { Resource resource = ((IModelSourceLocation) source).getResource(); addImportToCache(importDefinition, resource); } } /** * {@inheritDoc} */ public void aliasRegistered(AliasDefinition aliasDefinition) { Object source = aliasDefinition.getSource(); if (source instanceof IModelSourceLocation) { Resource resource = ((IModelSourceLocation) source).getResource(); addAliasToCache(aliasDefinition, resource); } } /** * Converts the given {@link ComponentDefinition} into a corresponding {@link ISourceModelElement} via a * namespace-specific {@link IModelElementProvider}. These providers are registered via the extension point * <code>org.springframework.ide.eclipse.beans.core.namespaces</code>. */ public void componentRegistered(ComponentDefinition componentDefinition) { Object source = componentDefinition.getSource(); // make sure to attach a default source location if (source == null && currentEncodedResource != null && currentEncodedResource.getResource() != null) { source = sourceExtractor.extractSource(documentAccessor.getCurrentElement(), currentEncodedResource.getResource()); for (BeanDefinition beanDefinition : componentDefinition.getBeanDefinitions()) { if (beanDefinition.getSource() == null && beanDefinition instanceof AbstractBeanDefinition) { ((AbstractBeanDefinition) beanDefinition).setSource(source); ((AbstractBeanDefinition) beanDefinition).setRole(BeanDefinition.ROLE_INFRASTRUCTURE); } } } // make sure nested BeanDefinitions have all the source extraction applied addSourceToNestedBeanDefinitions(componentDefinition); if (source instanceof IModelSourceLocation) { Resource resource = ((IModelSourceLocation) source).getResource(); addComponentToCache(componentDefinition, resource); } } private void addAliasToCache(AliasDefinition aliasDefinition, Resource resource) { if (aliasDefinitionsCache.containsKey(resource)) { aliasDefinitionsCache.get(resource).add(aliasDefinition); } else { Set<AliasDefinition> aliasDefinitions = new LinkedHashSet<AliasDefinition>(); aliasDefinitions.add(aliasDefinition); aliasDefinitionsCache.put(resource, aliasDefinitions); } } private void addSourceToNestedBeanDefinitions(ComponentDefinition componentDefinition) { if (componentDefinition instanceof CompositeComponentDefinition) { CompositeComponentDefinition compositeComponentDefinition = (CompositeComponentDefinition) componentDefinition; for (ComponentDefinition nestedComponentDefinition : compositeComponentDefinition.getNestedComponents()) { for (BeanDefinition beanDefinition : nestedComponentDefinition.getBeanDefinitions()) { if (!(beanDefinition.getSource() instanceof IModelSourceLocation) && beanDefinition instanceof AbstractBeanDefinition) { ((AbstractBeanDefinition) beanDefinition).setSource(sourceExtractor.extractSource( beanDefinition.getSource(), resource)); } } } } } private void addComponentToCache(ComponentDefinition componentDefinition, Resource resource) { if (componentDefinitionsCache.containsKey(resource)) { componentDefinitionsCache.get(resource).add(componentDefinition); } else { Set<ComponentDefinition> componentDefinitions = new LinkedHashSet<ComponentDefinition>(); componentDefinitions.add(componentDefinition); componentDefinitionsCache.put(resource, componentDefinitions); } } private void addDefaultToCache(DocumentDefaultsDefinition defaultsDefinition, Resource resource) { if (!defaultDefinitionsCache.containsKey(resource)) { defaultDefinitionsCache.put(resource, defaultsDefinition); } } private void addImportToCache(ImportDefinition importDefinition, Resource resource) { if (importDefinitionsCache.containsKey(resource)) { importDefinitionsCache.get(resource).add(importDefinition); } else { Set<ImportDefinition> importDefinitions = new LinkedHashSet<ImportDefinition>(); importDefinitions.add(importDefinition); importDefinitionsCache.put(resource, importDefinitions); } } public void registerComponents() { // Start with the root resource defaults = defaultDefinitionsCache.get(resource); Set<ComponentDefinition> componentDefinitions = componentDefinitionsCache.get(resource); if (componentDefinitions != null) { for (ComponentDefinition componentDefinition : componentDefinitions) { registerComponentDefinition(componentDefinition, elementProviders); } } Set<AliasDefinition> aliasDefinitions = aliasDefinitionsCache.get(resource); if (aliasDefinitions != null) { for (AliasDefinition aliasDefinition : aliasDefinitions) { aliases.put(aliasDefinition.getAlias(), new BeanAlias(config, aliasDefinition)); } } Set<ImportDefinition> importDefinitions = importDefinitionsCache.get(resource); if (importDefinitions != null) { for (ImportDefinition importDefinition : importDefinitions) { processImportDefinition(importDefinition, config); } } } private void processImportDefinition(ImportDefinition importDefinition, IBeansConfig config) { BeansImport beansImport = new BeansImport(config, importDefinition); if (config instanceof BeansConfig) { imports.add(beansImport); } else if (config instanceof ImportedBeansConfig) { ((ImportedBeansConfig) config).addImport(beansImport); } if (((IBeansProject) getElementParent()).isImportsEnabled()) { Resource[] importedResources = importDefinition.getActualResources(); for (Resource importedResource : importedResources) { ImportedBeansConfig importedBeansConfig = new ImportedBeansConfig(beansImport, importedResource, getType()); importedBeansConfig.readConfig(); beansImport.addImportedBeansConfig(importedBeansConfig); importedBeansConfig.setDefaults(defaultDefinitionsCache.get(importedResource)); Set<ComponentDefinition> componentDefinitions = componentDefinitionsCache.get(importedResource); if (componentDefinitions != null) { for (ComponentDefinition componentDefinition : componentDefinitions) { String uri = NamespaceUtils.getNameSpaceURI(componentDefinition); IModelElementProvider provider = elementProviders.get(uri); if (provider == null) { provider = DEFAULT_ELEMENT_PROVIDER; } ISourceModelElement element = provider.getElement(importedBeansConfig, componentDefinition); if (element instanceof IBean) { importedBeansConfig.addBean((IBean) element); } else if (element instanceof IBeansComponent) { importedBeansConfig.addComponent((IBeansComponent) element); } } } Set<AliasDefinition> aliasDefinitions = aliasDefinitionsCache.get(importedResource); if (aliasDefinitions != null) { for (AliasDefinition aliasDefinition : aliasDefinitions) { importedBeansConfig.addAlias(new BeanAlias(importedBeansConfig, aliasDefinition)); } } // Process nested imports Set<ImportDefinition> importDefinitions = importDefinitionsCache.get(importedResource); if (importDefinitions != null) { for (ImportDefinition nestedImportDefinition : importDefinitions) { processImportDefinition(nestedImportDefinition, importedBeansConfig); } } importedBeansConfig.readFinish(); } } } } /** * {@link ResourcePatternResolver} that checks if <code>.class</code> resource are being requested. */ class ClassResourceFilteringPatternResolver extends EclipsePathMatchingResourcePatternResolver implements ResourcePatternResolver { /** * Creates a new {@link ClassResourceFilteringPatternResolver} */ public ClassResourceFilteringPatternResolver(IProject project, ClassLoader classLoader) { super(project, classLoader); } /** * {@inheritDoc} */ @Override public Resource getResource(String location) { // Pass package scanning through to the default pattern resolver. This is required for // component-scanning. if (location.endsWith(ClassUtils.CLASS_FILE_SUFFIX)) { return super.getResource(location); } return null; } /** * {@inheritDoc} */ @Override public Resource[] getResources(String locationPattern) throws IOException { // Pass package scanning through to the default pattern resolver. This is required for // component-scanning. if (locationPattern.endsWith(ClassUtils.CLASS_FILE_SUFFIX)) { return super.getResources(locationPattern); } return new Resource[0]; } } /** * SAX {@link ErrorHandler} implementation that creates {@link ValidationProblem}s for all reported problems */ class BeansConfigErrorHandler implements ErrorHandler { /** * {@inheritDoc} */ public void warning(SAXParseException e) throws SAXException { problems.add(new ValidationProblem(IMarker.SEVERITY_WARNING, e.getMessage(), getCurrentResource(), e .getLineNumber())); } /** * {@inheritDoc} */ public void error(SAXParseException e) throws SAXException { problems.add(new ValidationProblem(IMarker.SEVERITY_ERROR, e.getMessage(), getCurrentResource(), e .getLineNumber())); } /** * {@inheritDoc} */ public void fatalError(SAXParseException e) throws SAXException { problems.add(new ValidationProblem(IMarker.SEVERITY_ERROR, e.getMessage(), getCurrentResource(), e .getLineNumber())); } } /** * Implementation of the Spring tooling API {@link ProblemReporter} interface. This implementation creates * {@link ValidationProblem}s for all reported problems. */ class BeansConfigProblemReporter implements ProblemReporter { /** * {@inheritDoc} */ public void fatal(Problem problem) { problems.add(new ValidationProblem(IMarker.SEVERITY_ERROR, getMessage(problem), getCurrentResource(), getLine(problem))); throw new BeanDefinitionParsingException(problem); } /** * {@inheritDoc} */ public void error(Problem problem) { problems.add(new ValidationProblem(IMarker.SEVERITY_ERROR, getMessage(problem), getCurrentResource(), getLine(problem))); } /** * {@inheritDoc} */ public void warning(Problem problem) { problems.add(new ValidationProblem(IMarker.SEVERITY_WARNING, getMessage(problem), getCurrentResource(), getLine(problem))); } /** * Returns the concatenated message form the {@link Problem} and the {@link Problem#getRootCause()}. */ private String getMessage(Problem problem) { StringBuffer message = new StringBuffer(problem.getMessage()); Throwable rootCause = problem.getRootCause(); if (rootCause != null) { // Retrieve nested exception while (rootCause.getCause() != null) { rootCause = rootCause.getCause(); } message.append(": "); message.append(rootCause.getMessage()); } return message.toString(); } /** * Returns the line of the problem by introspecting the {@link XmlSourceLocation}. */ private int getLine(Problem problem) { Object source = problem.getLocation().getSource(); if (source instanceof XmlSourceLocation) { return ((XmlSourceLocation) source).getStartLine(); } else if (source instanceof Node) { return LineNumberPreservingDOMParser.getStartLineNumber((Node) source); } return -1; } } /** * Special {@link ReaderEventListener} implementation that passes * {@link ReaderEventListener#componentRegistered(ComponentDefinition)} calls to this configs * {@link BeansConfig#registerComponentDefinition(ComponentDefinition, Map)}. * @author Christian Dupuis * @since 2.2.5 */ class BeansConfigPostProcessorReaderEventListener extends EmptyReaderEventListener { // Keep the contributed model element providers final Map<String, IModelElementProvider> elementProviders = NamespaceUtils.getElementProviders(); @Override public void componentRegistered(ComponentDefinition componentDefinition) { // make sure that all components that come through are safe for the model if (componentDefinition.getSource() == null) { if (componentDefinition instanceof BeanComponentDefinition) { ((AbstractBeanDefinition) ((BeanComponentDefinition) componentDefinition).getBeanDefinition()) .setSource(new DefaultModelSourceLocation(1, 1, resource)); } } registerComponentDefinition(componentDefinition, elementProviders); } } /** * Extension to {@link SimpleBeanDefinitionRegistry} that suppresses registrations of * {@link ScannedGenericBeanDefinition} instances as those contain references to the a classloader which we want to * discard. * @since 2.3.1 */ class ScannedGenericBeanDefinitionSuppressingBeanDefinitionRegistry extends SimpleBeanDefinitionRegistry { @Override public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) throws BeanDefinitionStoreException { beanRegistrationTracker.registerBean(beanName, beanDefinition); if (beanDefinition instanceof ScannedGenericBeanDefinition) { super.registerBeanDefinition(beanName, new InternalScannedGenericBeanDefinition( (ScannedGenericBeanDefinition) beanDefinition)); } else { super.registerBeanDefinition(beanName, beanDefinition); } } } /** * Alternative to {@link ScannedGenericBeanDefinition} that rejects the internal dependency to a ClassLoader hold by * the {@link AnnotationMetadata}. * @since 2.3.1 */ protected static class InternalScannedGenericBeanDefinition extends GenericBeanDefinition implements AnnotatedBeanDefinition { private static final long serialVersionUID = 467157320316462045L; private AnnotationMetadata metadata; private MethodMetadata factoryMethodMetadata; public InternalScannedGenericBeanDefinition(ScannedGenericBeanDefinition beanDefinition) { setBeanClassName(beanDefinition.getBeanClassName()); setSource(beanDefinition.getSource()); setResource(beanDefinition.getResource()); metadata = beanDefinition.getMetadata(); factoryMethodMetadata = beanDefinition.getFactoryMethodMetadata(); } public AnnotationMetadata getMetadata() { return metadata; } public MethodMetadata getFactoryMethodMetadata() { return factoryMethodMetadata; } } /** * Extension to the default {@link DefaultBeanDefinitionDocumentReader} to suppress import statements with * placeholders in resource attributes as this is not support in the IDE. * @since 2.3.1 */ static class ToolingFriendlyBeanDefinitionDocumentReader extends DefaultBeanDefinitionDocumentReader { private BeanDefinitionParserDelegate delegate; private BeansConfig beansConfig; public ToolingFriendlyBeanDefinitionDocumentReader(BeansConfig beansConfig) { this.beansConfig = beansConfig; } /** * {@inheritDoc} */ @Override protected void importBeanDefinitionResource(Element ele) { String location = ele.getAttribute(RESOURCE_ATTRIBUTE); if (SpringCoreUtils.hasPlaceHolder(location)) { getReaderContext().warning("Resource location contains placeholder", ele); } else { super.importBeanDefinitionResource(ele); } } /** * {@inheritDoc} */ @Override protected BeanDefinitionParserDelegate createDelegate(XmlReaderContext readerContext, Element root, BeanDefinitionParserDelegate parentDelegate) { BeanDefinitionParserDelegate delegate = new ErrorSuppressingBeanDefinitionParserDelegate(readerContext); delegate.initDefaults(root, parentDelegate); return delegate; } /** * {@inheritDoc} */ @Override protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) { BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele); if (bdHolder != null) { // Obtain source before decorating; decoration might discard the original source location Object source = bdHolder.getSource(); bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder); // Set the original source in case it got discared by the decorator if (bdHolder.getSource() == null) { ((AbstractBeanDefinition) bdHolder.getBeanDefinition()).setSource(source); } try { // Register the final decorated instance BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry()); } catch (BeanDefinitionStoreException ex) { getReaderContext().error( "Failed to register bean definition with name '" + bdHolder.getBeanName() + "'", ele, ex); } // Send registration event getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder)); } } @Override protected void doRegisterBeanDefinitions(Element root) { String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE); String[] specifiedProfiles = null; if (StringUtils.hasText(profileSpec)) { specifiedProfiles = StringUtils.tokenizeToStringArray(profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS); } // Spring 3.1 profile support; register composite component definition to carry nested beans ProfileAwareCompositeComponentDefinition cd = new ProfileAwareCompositeComponentDefinition(root.getNodeName(), getReaderContext().extractSource(root), specifiedProfiles); beansConfig.beanRegistrationTracker.enter(cd); try { // any nested <beans> elements will cause recursion in this method. In // order to propagate and preserve <beans> default-* attributes correctly, // keep track of the current (parent) delegate, which may be null. Create // the new (child) delegate with a reference to the parent for fallback purposes, // then ultimately reset this.delegate back to its original (parent) reference. // this behavior emulates a stack of delegates without actually necessitating one. BeanDefinitionParserDelegate parent = this.delegate; this.delegate = createDelegate(getReaderContext(), root, parent); preProcessXml(root); parseBeanDefinitions(root, this.delegate); postProcessXml(root); this.delegate = parent; } finally { beansConfig.beanRegistrationTracker.exit(cd); getReaderContext().fireComponentRegistered(cd); } } } class ProfileAwareReaderContext extends XmlReaderContext { private ReaderEventListener eventListener; public ProfileAwareReaderContext(Resource resource, ProblemReporter problemReporter, ReaderEventListener eventListener, SourceExtractor sourceExtractor, XmlBeanDefinitionReader reader, NamespaceHandlerResolver namespaceHandlerResolver) { super(resource, problemReporter, eventListener, sourceExtractor, reader, namespaceHandlerResolver); this.eventListener = eventListener; } @Override public void fireComponentRegistered(ComponentDefinition componentDefinition) { CompositeComponentDefinition currentContext = beanRegistrationTracker.currentContext(); if (currentContext!=null) { currentContext.addNestedComponent(componentDefinition); } else { eventListener.componentRegistered(componentDefinition); } } } /** * Extension to {@link BeanDefinitionParserDelegate} that captures the class and linkage errors from loading * {@link NamespaceHandler}s instances. * @since 2.3.1 */ static class ErrorSuppressingBeanDefinitionParserDelegate extends BeanDefinitionParserDelegate { private final XmlReaderContext readerContext; public ErrorSuppressingBeanDefinitionParserDelegate(XmlReaderContext readerContext) { super(readerContext); this.readerContext = readerContext; } /** * {@inheritDoc} */ @Override public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) { String namespaceUri = getNamespaceURI(ele); try { readerContext.getNamespaceHandlerResolver().resolve(namespaceUri); } catch (FatalBeanException e) { // Beautify the exception message bit String msg = e.getMessage(); int ix = msg.indexOf(';'); if (ix > 0) { msg = msg.substring(0, ix); } readerContext.warning(msg + ". Check Error Log for more details.", ele); BeansCorePlugin.log(new Status(IStatus.WARNING, BeansCorePlugin.PLUGIN_ID, "Problem loading NamespaceHandler for '" + namespaceUri + "'.", e)); return null; } return super.parseCustomElement(ele, containingBd); } } public BeanDefinitionRegistry getRawBeanDefinitions(CompositeComponentDefinition context) { return beanRegistrationTracker.getRegistry(context); } }