blob: 13cfa9e13d2a8a9b32666e2bcec553b80d9aba07 [file] [log] [blame]
/*
* Copyright 2000-2014 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.javaee;
import com.intellij.application.options.PathMacrosImpl;
import com.intellij.application.options.ReplacePathToMacroMap;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.*;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.AtomicNotNullLazyValue;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.NotNullLazyKey;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import com.intellij.psi.xml.XmlFile;
import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.HashMap;
import com.intellij.xml.Html5SchemaProvider;
import com.intellij.xml.XmlSchemaProvider;
import com.intellij.xml.util.XmlUtil;
import org.jdom.Element;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import java.io.File;
import java.net.URL;
import java.util.*;
@State(name = "ExternalResourceManagerImpl",
storages = {@Storage(file = StoragePathMacros.APP_CONFIG + "/other.xml")})
public class ExternalResourceManagerExImpl extends ExternalResourceManagerEx implements PersistentStateComponent<Element> {
static final Logger LOG = Logger.getInstance(ExternalResourceManagerExImpl.class);
@NonNls public static final String J2EE_1_3 = "http://java.sun.com/dtd/";
@NonNls public static final String J2EE_1_2 = "http://java.sun.com/j2ee/dtds/";
@NonNls public static final String J2EE_NS = "http://java.sun.com/xml/ns/j2ee/";
@NonNls public static final String JAVAEE_NS = "http://java.sun.com/xml/ns/javaee/";
private static final String CATALOG_PROPERTIES_ELEMENT = "CATALOG_PROPERTIES";
private final Map<String, Map<String, String>> myResources = new HashMap<String, Map<String, String>>();
private final Set<String> myResourceLocations = new HashSet<String>();
private final Set<String> myIgnoredResources = new HashSet<String>();
private final AtomicNotNullLazyValue<Map<String, Map<String, Resource>>> myStdResources = new AtomicNotNullLazyValue<Map<String, Map<String, Resource>>>() {
@NotNull
@Override
protected Map<String, Map<String, Resource>> compute() {
return computeStdResources();
}
};
private String myDefaultHtmlDoctype = HTML5_DOCTYPE_ELEMENT;
private String myCatalogPropertiesFile;
private XMLCatalogManager myCatalogManager;
private static final String HTML5_DOCTYPE_ELEMENT = "HTML5";
protected Map<String, Map<String, Resource>> computeStdResources() {
ResourceRegistrarImpl registrar = new ResourceRegistrarImpl();
for (StandardResourceProvider provider : Extensions.getExtensions(StandardResourceProvider.EP_NAME)) {
provider.registerResources(registrar);
}
StandardResourceEP[] extensions = Extensions.getExtensions(StandardResourceEP.EP_NAME);
for (StandardResourceEP extension : extensions) {
registrar.addStdResource(extension.url, extension.version, extension.resourcePath, null, extension.getLoaderForClass());
}
myIgnoredResources.addAll(registrar.getIgnored());
return registrar.getResources();
}
private final List<ExternalResourceListener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList();
@NonNls private static final String RESOURCE_ELEMENT = "resource";
@NonNls private static final String URL_ATTR = "url";
@NonNls private static final String LOCATION_ATTR = "location";
@NonNls private static final String IGNORED_RESOURCE_ELEMENT = "ignored-resource";
@NonNls private static final String HTML_DEFAULT_DOCTYPE_ELEMENT = "default-html-doctype";
private static final String DEFAULT_VERSION = null;
@Override
public boolean isStandardResource(VirtualFile file) {
VirtualFile parent = file.getParent();
return parent != null && parent.getName().equals("standardSchemas");
}
@Override
public boolean isUserResource(VirtualFile file) {
return myResourceLocations.contains(file.getUrl());
}
@Nullable
static <T> Map<String, T> getMap(@NotNull final Map<String, Map<String, T>> resources,
@Nullable final String version,
final boolean create) {
Map<String, T> map = resources.get(version);
if (map == null) {
if (create) {
map = ContainerUtil.newHashMap();
resources.put(version, map);
}
else if (version == null || !version.equals(DEFAULT_VERSION)) {
map = resources.get(DEFAULT_VERSION);
}
}
return map;
}
@Override
public String getResourceLocation(String url) {
return getResourceLocation(url, DEFAULT_VERSION);
}
@Override
public String getResourceLocation(@NonNls String url, String version) {
String result = getUserResource(url, version);
if (result == null) {
XMLCatalogManager manager = getCatalogManager();
if (manager != null) {
result = manager.resolve(url);
}
}
if (result == null) {
result = getStdResource(url, version);
}
if (result == null) {
result = url;
}
return result;
}
@Override
@Nullable
public String getUserResource(Project project, String url, String version) {
String resource = getProjectResources(project).getUserResource(url, version);
return resource == null ? getUserResource(url, version) : resource;
}
@Override
@Nullable
public String getStdResource(String url, String version) {
Map<String, Resource> map = getMap(myStdResources.getValue(), version, false);
if (map != null) {
Resource resource = map.get(url);
return resource == null ? null : resource.getResourceUrl();
}
else {
return null;
}
}
@Nullable
private String getUserResource(String url, String version) {
Map<String, String> map = getMap(myResources, version, false);
return map != null ? map.get(url) : null;
}
@Override
public String getResourceLocation(@NonNls String url, @NotNull Project project) {
String location = getProjectResources(project).getResourceLocation(url);
return location == null || location.equals(url) ? getResourceLocation(url) : location;
}
public String getResourceLocation(@NonNls String url, String version, @NotNull Project project) {
String location = getProjectResources(project).getResourceLocation(url, version);
return location == null || location.equals(url) ? getResourceLocation(url, version) : location;
}
@Override
@Nullable
public PsiFile getResourceLocation(@NotNull @NonNls final String url, @NotNull final PsiFile baseFile, final String version) {
final XmlFile schema = XmlSchemaProvider.findSchema(url, baseFile);
if (schema != null) {
return schema;
}
final String location = getResourceLocation(url, version, baseFile.getProject());
return XmlUtil.findXmlFile(baseFile, location);
}
@Override
public String[] getResourceUrls(FileType fileType, final boolean includeStandard) {
return getResourceUrls(fileType, DEFAULT_VERSION, includeStandard);
}
@Override
public String[] getResourceUrls(@Nullable final FileType fileType, @NonNls final String version, final boolean includeStandard) {
final List<String> result = new LinkedList<String>();
addResourcesFromMap(result, version, myResources);
if (includeStandard) {
addResourcesFromMap(result, version, myStdResources.getValue());
}
return ArrayUtil.toStringArray(result);
}
private static <T> void addResourcesFromMap(final List<String> result,
String version,
Map<String, Map<String, T>> resourcesMap) {
Map<String, T> resources = getMap(resourcesMap, version, false);
if (resources == null) return;
result.addAll(resources.keySet());
}
@TestOnly
public static void addTestResource(final String url, final String location, Disposable parentDisposable) {
final ExternalResourceManagerExImpl instance = (ExternalResourceManagerExImpl)getInstance();
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
instance.addResource(url, location);
}
});
Disposer.register(parentDisposable, new Disposable() {
@Override
public void dispose() {
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
instance.removeResource(url);
}
});
}
});
}
@Override
public void addResource(String url, String location) {
addResource(url, DEFAULT_VERSION, location);
}
@Override
public void addResource(@NonNls String url, @NonNls String version, @NonNls String location) {
ApplicationManager.getApplication().assertWriteAccessAllowed();
addSilently(url, version, location);
fireExternalResourceChanged();
}
private void addSilently(String url, String version, String location) {
final Map<String, String> map = getMap(myResources, version, true);
assert map != null;
map.put(url, location);
myResourceLocations.add(location);
incModificationCount();
}
@Override
public void removeResource(String url) {
removeResource(url, DEFAULT_VERSION);
}
@Override
public void removeResource(String url, String version) {
ApplicationManager.getApplication().assertWriteAccessAllowed();
Map<String, String> map = getMap(myResources, version, false);
if (map != null) {
String location = map.remove(url);
if (location != null) {
myResourceLocations.remove(location);
}
incModificationCount();
fireExternalResourceChanged();
}
}
@Override
public void removeResource(String url, @NotNull Project project) {
getProjectResources(project).removeResource(url);
}
@Override
public void addResource(@NonNls String url, @NonNls String location, @NotNull Project project) {
getProjectResources(project).addResource(url, location);
}
@Override
public String[] getAvailableUrls() {
Set<String> urls = new HashSet<String>();
for (Map<String, String> map : myResources.values()) {
urls.addAll(map.keySet());
}
return ArrayUtil.toStringArray(urls);
}
@Override
public String[] getAvailableUrls(Project project) {
return getProjectResources(project).getAvailableUrls();
}
@Override
public void clearAllResources() {
myResources.clear();
myIgnoredResources.clear();
}
@Override
public void clearAllResources(Project project) {
ApplicationManager.getApplication().assertWriteAccessAllowed();
clearAllResources();
getProjectResources(project).clearAllResources();
incModificationCount();
fireExternalResourceChanged();
}
@Override
public void addIgnoredResource(String url) {
ApplicationManager.getApplication().assertWriteAccessAllowed();
addIgnoredSilently(url);
fireExternalResourceChanged();
}
private void addIgnoredSilently(String url) {
myIgnoredResources.add(url);
incModificationCount();
}
@Override
public void removeIgnoredResource(String url) {
ApplicationManager.getApplication().assertWriteAccessAllowed();
if (myIgnoredResources.remove(url)) {
incModificationCount();
fireExternalResourceChanged();
}
}
@Override
public boolean isIgnoredResource(String url) {
myStdResources.getValue(); // ensure ignored resources are loaded
return myIgnoredResources.contains(url) || isImplicitNamespaceDescriptor(url);
}
private static boolean isImplicitNamespaceDescriptor(String url) {
for (ImplicitNamespaceDescriptorProvider namespaceDescriptorProvider : Extensions
.getExtensions(ImplicitNamespaceDescriptorProvider.EP_NAME)) {
if (namespaceDescriptorProvider.getNamespaceDescriptor(null, url, null) != null) return true;
}
return false;
}
@Override
public String[] getIgnoredResources() {
myStdResources.getValue(); // ensure ignored resources are loaded
return ArrayUtil.toStringArray(myIgnoredResources);
}
@Override
public long getModificationCount(@NotNull Project project) {
return getProjectResources(project).getModificationCount();
}
@Nullable
@Override
public Element getState() {
Element element = new Element("state");
final String[] urls = getAvailableUrls();
for (String url : urls) {
if (url == null) continue;
String location = getResourceLocation(url);
if (location == null) continue;
final Element e = new Element(RESOURCE_ELEMENT);
e.setAttribute(URL_ATTR, url);
e.setAttribute(LOCATION_ATTR, location.replace(File.separatorChar, '/'));
element.addContent(e);
}
final String[] ignoredResources = getIgnoredResources();
for (String ignoredResource : ignoredResources) {
final Element e = new Element(IGNORED_RESOURCE_ELEMENT);
e.setAttribute(URL_ATTR, ignoredResource);
element.addContent(e);
}
if (myDefaultHtmlDoctype != null && !HTML5_DOCTYPE_ELEMENT.equals(myDefaultHtmlDoctype)) {
final Element e = new Element(HTML_DEFAULT_DOCTYPE_ELEMENT);
e.setText(myDefaultHtmlDoctype);
element.addContent(e);
}
if (myCatalogPropertiesFile != null) {
Element properties = new Element(CATALOG_PROPERTIES_ELEMENT);
properties.setText(myCatalogPropertiesFile);
element.addContent(properties);
}
final ReplacePathToMacroMap macroReplacements = new ReplacePathToMacroMap();
PathMacrosImpl.getInstanceEx().addMacroReplacements(macroReplacements);
macroReplacements.substitute(element, SystemInfo.isFileSystemCaseSensitive);
return element;
}
@Override
public void loadState(Element element) {
final ExpandMacroToPathMap macroExpands = new ExpandMacroToPathMap();
PathMacrosImpl.getInstanceEx().addMacroExpands(macroExpands);
macroExpands.substitute(element, SystemInfo.isFileSystemCaseSensitive);
incModificationCount();
for (final Object o1 : element.getChildren(RESOURCE_ELEMENT)) {
Element e = (Element)o1;
addSilently(e.getAttributeValue(URL_ATTR), DEFAULT_VERSION, e.getAttributeValue(LOCATION_ATTR).replace('/', File.separatorChar));
}
for (final Object o : element.getChildren(IGNORED_RESOURCE_ELEMENT)) {
Element e = (Element)o;
addIgnoredSilently(e.getAttributeValue(URL_ATTR));
}
Element child = element.getChild(HTML_DEFAULT_DOCTYPE_ELEMENT);
if (child != null) {
String text = child.getText();
if (FileUtil.toSystemIndependentName(text).endsWith(".jar!/resources/html5-schema/html5.rnc")) {
text = HTML5_DOCTYPE_ELEMENT;
}
myDefaultHtmlDoctype = text;
}
Element catalogElement = element.getChild(CATALOG_PROPERTIES_ELEMENT);
if (catalogElement != null) {
myCatalogPropertiesFile = catalogElement.getTextTrim();
}
}
@Override
public void addExternalResourceListener(ExternalResourceListener listener) {
myListeners.add(listener);
}
@Override
public void removeExternalResourceListener(ExternalResourceListener listener) {
myListeners.remove(listener);
}
private void fireExternalResourceChanged() {
for (ExternalResourceListener listener : myListeners) {
listener.externalResourceChanged();
}
}
Collection<Map<String, Resource>> getStandardResources() {
return myStdResources.getValue().values();
}
private static final NotNullLazyKey<ExternalResourceManagerExImpl, Project> INSTANCE_CACHE = ServiceManager.createLazyKey(ExternalResourceManagerExImpl.class);
private static ExternalResourceManagerExImpl getProjectResources(Project project) {
return INSTANCE_CACHE.getValue(project);
}
@Override
@NotNull
public String getDefaultHtmlDoctype(@NotNull Project project) {
final String doctype = getProjectResources(project).myDefaultHtmlDoctype;
if (XmlUtil.XHTML_URI.equals(doctype)) {
return XmlUtil.XHTML4_SCHEMA_LOCATION;
}
else if (HTML5_DOCTYPE_ELEMENT.equals(doctype)) {
return Html5SchemaProvider.getHtml5SchemaLocation();
}
else {
return doctype;
}
}
@Override
public void setDefaultHtmlDoctype(@NotNull String defaultHtmlDoctype, @NotNull Project project) {
getProjectResources(project).setDefaultHtmlDoctype(defaultHtmlDoctype);
}
@Override
public String getCatalogPropertiesFile() {
return myCatalogPropertiesFile;
}
@Override
public void setCatalogPropertiesFile(String filePath) {
myCatalogManager = null;
myCatalogPropertiesFile = filePath;
incModificationCount();
}
@Nullable
private XMLCatalogManager getCatalogManager() {
if (myCatalogManager == null && myCatalogPropertiesFile != null) {
myCatalogManager = new XMLCatalogManager(myCatalogPropertiesFile);
}
return myCatalogManager;
}
private void setDefaultHtmlDoctype(String defaultHtmlDoctype) {
incModificationCount();
if (Html5SchemaProvider.getHtml5SchemaLocation().equals(defaultHtmlDoctype)) {
myDefaultHtmlDoctype = HTML5_DOCTYPE_ELEMENT;
}
else {
myDefaultHtmlDoctype = defaultHtmlDoctype;
}
fireExternalResourceChanged();
}
@TestOnly
public static void registerResourceTemporarily(final String url, final String location, Disposable disposable) {
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
getInstance().addResource(url, location);
}
});
Disposer.register(disposable, new Disposable() {
@Override
public void dispose() {
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
getInstance().removeResource(url);
}
});
}
});
}
static class Resource {
private final String myFile;
private final ClassLoader myClassLoader;
private final Class myClass;
private volatile String myResolvedResourcePath;
Resource(String _file, Class _class, ClassLoader _classLoader) {
myFile = _file;
myClass = _class;
myClassLoader = _classLoader;
}
Resource(String _file, Resource baseResource) {
this(_file, baseResource.myClass, baseResource.myClassLoader);
}
String directoryName() {
int i = myFile.lastIndexOf('/');
return i > 0 ? myFile.substring(0, i) : myFile;
}
@Nullable
String getResourceUrl() {
String resolvedResourcePath = myResolvedResourcePath;
if (resolvedResourcePath != null) return resolvedResourcePath;
final URL resource = myClass == null ? myClassLoader.getResource(myFile) : myClass.getResource(myFile);
if (resource == null) {
String message = "Cannot find standard resource. filename:" + myFile + " class=" + myClass + ", classLoader:" + myClassLoader;
if (ApplicationManager.getApplication().isUnitTestMode()) {
LOG.error(message);
}
else {
LOG.warn(message);
}
myResolvedResourcePath = null;
return null;
}
String path = FileUtil.unquote(resource.toString());
// this is done by FileUtil for windows
path = path.replace('\\','/');
myResolvedResourcePath = path;
return path;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Resource resource = (Resource)o;
if (myClassLoader != resource.myClassLoader) return false;
if (myClass != resource.myClass) return false;
if (myFile != null ? !myFile.equals(resource.myFile) : resource.myFile != null) return false;
return true;
}
@Override
public int hashCode() {
return myFile.hashCode();
}
@Override
public String toString() {
return myFile + " for " + myClassLoader;
}
}
}