blob: ca12a92499ddc5eccdbb0b504eb2097f201405be [file] [log] [blame]
/*
* Copyright 2000-2010 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.lang.ant.dom;
import com.intellij.lang.ant.AntFilesProvider;
import com.intellij.lang.ant.AntSupport;
import com.intellij.lang.ant.ReflectedProject;
import com.intellij.lang.ant.config.impl.AntResourcesClassLoader;
import com.intellij.lang.properties.IProperty;
import com.intellij.lang.properties.psi.PropertiesFile;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileTypes.LanguageFileType;
import com.intellij.openapi.fileTypes.StdFileTypes;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiFileFactory;
import com.intellij.psi.PsiFileSystemItem;
import com.intellij.psi.xml.XmlElement;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.LocalTimeCounter;
import com.intellij.util.xml.XmlName;
import gnu.trove.THashMap;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
/**
* Storage for user-defined tasks and data types
* parsed from ant files
* @author Eugene Zhuravlev
* Date: Jul 1, 2010
*/
public class CustomAntElementsRegistry {
public static ThreadLocal<Boolean> ourIsBuildingClasspathForCustomTagLoading = new ThreadLocal<Boolean>() {
protected Boolean initialValue() {
return Boolean.FALSE;
}
};
private static final Logger LOG = Logger.getInstance("#com.intellij.lang.ant.dom.CustomAntElementsRegistry");
private static final Key<CustomAntElementsRegistry> REGISTRY_KEY = Key.create("_custom_element_registry_");
private final Map<XmlName, ClassProvider> myCustomElements = new THashMap<XmlName, ClassProvider>();
private final Map<AntDomNamedElement, String> myTypeDefErrors = new THashMap<AntDomNamedElement, String>();
private final Map<XmlName, AntDomNamedElement> myDeclarations = new THashMap<XmlName, AntDomNamedElement>();
private final Map<String, ClassLoader> myNamedLoaders = new THashMap<String, ClassLoader>();
private CustomAntElementsRegistry(final AntDomProject antProject) {
antProject.accept(new CustomTagDefinitionFinder(antProject));
}
public static CustomAntElementsRegistry getInstance(AntDomProject antProject) {
CustomAntElementsRegistry registry = antProject.getContextAntProject().getUserData(REGISTRY_KEY);
if (registry == null) {
registry = new CustomAntElementsRegistry(antProject);
antProject.putUserData(REGISTRY_KEY, registry);
}
return registry;
}
@NotNull
public Set<XmlName> getCompletionVariants(AntDomElement parentElement) {
if (parentElement instanceof AntDomCustomElement) {
// this case is already handled in AntDomExtender when defining children
return Collections.emptySet();
}
final Set<XmlName> result = new HashSet<XmlName>();
final Pair<AntDomMacroDef, AntDomScriptDef> contextMacroOrScriptDef = getContextMacroOrScriptDef(parentElement);
final AntDomMacroDef restrictToMacroDef = contextMacroOrScriptDef != null? contextMacroOrScriptDef.getFirst() : null;
final AntDomScriptDef restrictToScriptDef = contextMacroOrScriptDef != null? contextMacroOrScriptDef.getSecond() : null;
final boolean parentIsDataType = parentElement.isDataType();
for (final XmlName xmlName : myCustomElements.keySet()) {
final AntDomNamedElement declaringElement = myDeclarations.get(xmlName);
if (declaringElement instanceof AntDomMacrodefElement) {
if (restrictToMacroDef == null || !restrictToMacroDef.equals(declaringElement.getParentOfType(AntDomMacroDef.class, true))) {
continue;
}
}
else if (declaringElement instanceof AntDomScriptdefElement) {
if (restrictToScriptDef == null || !restrictToScriptDef.equals(declaringElement.getParentOfType(AntDomScriptDef.class, true))) {
continue;
}
}
if (declaringElement != null) {
if (declaringElement.equals(restrictToMacroDef) || declaringElement.equals(restrictToScriptDef)) {
continue;
}
}
if (parentIsDataType) {
if (declaringElement instanceof AntDomMacroDef || declaringElement instanceof AntDomScriptDef || declaringElement instanceof AntDomTaskdef) {
continue;
}
if (declaringElement instanceof AntDomTypeDef) {
final AntDomTypeDef typedef = (AntDomTypeDef)declaringElement;
final Class clazz = lookupClass(xmlName);
if (clazz != null && typedef.isTask(clazz)) {
continue;
}
}
}
result.add(xmlName);
}
return result;
}
@Nullable
private Pair<AntDomMacroDef, AntDomScriptDef> getContextMacroOrScriptDef(AntDomElement element) {
final AntDomMacroDef macrodef = element.getParentOfType(AntDomMacroDef.class, false);
if (macrodef != null) {
return new Pair<AntDomMacroDef, AntDomScriptDef>(macrodef, null);
}
for (AntDomCustomElement custom = element.getParentOfType(AntDomCustomElement.class, false); custom != null; custom = custom.getParentOfType(AntDomCustomElement.class, true)) {
final AntDomNamedElement declaring = getDeclaringElement(custom.getXmlName());
if (declaring instanceof AntDomMacroDef) {
return new Pair<AntDomMacroDef, AntDomScriptDef>((AntDomMacroDef)declaring, null);
}
else if (declaring instanceof AntDomScriptDef) {
return new Pair<AntDomMacroDef, AntDomScriptDef>(null, (AntDomScriptDef)declaring);
}
}
return null;
}
@Nullable
public AntDomElement findDeclaringElement(final AntDomElement parentElement, final XmlName customElementName) {
final AntDomElement declaration = myDeclarations.get(customElementName);
if (declaration == null) {
return null;
}
if (declaration instanceof AntDomMacrodefElement) {
final Pair<AntDomMacroDef, AntDomScriptDef> contextMacroOrScriptDef = getContextMacroOrScriptDef(parentElement);
final AntDomMacroDef macrodefUsed = contextMacroOrScriptDef != null? contextMacroOrScriptDef.getFirst() : null;
if (macrodefUsed == null || !macrodefUsed.equals(declaration.getParentOfType(AntDomMacroDef.class, true))) {
return null;
}
}
else if (declaration instanceof AntDomScriptdefElement) {
final Pair<AntDomMacroDef, AntDomScriptDef> contextMacroOrScriptDef = getContextMacroOrScriptDef(parentElement);
final AntDomScriptDef scriptDefUsed = contextMacroOrScriptDef != null? contextMacroOrScriptDef.getSecond() : null;
if (scriptDefUsed == null || !scriptDefUsed.equals(declaration.getParentOfType(AntDomScriptDef.class, true))) {
return null;
}
}
return declaration;
}
public AntDomNamedElement getDeclaringElement(XmlName customElementName) {
return myDeclarations.get(customElementName);
}
@Nullable
public Class lookupClass(XmlName xmlName) {
final ClassProvider provider = myCustomElements.get(xmlName);
return provider == null ? null : provider.lookupClass();
}
@Nullable
public String lookupError(XmlName xmlName) {
final ClassProvider provider = myCustomElements.get(xmlName);
return provider == null ? null : provider.getError();
}
public boolean hasTypeLoadingErrors(AntDomTypeDef typedef) {
final String generalError = myTypeDefErrors.get(typedef);
if (generalError != null) {
return true;
}
for (Map.Entry<XmlName, AntDomNamedElement> entry : myDeclarations.entrySet()) {
if (typedef.equals(entry.getValue())) {
if (lookupError(entry.getKey()) != null) {
return true;
}
}
}
return false;
}
public List<String> getTypeLoadingErrors(AntDomTypeDef typedef) {
final String generalError = myTypeDefErrors.get(typedef);
if (generalError != null) {
return Collections.singletonList(generalError);
}
List<String> errors = null;
for (Map.Entry<XmlName, AntDomNamedElement> entry : myDeclarations.entrySet()) {
if (typedef.equals(entry.getValue())) {
final String err = lookupError(entry.getKey());
if (err != null) {
if (errors == null) {
errors = new ArrayList<String>();
}
errors.add(err);
}
}
}
return errors == null? Collections.<String>emptyList() : errors;
}
private void rememberNamedClassLoader(AntDomCustomClasspathComponent typedef, AntDomProject antProject) {
final String loaderRef = typedef.getLoaderRef().getStringValue();
if (loaderRef != null) {
if (!myNamedLoaders.containsKey(loaderRef)) {
myNamedLoaders.put(loaderRef, createClassLoader(collectUrls(typedef), antProject));
}
}
}
@NotNull
private ClassLoader getClassLoader(AntDomCustomClasspathComponent customComponent, AntDomProject antProject) {
final String loaderRef = customComponent.getLoaderRef().getStringValue();
if (loaderRef != null) {
final ClassLoader loader = myNamedLoaders.get(loaderRef);
if (loader != null) {
return loader;
}
}
return createClassLoader(collectUrls(customComponent), antProject);
}
@Nullable
public static PsiFile loadContentAsFile(PsiFile originalFile, LanguageFileType fileType) {
final VirtualFile vFile = originalFile.getVirtualFile();
if (vFile == null) {
return null;
}
try {
return loadContentAsFile(originalFile.getProject(), vFile.getInputStream(), fileType);
}
catch (IOException e) {
LOG.info(e);
}
return null;
}
public static PsiFile loadContentAsFile(Project project, InputStream stream, LanguageFileType fileType) throws IOException {
final StringBuilder builder = new StringBuilder();
try {
int nextByte;
while ((nextByte = stream.read()) >= 0) {
builder.append((char)nextByte);
}
}
finally {
stream.close();
}
final PsiFileFactory factory = PsiFileFactory.getInstance(project);
return factory.createFileFromText("_ant_dummy__." + fileType.getDefaultExtension(), fileType, builder, LocalTimeCounter.currentTime(), false, false);
}
private void addCustomDefinition(@NotNull AntDomNamedElement declaringTag, String customTagName, String nsUri, ClassProvider classProvider) {
final XmlName xmlName = new XmlName(customTagName, nsUri == null? "" : nsUri);
myCustomElements.put(xmlName, classProvider);
myDeclarations.put(xmlName, declaringTag);
}
private static PsiFile createDummyFile(@NonNls final String name, final LanguageFileType type, final CharSequence str, Project project) {
return PsiFileFactory.getInstance(project).createFileFromText(name, type, str, LocalTimeCounter.currentTime(), false, false);
}
private static boolean isXmlFormat(AntDomTypeDef typedef, @NotNull final String resourceOrFileName) {
final String format = typedef.getFormat().getStringValue();
if (format != null) {
return "xml".equalsIgnoreCase(format);
}
return StringUtil.endsWithIgnoreCase(resourceOrFileName, ".xml");
}
@NotNull
public static ClassLoader createClassLoader(final List<URL> urls, final AntDomProject antProject) {
final ClassLoader parentLoader = antProject.getClassLoader();
if (urls.size() == 0) {
return parentLoader;
}
return new AntResourcesClassLoader(urls, parentLoader, false, false);
}
public static List<URL> collectUrls(AntDomClasspathElement typedef) {
boolean cleanupNeeded = false;
if (!ourIsBuildingClasspathForCustomTagLoading.get()) {
ourIsBuildingClasspathForCustomTagLoading.set(Boolean.TRUE);
cleanupNeeded = true;
}
try {
final List<URL> urls = new ArrayList<URL>();
// check classpath attribute
final List<File> cpFiles = typedef.getClasspath().getValue();
if (cpFiles != null) {
for (File file : cpFiles) {
try {
urls.add(toLocalURL(file));
}
catch (MalformedURLException ignored) {
LOG.info(ignored);
}
}
}
final HashSet<AntFilesProvider> processed = new HashSet<AntFilesProvider>();
final AntDomElement referencedPath = typedef.getClasspathRef().getValue();
if (referencedPath instanceof AntFilesProvider) {
for (File cpFile : ((AntFilesProvider)referencedPath).getFiles(processed)) {
try {
urls.add(toLocalURL(cpFile));
}
catch (MalformedURLException ignored) {
LOG.info(ignored);
}
}
}
// check nested elements
for (final Iterator<AntDomElement> it = typedef.getAntChildrenIterator(); it.hasNext();) {
AntDomElement child = it.next();
if (child instanceof AntFilesProvider) {
for (File cpFile : ((AntFilesProvider)child).getFiles(processed)) {
try {
urls.add(toLocalURL(cpFile));
}
catch (MalformedURLException ignored) {
LOG.info(ignored);
}
}
}
}
return urls;
}
finally {
if (cleanupNeeded) {
ourIsBuildingClasspathForCustomTagLoading.remove();
}
}
}
private static URL toLocalURL(final File file) throws MalformedURLException {
return file.toURI().toURL();
}
private class CustomTagDefinitionFinder extends AntDomRecursiveVisitor {
private final Set<AntDomElement> myElementsOnThePath = new HashSet<AntDomElement>();
private final Set<String> processedAntlibs = new HashSet<String>();
private final AntDomProject myAntProject;
public CustomTagDefinitionFinder(AntDomProject antProject) {
myAntProject = antProject;
}
public void visitAntDomElement(AntDomElement element) {
if (element instanceof AntDomCustomElement || myElementsOnThePath.contains(element)) {
return; // avoid stack overflow
}
myElementsOnThePath.add(element);
try {
final XmlTag tag = element.getXmlTag();
if (tag != null) {
final String[] uris = tag.knownNamespaces();
for (String uri : uris) {
if (!processedAntlibs.contains(uri)) {
processedAntlibs.add(uri);
final String antLibResource = AntDomAntlib.toAntlibResource(uri);
if (antLibResource != null) {
final XmlElement xmlElement = element.getXmlElement();
if (xmlElement != null) {
final ClassLoader loader = myAntProject.getClassLoader();
final InputStream stream = loader.getResourceAsStream(antLibResource);
if (stream != null) {
try {
final XmlFile xmlFile = (XmlFile)loadContentAsFile(xmlElement.getProject(), stream, StdFileTypes.XML);
if (xmlFile != null) {
loadDefinitionsFromAntlib(xmlFile, uri, loader, null, myAntProject);
}
}
catch (IOException e) {
LOG.info(e);
}
}
}
}
}
}
}
super.visitAntDomElement(element);
}
finally {
myElementsOnThePath.remove(element);
}
}
public void visitMacroDef(AntDomMacroDef macrodef) {
final String customTagName = macrodef.getName().getStringValue();
if (customTagName != null) {
final String nsUri = macrodef.getUri().getStringValue();
addCustomDefinition(macrodef, customTagName, nsUri, ClassProvider.EMPTY);
for (AntDomMacrodefElement element : macrodef.getMacroElements()) {
final String customSubTagName = element.getName().getStringValue();
if (customSubTagName != null) {
addCustomDefinition(element, customSubTagName, nsUri, ClassProvider.EMPTY);
}
}
}
}
public void visitScriptDef(AntDomScriptDef scriptdef) {
final String customTagName = scriptdef.getName().getStringValue();
if (customTagName != null) {
final String nsUri = scriptdef.getUri().getStringValue();
final ClassLoader classLoader = getClassLoader(scriptdef, myAntProject);
// register the scriptdef
addCustomDefinition(scriptdef, customTagName, nsUri, ClassProvider.EMPTY);
// registering nested elements
ReflectedProject reflectedProject = null;
for (AntDomScriptdefElement element : scriptdef.getScriptdefElements()) {
final String customSubTagName = element.getName().getStringValue();
if (customSubTagName != null) {
final String classname = element.getClassname().getStringValue();
if (classname != null) {
addCustomDefinition(element, customTagName, nsUri, ClassProvider.create(classname, classLoader));
}
else {
Class clazz = null;
final String typeName = element.getElementType().getStringValue();
if (typeName != null) {
clazz = lookupClass(new XmlName(typeName));
if (clazz == null) {
if (reflectedProject == null) { // lazy init
reflectedProject = ReflectedProject.getProject(myAntProject.getClassLoader());
}
final Hashtable<String, Class> coreTasks = reflectedProject.getTaskDefinitions();
if (coreTasks != null) {
clazz = coreTasks.get(typeName);
}
if (clazz == null) {
final Hashtable<String, Class> coreTypes = reflectedProject.getDataTypeDefinitions();
if (coreTypes != null) {
clazz = coreTypes.get(typeName);
}
}
}
}
addCustomDefinition(element, customSubTagName, nsUri, ClassProvider.create(clazz));
}
}
}
}
}
public void visitPresetDef(AntDomPresetDef presetdef) {
final String customTagName = presetdef.getName().getStringValue();
if (customTagName != null) {
final String nsUri = presetdef.getUri().getStringValue();
addCustomDefinition(presetdef, customTagName, nsUri, ClassProvider.EMPTY);
}
}
public void visitTypeDef(AntDomTypeDef typedef) {
// if loaderRef attribute is specified, make sure the loader is built and stored
rememberNamedClassLoader(typedef, myAntProject);
defineCustomElements(typedef, myAntProject);
}
public void visitInclude(AntDomInclude includeTag) {
processInclude(includeTag);
}
public void visitImport(AntDomImport importTag) {
processInclude(importTag);
}
private void processInclude(AntDomIncludingDirective directive) {
final PsiFileSystemItem item = directive.getFile().getValue();
if (item instanceof PsiFile) {
final AntDomProject slaveProject = AntSupport.getAntDomProject((PsiFile)item);
if (slaveProject != null) {
slaveProject.accept(this);
}
}
}
private void defineCustomElements(AntDomTypeDef typedef, final AntDomProject antProject) {
final String uri = typedef.getUri().getStringValue();
final String customTagName = typedef.getName().getStringValue();
final String classname = typedef.getClassName().getStringValue();
if (classname != null && customTagName != null) {
addCustomDefinition(typedef, customTagName, uri, ClassProvider.create(classname, getClassLoader(typedef, antProject)));
}
else {
defineCustomElementsFromResources(typedef, uri, antProject, null);
}
}
private void defineCustomElementsFromResources(AntDomTypeDef typedef, final String uri, AntDomProject antProject, ClassLoader loader) {
final XmlElement xmlElement = antProject.getXmlElement();
final Project project = xmlElement != null? xmlElement.getProject() : null;
if (project == null) {
return;
}
XmlFile xmlFile = null;
PropertiesFile propFile = null;
final String resource = typedef.getResource().getStringValue();
if (resource != null) {
if (loader == null) {
loader = getClassLoader(typedef, antProject);
}
final InputStream stream = loader.getResourceAsStream(resource);
if (stream != null) {
try {
if (isXmlFormat(typedef, resource)) {
xmlFile = (XmlFile)loadContentAsFile(project, stream, StdFileTypes.XML);
}
else {
propFile = (PropertiesFile)loadContentAsFile(project, stream, StdFileTypes.PROPERTIES);
}
}
catch (IOException e) {
LOG.info(e);
}
}
else {
myTypeDefErrors.put(typedef, "Resource \"" + resource + "\" not found in the classpath");
}
}
else {
final PsiFileSystemItem file = typedef.getFile().getValue();
if (file instanceof PsiFile) {
if (isXmlFormat(typedef, file.getName())) {
xmlFile = file instanceof XmlFile ? (XmlFile)file : (XmlFile)loadContentAsFile((PsiFile)file, StdFileTypes.XML);
}
else { // assume properties format
propFile = file instanceof PropertiesFile ? (PropertiesFile)file : (PropertiesFile)loadContentAsFile((PsiFile)file, StdFileTypes.PROPERTIES);
}
}
}
if (propFile != null) {
if (loader == null) { // if not initialized yet
loader = getClassLoader(typedef, antProject);
}
for (final IProperty property : propFile.getProperties()) {
addCustomDefinition(typedef, property.getUnescapedKey(), uri, ClassProvider.create(property.getUnescapedValue(), loader));
}
}
if (xmlFile != null) {
if (loader == null) { // if not initialized yet
loader = getClassLoader(typedef, antProject);
}
loadDefinitionsFromAntlib(xmlFile, uri, loader, typedef, antProject);
}
}
private void loadDefinitionsFromAntlib(XmlFile xmlFile, String uri, ClassLoader loader, @Nullable AntDomTypeDef typedef, AntDomProject antProject) {
final AntDomAntlib antLib = AntSupport.getAntLib(xmlFile);
if (antLib != null) {
final List<AntDomTypeDef> defs = new ArrayList<AntDomTypeDef>();
defs.addAll(antLib.getTaskdefs());
defs.addAll(antLib.getTypedefs());
if (!defs.isEmpty()) {
for (AntDomTypeDef def : defs) {
final String tagName = def.getName().getStringValue();
final String className = def.getClassName().getStringValue();
if (tagName != null && className != null) {
AntDomNamedElement declaringElement = typedef != null? typedef : def;
addCustomDefinition(declaringElement, tagName, uri, ClassProvider.create(className, loader));
}
else {
defineCustomElementsFromResources(def, uri, antProject, loader);
}
}
}
}
}
}
}