package com.intellij.javascript.flex.mxml.schema;
import com.intellij.javascript.flex.mxml.MxmlJSClass;
import com.intellij.javascript.flex.resolve.FlexResolveHelper;
import com.intellij.lang.javascript.JavaScriptSupportLoader;
import com.intellij.lang.javascript.flex.FlexModuleType;
import com.intellij.lang.javascript.flex.XmlBackedJSClassImpl;
import com.intellij.lang.javascript.index.JSPackageIndex;
import com.intellij.lang.javascript.psi.ecmal4.JSClass;
import com.intellij.lang.javascript.psi.ecmal4.JSQualifiedNamedElement;
import com.intellij.lang.javascript.psi.resolve.JSResolveUtil;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleType;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.*;
import com.intellij.psi.xml.XmlDocument;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Processor;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.indexing.FileBasedIndex;
import com.intellij.xml.XmlElementDescriptor;
import com.intellij.xml.XmlSchemaProvider;
import gnu.trove.THashMap;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
/**
* @author Maxim.Mossienko
*/
public class FlexSchemaHandler extends XmlSchemaProvider implements DumbAware {
private static final Pattern prefixPattern = Pattern.compile("[a-z_][a-z_0-9]*");
@Nullable
public XmlFile getSchema(@NotNull @NonNls final String url, final Module module, @NotNull final PsiFile baseFile) {
return url.length() > 0 && JavaScriptSupportLoader.isFlexMxmFile(baseFile) ? getFakeSchemaReference(url, module) : null;
}
private static final Key<Map<String, ParameterizedCachedValue<XmlFile, Module>>> DESCRIPTORS_MAP_IN_MODULE =
Key.create("FLEX_DESCRIPTORS_MAP_IN_MODULE");
@Nullable
private static synchronized XmlFile getFakeSchemaReference(final String uri, @Nullable final Module module) {
if (module == null) {
return null;
}
if (ModuleType.get(module) == FlexModuleType.getInstance() || !CodeContext.isStdNamespace(uri)) {
Map<String, ParameterizedCachedValue<XmlFile, Module>> descriptors = module.getUserData(DESCRIPTORS_MAP_IN_MODULE);
if (descriptors == null) {
descriptors = new THashMap<>();
module.putUserData(DESCRIPTORS_MAP_IN_MODULE, descriptors);
}
ParameterizedCachedValue<XmlFile, Module> reference = descriptors.get(uri);
if (reference == null) {
reference = CachedValuesManager.getManager(module.getProject())
.createParameterizedCachedValue(new ParameterizedCachedValueProvider<XmlFile, Module>() {
@Override
public CachedValueProvider.Result<XmlFile> compute(Module module) {
final URL resource = FlexSchemaHandler.class.getResource("z.xsd");
final VirtualFile fileByURL = VfsUtil.findFileByURL(resource);
XmlFile result = (XmlFile)PsiManager.getInstance(module.getProject()).findFile(fileByURL).copy();
result.putUserData(FlexMxmlNSDescriptor.NS_KEY, uri);
result.putUserData(FlexMxmlNSDescriptor.MODULE_KEY, module);
return new CachedValueProvider.Result<>(result, PsiModificationTracker.MODIFICATION_COUNT);
}
}, false);
descriptors.put(uri, reference);
}
assert !module.getProject().isDisposed() : module.getProject() + " already disposed";
return reference.getValue(module);
}
return null;
}
@Override
public boolean isAvailable(final @NotNull XmlFile file) {
return JavaScriptSupportLoader.isFlexMxmFile(file);
}
@NotNull
@Override
public Set<String> getAvailableNamespaces(@NotNull XmlFile _file, @Nullable @NonNls final String tagName) {
// tagName == null => tag name completion
// tagName != null => guess namespace of unresolved tag
PsiFile originalFile = _file.getOriginalFile();
if (originalFile instanceof XmlFile) _file = (XmlFile)originalFile;
final XmlFile file = _file;
final Project project = file.getProject();
final Module module = ProjectRootManager.getInstance(project).getFileIndex().getModuleForFile(file.getVirtualFile());
final Collection<String> illegalNamespaces = getIllegalNamespaces(file);
final Set<String> result = new THashSet<>();
final Set<String> componentsThatHaveNotPackageBackedNamespace = new THashSet<>();
for (final String namespace : CodeContextHolder.getInstance(project).getNamespaces(module)) {
if (!CodeContext.isPackageBackedNamespace(namespace) && !illegalNamespaces.contains(namespace)) {
// package backed namespaces will be added later from JSPackageIndex
if (tagName == null) {
result.add(namespace);
}
else {
final XmlElementDescriptor descriptor = CodeContext.getContext(namespace, module).getElementDescriptor(tagName, (XmlTag)null);
if (descriptor != null) {
result.add(namespace);
componentsThatHaveNotPackageBackedNamespace.add(descriptor.getQualifiedName());
}
}
}
}
if (tagName == null && !illegalNamespaces.contains(JavaScriptSupportLoader.MXML_URI)) {
result.add(JavaScriptSupportLoader.MXML_URI);
}
if (XmlBackedJSClassImpl.SCRIPT_TAG_NAME.equals(tagName) || "Style".equals(tagName)) return result;
if (DumbService.isDumb(project)) return result;
if (tagName == null) {
FileBasedIndex.getInstance().processAllKeys(JSPackageIndex.INDEX_ID, packageName -> {
// packages that don't contain suitable classes will be filtered out
// in DefultXmlExtension.getAvailableTagNames -> TagNameReference.getTagNameVariants()
result.add(StringUtil.isEmpty(packageName) ? "*" : packageName + ".*");
return true;
}, project);
}
else {
final GlobalSearchScope scopeWithLibs = module != null
? GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(module)
: GlobalSearchScope.allScope(project);
for (JSQualifiedNamedElement element : JSResolveUtil.findElementsByName(tagName, project, scopeWithLibs, false)) {
if (element instanceof JSClass && CodeContext.hasDefaultConstructor((JSClass)element)) {
final String packageName = StringUtil.getPackageName(element.getQualifiedName());
if (!componentsThatHaveNotPackageBackedNamespace.contains(StringUtil.getQualifiedName(packageName, tagName))) {
result.add(StringUtil.isEmpty(packageName) ? "*" : packageName + ".*");
}
}
}
}
final GlobalSearchScope scopeWithoutLibs = module != null
? GlobalSearchScope.moduleWithDependenciesScope(module)
: GlobalSearchScope.allScope(project);
// packages that contain *.mxml files and do not contain *.as are not retrieved from JSPackageIndex
FlexResolveHelper
.processAllMxmlAndFxgFiles(scopeWithoutLibs, project,
new FlexResolveHelper.MxmlAndFxgFilesProcessor() {
public void addDependency(final PsiDirectory directory) {
}
public boolean processFile(final VirtualFile file, final VirtualFile root) {
if (tagName == null || tagName.equals(file.getNameWithoutExtension())) {
final String packageName = VfsUtilCore.getRelativePath(file.getParent(), root, '.');
if (packageName != null &&
(tagName == null || !componentsThatHaveNotPackageBackedNamespace
.contains(StringUtil.getQualifiedName(packageName, tagName)))) {
result.add(StringUtil.isEmpty(packageName) ? "*" : packageName + ".*");
}
}
return true;
}
},
tagName);
return result;
}
private static Collection<String> getIllegalNamespaces(final XmlFile file) {
final XmlDocument document = file.getDocument();
final XmlTag rootTag = document == null ? null : document.getRootTag();
final String[] knownNamespaces = rootTag == null ? null : rootTag.knownNamespaces();
final Collection<String> illegalNamespaces = new ArrayList<>();
if (knownNamespaces != null) {
if (ArrayUtil.contains(JavaScriptSupportLoader.MXML_URI, knownNamespaces)) {
ContainerUtil.addAll(illegalNamespaces, MxmlJSClass.FLEX_4_NAMESPACES);
}
else if (ArrayUtil.contains(JavaScriptSupportLoader.MXML_URI3, knownNamespaces)) {
illegalNamespaces.add(JavaScriptSupportLoader.MXML_URI);
}
}
return illegalNamespaces;
}
@Override
public String getDefaultPrefix(@NotNull @NonNls final String namespace, @NotNull final XmlFile context) {
return getUniquePrefix(namespace, context);
}
static String getUniquePrefix(final String namespace, final XmlFile xmlFile) {
String prefix = getDefaultPrefix(namespace);
final XmlDocument document = xmlFile.getDocument();
final XmlTag tag = document == null ? null : document.getRootTag();
final String[] knownPrefixes = getKnownPrefixes(tag);
if (ArrayUtil.contains(prefix, knownPrefixes)) {
for (int i = 2; ; i++) {
final String newPrefix = prefix + i;
if (!ArrayUtil.contains(newPrefix, knownPrefixes)) {
prefix = newPrefix;
break;
}
}
}
return prefix;
}
public static String getDefaultPrefix(@NotNull @NonNls String namespace) {
if (JavaScriptSupportLoader.MXML_URI.equals(namespace)) return "mx";
if (JavaScriptSupportLoader.MXML_URI3.equals(namespace)) return "fx";
if (MxmlJSClass.MXML_URI4.equals(namespace)) return "s";
if (MxmlJSClass.MXML_URI5.equals(namespace)) return "h";
if (MxmlJSClass.MXML_URI6.equals(namespace)) return "mx";
if ("*".equals(namespace)) return "local";
namespace = FileUtil.toSystemIndependentName(namespace.toLowerCase());
String prefix = namespace;
if (namespace.endsWith(".*") && namespace.length() > 2) {
final String pack = namespace.substring(0, namespace.length() - 2);
prefix = pack.substring(pack.lastIndexOf('.') + 1);
}
else {
final String schemaMarker = "://";
int schemaMarkerIndex = namespace.indexOf(schemaMarker);
if (schemaMarkerIndex > 0) {
String path = namespace.substring(schemaMarkerIndex + schemaMarker.length());
path = StringUtil.trimStart(path, "www.");
path = StringUtil.trimEnd(path, "/");
final String lastSegment = path.substring(path.lastIndexOf('/') + 1);
if (prefixPattern.matcher(lastSegment).matches()) {
return lastSegment;
}
final int dotIndex = path.indexOf('.');
final int slashIndex = path.indexOf('/');
final int endIndex = (dotIndex == -1)
? (slashIndex == -1 ? path.length() : slashIndex)
: (slashIndex == -1 ? dotIndex : Math.min(dotIndex, slashIndex));
if (path.length() > 0 && endIndex > 0) {
prefix = path.substring(0, endIndex);
}
}
}
if (prefix != null && prefixPattern.matcher(prefix).matches()) {
return prefix;
}
return "undefined";
}
private static String[] getKnownPrefixes(final XmlTag tag) {
final String[] namespaces = tag == null ? null : tag.knownNamespaces();
if (namespaces != null && namespaces.length > 0) {
final String[] knownPrefixes = new String[namespaces.length];
for (int i = 0; i < namespaces.length; i++) {
knownPrefixes[i] = tag.getPrefixByNamespace(namespaces[i]);
}
return knownPrefixes;
}
return ArrayUtil.EMPTY_STRING_ARRAY;
}
}