blob: d5143e99a1c9e2f160324a8ad54614a12b21fbfc [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2019 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - derived implementation
* Martin Karpisek - bug 195840
*******************************************************************************/
package org.eclipse.ant.internal.ui.editor.utils;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Location;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.ProjectHelperRepository;
import org.apache.tools.ant.RuntimeConfigurable;
import org.apache.tools.ant.Target;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.UnknownElement;
import org.apache.tools.ant.helper.AntXMLContext;
import org.apache.tools.ant.helper.ProjectHelper2;
import org.apache.tools.ant.util.FileUtils;
import org.apache.tools.ant.util.JAXPUtils;
import org.eclipse.ant.internal.core.IAntCoreConstants;
import org.eclipse.ant.internal.ui.model.IAntModel;
import org.eclipse.core.resources.IFile;
import org.eclipse.jface.text.BadLocationException;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.ext.LexicalHandler;
/**
* Derived from the original Ant ProjectHelper2 with help from the JAXPUtils class. This class provides parsing for using a String as a source and
* provides handlers that will continue parsing to completion upon hitting errors.
*/
public class ProjectHelper extends ProjectHelper2 {
/**
* helper for path -> URI and URI -> path conversions.
*/
private static FileUtils fu = null;
/**
* The build file that is to be parsed. Must be set if parsing is to be successful.
*/
private File buildFile = null;
private static String currentEntityName = null;
private static String currentEntityPath = null;
private static int currentImportStackSize = 1;
/**
* The Ant Model
*/
private static IAntModel fgAntModel;
/**
* The current Ant parsing context
*/
private static AntXMLContext fgAntContext;
private static AntHandler elementHandler = new ElementHandler();
private static AntHandler projectHandler = new ProjectHandler();
private static AntHandler targetHandler = new TargetHandler();
private static AntHandler mainHandler = new MainHandler();
private static LexicalHandler lexicalHandler = new LexHandler();
private static XMLReader fgXMLReader = null;
/*
* Required to remember the project names (in case they are required for target-prefixing). The build file(s) are parsed anyway and hence the
* project name is added to this map when it gets parsed.
*/
private static Map<String, String> parsedProjectNames = null;
public static class ElementHandler extends ProjectHelper2.ElementHandler {
private UnknownElement task = null;
private Task currentTask = null;
private Map<String, String> fNormalizedFileNames = new HashMap<>();
@Override
public AntHandler onStartChild(String uri, String tag, String qname, Attributes attrs, AntXMLContext context) {
return ProjectHelper.elementHandler;
}
@Override
public void onStartElement(String uri, String tag, String qname, Attributes attrs, AntXMLContext context) {
try {
RuntimeConfigurable wrapper = context.currentWrapper();
currentTask = null;
task = null;
if (wrapper != null) {
currentTask = (Task) wrapper.getProxy();
}
onStartElement0(uri, tag, qname, attrs, context);
Locator locator = context.getLocator();
getAntModel().addTask(task, currentTask, attrs, locator.getLineNumber(), locator.getColumnNumber());
}
catch (BuildException be) {
Locator locator = context.getLocator();
getAntModel().addTask(task, currentTask, attrs, locator.getLineNumber(), locator.getColumnNumber());
getAntModel().error(be);
}
}
@Override
public void onEndElement(String uri, String tag, AntXMLContext context) {
super.onEndElement(uri, tag, context);
Locator locator = context.getLocator();
if (getAntModel().canGetTaskInfo()) {
getAntModel().setCurrentElementLength(locator.getLineNumber(), locator.getColumnNumber());
}
}
private void onStartElement0(String uri, String tag, String qname, Attributes attrs, AntXMLContext context) {
RuntimeConfigurable parentWrapper = context.currentWrapper();
Object parent = null;
if (parentWrapper != null) {
parent = parentWrapper.getProxy();
}
/*
* UnknownElement is used for tasks and data types - with delayed eval
*/
task = new UnknownElement(tag);
task.setProject(context.getProject());
task.setNamespace(uri);
task.setQName(qname);
task.setTaskType(org.apache.tools.ant.ProjectHelper.genComponentName(task.getNamespace(), tag));
task.setTaskName(qname);
Locator contextLocator = context.getLocator();
String fileName = contextLocator.getSystemId();
String normalizedFileName = fNormalizedFileNames.get(fileName);
if (normalizedFileName == null) {
if (fileName.startsWith(IAntCoreConstants.FILE_PROTOCOL)) {
normalizedFileName = getFileUtils().fromURI(fileName);
fNormalizedFileNames.put(fileName, normalizedFileName);
} else {
normalizedFileName = fileName;
}
}
Target currentTarget = context.getCurrentTarget();
Location location = new Location(normalizedFileName, contextLocator.getLineNumber(), contextLocator.getColumnNumber());
task.setLocation(location);
task.setOwningTarget(currentTarget);
if (parent != null) {
// Nested element
((UnknownElement) parent).addChild(task);
} else {
// Task included in a target ( including the default one ).
currentTarget.addTask(task);
}
// do not configure the id of an augment node, it is resolved
// when it is configured
if (!IAntCoreConstants.AUGMENT.equals(task.getQName())) {
context.configureId(task, attrs);
}
// container.addTask(task);
// This is a nop in UE: task.init();
RuntimeConfigurable wrapper = new RuntimeConfigurable(task, task.getTaskName());
for (int i = 0; i < attrs.getLength(); i++) {
String attrUri = attrs.getURI(i);
if (attrUri != null && attrUri.length() != 0 && !attrUri.equals(uri)) {
continue; // Ignore attributes from unknown uris
}
String name = attrs.getLocalName(i);
String value = attrs.getValue(i);
// PR: Hack for ant-type value
// an ant-type is a component name which can
// be namespaced, need to extract the name
// and convert from qualified name to uri/name
if (name.equals("ant-type")) { //$NON-NLS-1$
int index = value.indexOf(':');
if (index != -1) {
String prefix = value.substring(0, index);
String mappedUri = context.getPrefixMapping(prefix);
if (mappedUri == null) {
throw new BuildException("Unable to find XML NS prefix " + prefix); //$NON-NLS-1$
}
value = org.apache.tools.ant.ProjectHelper.genComponentName(mappedUri, value.substring(index + 1));
}
}
wrapper.setAttribute(name, value);
}
if (parentWrapper != null) {
parentWrapper.addChild(wrapper);
}
context.pushWrapper(wrapper);
}
@Override
public void characters(char[] buf, int start, int count, AntXMLContext context) {
try {
super.characters(buf, start, count, context);
}
catch (SAXParseException e) {
ErrorHelper.handleErrorFromElementText(start, count, context, e);
}
catch (BuildException be) {
ErrorHelper.handleErrorFromElementText(start, count, context, be);
}
}
public void reset() {
task = null;
currentTask = null;
fNormalizedFileNames.clear();
}
}
public static class MainHandler extends ProjectHelper2.MainHandler {
@Override
public AntHandler onStartChild(String uri, String name, String qname, Attributes attrs, AntXMLContext context) throws SAXParseException {
if (name.equals("project") //$NON-NLS-1$
&& (uri.length() == 0 || uri.equals(ANT_CORE_URI))) {
return ProjectHelper.projectHandler;
}
try {
return super.onStartChild(uri, name, qname, attrs, context);
}
catch (SAXParseException e) {
getAntModel().error(e);
throw e;
}
}
}
/**
* Gets the associated project name by the absolute build-file path
*
* @param buildFile
* The file
* @return The project name
*/
public static String getProjectNameOfBuildFile(IFile buildFile) {
return parsedProjectNames.get(getBuildFileKey(buildFile));
}
/**
* Gets the associated project name by the absolute build-file path
*
* @param buildFile
* The file
* @return The project name
*/
public static String getProjectNameOfBuildFile(String absolutePath) {
return parsedProjectNames.get(absolutePath);
}
/**
* Builds the hash-map's build-file key
*
* @param buildFile
* The build file
* @return The key as string
*/
private static String getBuildFileKey(IFile buildFile) {
return buildFile.getLocation().toFile().getAbsolutePath();
}
/**
* Adds a parsed project-name to the property-holder (and initializes the collection if necessary)
*
* @param key
* The key (file-path).
* @param projectName
* The projectname to add.
*/
public static void storeParsedProjectName(String key, String projectName) {
// init if required
if (parsedProjectNames == null) {
parsedProjectNames = new HashMap<>();
}
parsedProjectNames.put(key, projectName);
}
/**
* Clear the parsed project-holder to avoid potential memory leaks.
*/
public static void clearAdditionalPropertyHolders() {
/*
* Currently only this "property-holder" is used. Extend if necessary.
*/
if (parsedProjectNames != null) {
parsedProjectNames.clear();
parsedProjectNames = null;
}
}
public static class ProjectHandler extends ProjectHelper2.ProjectHandler {
@Override
public AntHandler onStartChild(String uri, String name, String qname, Attributes attrs, AntXMLContext context) {
if ((name.equals("target") || name.equals("extension-point"))//$NON-NLS-1$ //$NON-NLS-2$
&& (uri.length() == 0 || uri.equals(ANT_CORE_URI))) {
return ProjectHelper.targetHandler;
}
return ProjectHelper.elementHandler;
}
@Override
public void onEndElement(String uri, String tag, AntXMLContext context) {
super.onEndElement(uri, tag, context);
if (currentImportStackSize == 1) {
Locator locator = context.getLocator();
getAntModel().setCurrentElementLength(locator.getLineNumber(), locator.getColumnNumber());
}
}
@Override
public void onStartElement(String uri, String tag, String qname, Attributes attrs, AntXMLContext context) {
try {
super.onStartElement(uri, tag, qname, attrs, context);
// add project-name to property holder (if no aliases are used, the project name is required for target prefixing)
String currentProjectName = context.getCurrentProjectName();
// just an additional check if the name is non-empty
if (this.isCurrentProjectNameValid(currentProjectName)) {
if (context.getBuildFile() != null) {
storeParsedProjectName(context.getBuildFile().getAbsolutePath(), currentProjectName);
} else if (context.getBuildFileURL() != null) {
storeParsedProjectName(new File(context.getBuildFileURL().getPath()).getAbsolutePath(), currentProjectName);
}
}
}
catch (SAXParseException e) {
getAntModel().error(e);
}
catch (BuildException be) {
getAntModel().error(be);
}
if (context.getCurrentTarget() == null) {
// exception occurred creating the project
context.getProject().addTarget(IAntCoreConstants.EMPTY_STRING, context.getImplicitTarget());
context.setCurrentTarget(context.getImplicitTarget());
}
if (currentImportStackSize == 1) {
Locator locator = context.getLocator();
getAntModel().addProject(context.getProject(), locator.getLineNumber(), locator.getColumnNumber());
}
}
/**
* Checks if the parsed value is a valid project name (i.e. is non-empty: It doesn't make sense to store empty project-names)
*
* @param currentProjectName
* The current project name
* @return If the given project name is valid or not.
*/
private boolean isCurrentProjectNameValid(String currentProjectName) {
return currentProjectName != null && !currentProjectName.isEmpty();
}
@Override
public void characters(char[] buf, int start, int count, AntXMLContext context) {
try {
super.characters(buf, start, count, context);
}
catch (SAXParseException e) {
ErrorHelper.handleErrorFromElementText(start, count, context, e);
}
catch (BuildException be) {
ErrorHelper.handleErrorFromElementText(start, count, context, be);
}
}
}
public static class TargetHandler extends ProjectHelper2.TargetHandler {
@Override
public AntHandler onStartChild(String uri, String name, String qname, Attributes attrs, AntXMLContext context) {
return ProjectHelper.elementHandler;
}
@Override
public void onStartElement(String uri, String tag, String qname, Attributes attrs, AntXMLContext context) {
try {
super.onStartElement(uri, tag, qname, attrs, context);
Target newTarget = context.getCurrentTarget();
Locator locator = context.getLocator();
getAntModel().addTarget(newTarget, locator.getLineNumber(), locator.getColumnNumber());
}
catch (SAXParseException e) {
handleErrorInTarget(context, e);
}
catch (BuildException be) {
handleErrorInTarget(context, be);
}
}
private void handleErrorInTarget(AntXMLContext context, Exception e) {
Target newTarget = context.getCurrentTarget();
Locator locator = context.getLocator();
getAntModel().addTarget(newTarget, locator.getLineNumber(), locator.getColumnNumber());
getAntModel().errorFromElement(e, null, locator.getLineNumber(), locator.getColumnNumber());
}
@Override
public void onEndElement(String uri, String tag, AntXMLContext context) {
super.onEndElement(uri, tag, context);
Locator locator = context.getLocator();
getAntModel().setCurrentElementLength(locator.getLineNumber(), locator.getColumnNumber());
}
@Override
public void characters(char[] buf, int start, int count, AntXMLContext context) {
try {
super.characters(buf, start, count, context);
}
catch (SAXParseException e) {
ErrorHelper.handleErrorFromElementText(start, count, context, e);
}
catch (BuildException be) {
ErrorHelper.handleErrorFromElementText(start, count, context, be);
}
}
}
public static class RootHandler extends ProjectHelper2.RootHandler {
public RootHandler(AntXMLContext context, AntHandler rootHandler) {
super(context, rootHandler);
}
@Override
public void error(SAXParseException e) {
getAntModel().error(e);
}
@Override
public void fatalError(SAXParseException e) {
getAntModel().fatalError(e);
}
@Override
public void warning(SAXParseException e) {
getAntModel().warning(e);
}
@Override
public InputSource resolveEntity(String publicId, String systemId) {
InputSource source = super.resolveEntity(publicId, systemId);
if (source != null) {
String path = getFileUtils().fromURI(source.getSystemId());
if (currentEntityName == null) {
currentEntityPath = path;
} else {
getAntModel().addEntity(currentEntityName, path);
currentEntityName = null;
}
}
return source;
}
@Override
public void startPrefixMapping(String prefix, String uri) {
super.startPrefixMapping(prefix, uri);
getAntModel().addPrefixMapping(prefix, uri);
}
}
private static class ErrorHelper {
public static void handleErrorFromElementText(int start, int count, AntXMLContext context, Exception e) {
Locator locator = context.getLocator();
int columnNumber = locator.getColumnNumber();
if (columnNumber > -1) {
int offset = start;
try {
offset = getAntModel().getOffset(locator.getLineNumber(), 1);
}
catch (BadLocationException e1) {
// do nothing
}
getAntModel().errorFromElementText(e, offset, locator.getColumnNumber());
} else {
getAntModel().errorFromElementText(e, start, count);
}
}
}
private static class LexHandler implements LexicalHandler {
@Override
public void endCDATA() throws SAXException {
// do nothing
}
@Override
public void endDTD() throws SAXException {
if (getAntModel().canGetLexicalInfo()) {
AntXMLContext context = getContext();
Locator locator = context.getLocator();
getAntModel().setCurrentElementLength(locator.getLineNumber(), locator.getColumnNumber());
}
}
@Override
public void startCDATA() throws SAXException {
// do nothing
}
@Override
public void comment(char[] ch, int start, int length) throws SAXException {
if (getAntModel().canGetLexicalInfo()) {
AntXMLContext context = getContext();
Locator locator = context.getLocator();
if (locator != null) {
getAntModel().addComment(locator.getLineNumber(), locator.getColumnNumber(), length);
}
}
}
@Override
public void endEntity(String name) throws SAXException {
// do nothing
}
@Override
public void startEntity(String name) throws SAXException {
if (currentEntityPath == null) {
currentEntityName = name;
} else {
getAntModel().addEntity(name, currentEntityPath);
currentEntityPath = null;
}
}
@Override
public void startDTD(String name, String publicId, String systemId) throws SAXException {
if (getAntModel().canGetLexicalInfo()) {
AntXMLContext context = getContext();
Locator locator = context.getLocator();
getAntModel().addDTD(name, locator.getLineNumber(), locator.getColumnNumber());
}
}
}
public ProjectHelper(IAntModel model) {
setAntModel(model);
}
/**
* Constructor
* <p>
* This constructor is only to be used by the {@link ProjectHelperRepository} when loading instances of registered helpers.
* </p>
*
* @since 3.7
* @noreference This constructor is not intended to be referenced by clients.
*/
public ProjectHelper() {
}
/**
* Parses the project file, configuring the project as it goes.
*
* @param project
* the current project
* @param source
* the XML source or a {@link File}
* @param handler
* the root handler to use (contains the current context)
* @exception BuildException
* if the configuration is invalid or cannot be read
*/
@Override
public void parse(Project project, Object source, ProjectHelper2.RootHandler handler) throws BuildException {
if (!(source instanceof String) && !(source instanceof File)) {
// this should only occur with a source URL and that should not be possible currently
// as Antlib hard codes using ProjectHelper2 (bug 152793)
super.parse(project, source, handler);
return;
}
AntXMLContext context = (AntXMLContext) project.getReference("ant.parsing.context"); //$NON-NLS-1$
// switch to using "our" handler so parsing will continue on hitting errors.
handler = new RootHandler(context, mainHandler);
InputStream stream = null;
try {
InputSource inputSource = null;
if ((source instanceof File)) {
buildFile = (File) source;
buildFile = getFileUtils().normalize(buildFile.getAbsolutePath());
stream = new FileInputStream(buildFile);
inputSource = new InputSource(stream);
} else if (source instanceof String) {
IAntModel model = getAntModel();
String encoding = IAntCoreConstants.UTF_8;
if (model != null) {
encoding = model.getEncoding();
}
stream = new ByteArrayInputStream(((String) source).getBytes(encoding));
inputSource = new InputSource(stream);
}
/**
* SAX 2 style parser used to parse the given file.
*/
// We cannot use the JAXPUtils support here as the underlying parser factory is cached and
// will not reflect classpath changes that effect which XML parser will be returned.
// see bug 59764
// XMLReader parser = JAXPUtils.getNamespaceXMLReader();
XMLReader parser = getNamespaceXMLReader();
if (parser == null) {
throw new BuildException(ProjectHelperMessages.ProjectHelper_0);
}
String uri = null;
if (buildFile != null) {
uri = getFileUtils().toURI(buildFile.getAbsolutePath());
}
if (uri != null) {
inputSource.setSystemId(uri);
}
context.setBuildFile(buildFile);
parser.setContentHandler(handler);
parser.setEntityResolver(handler);
parser.setErrorHandler(handler);
parser.setDTDHandler(handler);
parser.setProperty("http://xml.org/sax/properties/lexical-handler", lexicalHandler); //$NON-NLS-1$
parser.parse(inputSource);
}
catch (SAXParseException exc) {
getAntModel().fatalError(exc);
}
catch (SAXException exc) {
// ignore as we will be parsing incomplete source
}
catch (FileNotFoundException exc) {
throw new BuildException(exc);
}
catch (UnsupportedEncodingException exc) {
throw new BuildException(exc);
}
catch (IOException exc) {
throw new BuildException(exc);
}
finally {
try {
if (stream != null) {
stream.close();
}
}
catch (IOException ioe) {
// ignore this
}
}
}
/**
* Sets the buildfile that is about to be parsed or <code>null</code> if parsing has completed.
*
* @param file
* The buildfile about to be parsed
*/
public void setBuildFile(File file) {
buildFile = file;
currentImportStackSize = 1;
}
/*
* We override this method from ProjectHelper2 as we do not want to execute the implicit target or any other target for that matter as it could
* hang Eclipse. See https://bugs.eclipse.org/bugs/show_bug.cgi?id=50795 for more details.
*/
@Override
public void parse(Project project, Object source) throws BuildException {
AntXMLContext context = (AntXMLContext) project.getReference("ant.parsing.context"); //$NON-NLS-1$
if (context == null) {
getImportStack().removeAllElements();
context = new AntXMLContext(project);
project.addReference("ant.parsing.context", context); //$NON-NLS-1$
project.addReference("ant.targets", context.getTargets()); //$NON-NLS-1$
fgAntContext = context;
}
getImportStack().addElement(source);
currentImportStackSize = getImportStack().size();
if (getImportStack().size() > 1) {
// we are in an imported file.
context.setIgnoreProjectTag(true);
Target currentTarget = context.getCurrentTarget();
Target currentImplicit = context.getImplicitTarget();
Map<String, Target> currentTargets = context.getCurrentTargets();
try {
Target newCurrent = new Target();
newCurrent.setProject(project);
newCurrent.setName(IAntCoreConstants.EMPTY_STRING);
context.setCurrentTarget(newCurrent);
context.setCurrentTargets(new HashMap<String, Target>());
context.setImplicitTarget(newCurrent);
parse(project, source, new RootHandler(context, mainHandler));
}
finally {
context.setCurrentTarget(currentTarget);
context.setImplicitTarget(currentImplicit);
context.setCurrentTargets(currentTargets);
}
} else {
// top level file
context.setCurrentTargets(new HashMap<String, Target>());
parse(project, source, new RootHandler(context, mainHandler));
}
}
public static void reset() {
fgXMLReader = null;
fu = null;
}
public static void setAntModel(IAntModel antModel) {
fgAntModel = antModel;
((ProjectHelper.ElementHandler) elementHandler).reset();
fu = null;
fgAntContext = null;
}
public static IAntModel getAntModel() {
return fgAntModel;
}
public static AntXMLContext getContext() {
return fgAntContext;
}
private static FileUtils getFileUtils() {
if (fu == null) {
fu = FileUtils.getFileUtils();
}
return fu;
}
/**
* Returns a newly created SAX 2 XMLReader, which is namespace aware
*
* @return a SAX 2 XMLReader.
* @since Ant 1.6 from org.apache.tools.ant.util.JAXPUtils
*/
private XMLReader getNamespaceXMLReader() throws BuildException {
if (fgXMLReader == null) {
try {
fgXMLReader = newSAXParser(getNSParserFactory()).getXMLReader();
}
catch (SAXException e) {
// do nothing
}
}
return fgXMLReader;
}
/**
* Returns the parser factory to use to create namespace aware parsers.
*
* @return a SAXParserFactory to use which supports manufacture of namespace aware parsers
*
* @since Ant 1.6 from org.apache.tools.ant.util.JAXPUtils
*/
private SAXParserFactory getNSParserFactory() throws BuildException {
SAXParserFactory nsParserFactory = JAXPUtils.newParserFactory();
nsParserFactory.setNamespaceAware(true);
return nsParserFactory;
}
/**
* @return a new SAXParser instance as helper for getParser and getXMLReader.
*
* @since Ant 1.5 from org.apache.tools.ant.util.JAXPUtils
*/
private SAXParser newSAXParser(SAXParserFactory factory) {
try {
return factory.newSAXParser();
}
catch (ParserConfigurationException e) {
// do nothing
}
catch (SAXException e) {
// do nothing
}
return null;
}
}