/*******************************************************************************
* 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.springframework.ide.eclipse.metadata.locate;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentSkipListSet;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.ide.eclipse.beans.core.model.locate.AbstractJavaProjectPathMatchingBeansConfigLocator;
import org.springframework.ide.eclipse.beans.core.model.locate.IBeansConfigLocator;
import org.springframework.ide.eclipse.core.SpringCore;
import org.springframework.ide.eclipse.core.SpringCoreUtils;
import org.springframework.ide.eclipse.metadata.MetadataPlugin;
import org.springframework.util.StringUtils;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.XmlWebApplicationContext;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* {@link IBeansConfigLocator} implementation that uses the information from a <code>web.xml</code> to detect Spring
* configuration files.
* @author Christian Dupuis
* @author Leo Dos Santos
* @since 1.0.0
*/
public class DynamicWebProjectBeansConfigLocator extends AbstractJavaProjectPathMatchingBeansConfigLocator {
/** Name of the config set to be created for the auto-detected configs */
private static final String CONFIG_SET_NAME = "web-context"; //$NON-NLS-1$
/** Context parameter name to configure non-default config locations */
private static final String CONTEXT_CONFIG_LOCATION_PARAM_NAME = "contextConfigLocation"; //$NON-NLS-1$
/** The FQCN of the {@link ContextLoaderListener} */
private static final String CONTEXT_CONTEXT_LOADER_LISTENER_SERVLET = "org.springframework.web.context.ContextLoaderListener"; //$NON-NLS-1$
/** The FQCN of the {@link ContextLoaderServlet} */
private static final String CONTEXT_CONTEXT_LOADER_SERVLET_CLASS = "org.springframework.web.context.ContextLoaderServlet"; //$NON-NLS-1$
/** The FQCN of the DispatcherServlet */
private static final String DISPATCHER_SERVLET_CLASS = "org.springframework.web.servlet.DispatcherServlet"; //$NON-NLS-1$
/** The FQCN of the Spring WS MessageDispatcherServlet */
private static final String MESSAGE_DISPATCHER_SERVLET_CLASS = "org.springframework.ws.transport.http.MessageDispatcherServlet"; //$NON-NLS-1$
private static final String LISTENER_XPATH_EXPRESSION = "//web-app/listener";
private static final String CONTEXT_PARAM_XPATH_EXPRESSION = "//web-app/context-param";
private static final String SERVLET_XPATH_EXPRESSION = "//web-app/servlet";
private static final String LISTENER_CLASS = "listener-class";
private static final String INIT_PARAM = "init-param";
private static final String PARAM_NAME = "param-name";
private static final String PARAM_VALUE = "param-value";
private static final String SERVLET_CLASS = "servlet-class";
private static final String SERVLET_NAME = "servlet-name";
/** Default servlet context file suffix */
private static final String SERVLET_CONTEXT_SUFFIX = "-servlet.xml"; //$NON-NLS-1$
/** The detected file patterns to search for */
private Set<String> filePatterns = new ConcurrentSkipListSet<String>();
private final XPathExpression servletExpression;
private final XPathExpression listenerExpression;
private final XPathExpression contextParamExpression;
{
try {
XPathFactory newInstance = XPathFactory.newInstance();
XPath xpath = newInstance.newXPath();
this.servletExpression = xpath.compile(SERVLET_XPATH_EXPRESSION);
this.listenerExpression = xpath.compile(LISTENER_XPATH_EXPRESSION);
this.contextParamExpression = xpath.compile(CONTEXT_PARAM_XPATH_EXPRESSION);
}
catch (XPathExpressionException e) {
throw new RuntimeException(e);
}
}
@Override
public String getBeansConfigSetName(Set<IFile> files) {
return CONFIG_SET_NAME;
}
/**
* Returns <code>true</code> if the given <code>file</code> is a <code>web.xml</code>.
*/
public boolean requiresRefresh(IFile file) {
if (file.getFullPath().toString().endsWith("WEB-INF/web.xml")) {
IFile webArtifact = SpringCoreUtils.getDeploymentDescriptor(file.getProject());
if (webArtifact != null) {
return file.getFullPath().equals(webArtifact.getFullPath());
}
}
return false;
}
/**
* Returns <code>true</code> if the given project has the Spring IDE nature and has the <code>jst.web</code> facet.
*/
public boolean supports(IProject project) {
return SpringCoreUtils.isSpringProject(project) && SpringCoreUtils.hasProjectFacet(project, "jst.web"); //$NON-NLS-1$
}
private String getChildValue(Element node, String nodeName) {
NodeList children = node.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
if (nodeName.equals(child.getNodeName())) {
return child.getTextContent();
}
}
return null;
}
/**
* Creates file patterns from the given {@link #CONTEXT_CONFIG_LOCATION_PARAM_NAME} context parameters.
*/
private boolean processContextConfigLocationParameter(NodeList initParams) {
boolean nonDefaultLocations = false;
for (int i = 0; i < initParams.getLength(); i++) {
String name = null;
String value = null;
Node initParam = initParams.item(i);
NodeList initParamChildren = initParam.getChildNodes();
for (int j = 0; j < initParamChildren.getLength(); j++) {
Node initParamChild = initParamChildren.item(j);
if (PARAM_NAME.equals(initParamChild.getNodeName())) {
name = initParamChild.getTextContent();
}
else if (PARAM_VALUE.equals(initParamChild.getNodeName())) {
value = initParamChild.getTextContent();
}
}
if (CONTEXT_CONFIG_LOCATION_PARAM_NAME.equals(name) && StringUtils.hasText(value)) {
String[] configLocations = StringUtils.tokenizeToStringArray(value,
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
nonDefaultLocations = true;
for (String configLocation : configLocations) {
// Remove classpath: protocal from config location
int ix = configLocation.indexOf(':');
if (ix > 0) {
configLocation = configLocation.substring(ix + 1);
}
filePatterns.add(configLocation);
}
}
}
// By default if non specific configuration is found we need to install the default location
if (!nonDefaultLocations) {
filePatterns.add(XmlWebApplicationContext.DEFAULT_CONFIG_LOCATION);
}
return nonDefaultLocations;
}
/**
* Reads the <code>web.xml</code> deployment descriptor and searches for {@link ContextLoaderListener},
* {@link ContextLoaderServlet} and DispatcherServlet configuration sections.
*/
@Override
protected boolean canLocateInProject(IProject project) {
// Make sure it is a java project
if (!super.canLocateInProject(project)) {
return false;
}
// Add the path to the WEB-INF directory as root dir
IFile deploymentDescriptor = SpringCoreUtils.getDeploymentDescriptor(project);
if (deploymentDescriptor != null) {
try {
NodeList nodes = (NodeList) servletExpression.evaluate(SpringCoreUtils
.parseDocument(deploymentDescriptor), XPathConstants.NODESET);
for (int i = 0; i < nodes.getLength(); i++) {
Element element = (Element) nodes.item(i);
String servletName = getChildValue(element, SERVLET_NAME);
String servletClass = getChildValue(element, SERVLET_CLASS);
if (StringUtils.hasText(servletName) && StringUtils.hasText(servletClass)) {
// Figure out the DispatcherServlet names to add
// "WEB-INF/<servlet-name>-servlet.xml" as file pattern and install default
// pattern if required.
if (DISPATCHER_SERVLET_CLASS.equals(servletClass)
|| MESSAGE_DISPATCHER_SERVLET_CLASS.equals(servletClass)) {
NodeList initParams = element.getElementsByTagName(INIT_PARAM);
boolean nonDefaultLocations = processContextConfigLocationParameter(initParams);
if (!nonDefaultLocations) {
filePatterns.add(new StringBuilder(
XmlWebApplicationContext.DEFAULT_CONFIG_LOCATION_PREFIX).append(servletName)
.append(SERVLET_CONTEXT_SUFFIX).toString());
}
}
// Add ContextLoaderServlet configuration
else if (CONTEXT_CONTEXT_LOADER_SERVLET_CLASS.equals(servletClass)) {
NodeList initParams = element.getElementsByTagName(INIT_PARAM);
processContextConfigLocationParameter(initParams);
}
}
}
}
catch (Exception e) {
SpringCore.log(new Status(IStatus.WARNING, MetadataPlugin.PLUGIN_ID, 1, e.getMessage(), e));
}
// Add ContextLoaderListener configurations
try {
NodeList nodes = (NodeList) listenerExpression.evaluate(SpringCoreUtils
.parseDocument(deploymentDescriptor), XPathConstants.NODESET);
for (int i = 0; i < nodes.getLength(); i++) {
Element element = (Element) nodes.item(i);
String listenerClass = getChildValue(element, LISTENER_CLASS);
if (CONTEXT_CONTEXT_LOADER_LISTENER_SERVLET.equals(listenerClass)) {
NodeList contextParams = (NodeList) contextParamExpression.evaluate(SpringCoreUtils
.parseDocument(deploymentDescriptor), XPathConstants.NODESET);
processContextConfigLocationParameter(contextParams);
break;
}
}
}
catch (Exception e) {
SpringCore.log(new Status(IStatus.WARNING, MetadataPlugin.PLUGIN_ID, 1, e.getMessage(), e));
}
}
else {
return false;
}
return true;
}
/**
* {@inheritDoc}
*/
@Override
protected Set<String> getAllowedFilePatterns() {
return filePatterns;
}
/**
* Returns the root directories to search. Basically returns the java source folders and location of the
* <code>web.xml</code> as root directories.
*/
@Override
protected Set<IPath> getRootDirectories(IProject project) {
Set<IPath> rootDirectories = new LinkedHashSet<IPath>(super.getRootDirectories(project));
// Add the path to the WEB-INF directory as root dir
IFile webArtifact = SpringCoreUtils.getDeploymentDescriptor(project);
if (webArtifact != null) {
IPath path = webArtifact.getFullPath();
if (path != null) {
// Remove /WEB-INF/web.xml from path
rootDirectories.add(path.removeLastSegments(2));
}
}
return rootDirectories;
}
}