/*
* 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 org.napile.idea.thermit.dom;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.napile.idea.thermit.AntFilesProvider;
import org.napile.idea.thermit.ReflectedProject;
import org.napile.idea.thermit.ThermitSupport;
import org.napile.idea.thermit.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.io.FileUtil;
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;
/**
* Storage for user-defined tasks and data types
* parsed from thermit 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("#org.napile.idea.thermit.dom.CustomAntElementsRegistry");
private static final Key<CustomAntElementsRegistry> REGISTRY_KEY = Key.create("_custom_element_registry_");
private final Map<XmlName, Class> myCustomElements = new HashMap<XmlName, Class>();
private final Map<XmlName, String> myErrors = new HashMap<XmlName, String>();
private final Map<AntDomNamedElement, String> myTypeDefErrors = new HashMap<AntDomNamedElement, String>();
private final Map<XmlName, AntDomNamedElement> myDeclarations = new HashMap<XmlName, AntDomNamedElement>();
private final Map<String, ClassLoader> myNamedLoaders = new HashMap<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 = myCustomElements.get(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)
{
return myCustomElements.get(xmlName);
}
@Nullable
public String lookupError(XmlName xmlName)
{
return myErrors.get(xmlName);
}
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(myErrors.containsKey(entry.getKey()))
{
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 XmlName xmlName = entry.getKey();
if(myErrors.containsKey(xmlName))
{
final String err = myErrors.get(xmlName);
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));
}
}
}
@Nullable
private ClassLoader getClassLoader(AntDomCustomClasspathComponent customComponent, AntDomProject antProject)
{
final String loaderRef = customComponent.getLoaderRef().getStringValue();
if(loaderRef != null && myNamedLoaders.containsKey(loaderRef))
{
return myNamedLoaders.get(loaderRef);
}
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 registerElement(AntDomNamedElement declaringElement, String customTagName, String nsUri, String classname, ClassLoader loader)
{
Class clazz = null;
String error = null;
try
{
clazz = loader.loadClass(classname);
}
catch(ClassNotFoundException e)
{
error = "Class not found " + e.getMessage();
if(error == null)
{
error = "";
}
clazz = null;
}
catch(NoClassDefFoundError e)
{
error = "Class definition not found " + e.getMessage();
if(error == null)
{
error = "";
}
clazz = null;
}
catch(UnsupportedClassVersionError e)
{
error = "Unsupported class version " + e.getMessage();
if(error == null)
{
error = "";
}
clazz = null;
}
addCustomDefinition(declaringElement, customTagName, nsUri, clazz, error);
}
private void addCustomDefinition(@NotNull AntDomNamedElement declaringTag, String customTagName, String nsUri, Class clazz, String error)
{
final XmlName xmlName = new XmlName(customTagName, nsUri == null ? "" : nsUri);
if(error != null)
{
myErrors.put(xmlName, error);
}
myCustomElements.put(xmlName, clazz);
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");
}
@Nullable
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
{
String path = FileUtil.toSystemIndependentName(file.getPath());
if(!(StringUtil.endsWithIgnoreCase(path, ".jar") || StringUtil.endsWithIgnoreCase(path, ".zip")) && file.isDirectory())
{
if(!path.endsWith("/"))
{
path = path + "/";
}
}
return new URL("file", "", path);
}
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, null, null);
for(AntDomMacrodefElement element : macrodef.getMacroElements())
{
final String customSubTagName = element.getName().getStringValue();
if(customSubTagName != null)
{
addCustomDefinition(element, customSubTagName, nsUri, null, null);
}
}
}
}
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, null, null);
// 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)
{
registerElement(element, customTagName, nsUri, classname, classLoader);
}
else
{
Class clazz = null;
final String typeName = element.getElementType().getStringValue();
if(typeName != null)
{
clazz = myCustomElements.get(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, clazz, null);
}
}
}
}
}
public void visitPresetDef(AntDomPresetDef presetdef)
{
final String customTagName = presetdef.getName().getStringValue();
if(customTagName != null)
{
final String nsUri = presetdef.getUri().getStringValue();
addCustomDefinition(presetdef, customTagName, nsUri, null, null);
}
}
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 = ThermitSupport.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)
{
registerElement(typedef, customTagName, uri, 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);
}
if(loader != null)
{
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())
{
registerElement(typedef, property.getUnescapedKey(), uri, property.getValue(), 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 = ThermitSupport.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)
{
registerElement(typedef != null ? typedef : def, tagName, uri, className, loader);
}
else
{
defineCustomElementsFromResources(def, uri, antProject, loader);
}
}
}
}
}
}
}