package com.intellij.javascript.flex.resolve;
import com.intellij.lang.javascript.index.JavaScriptIndex;
import com.intellij.lang.javascript.psi.ecmal4.JSQualifiedNamedElement;
import com.intellij.openapi.util.JDOMUtil;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.impl.source.parsing.xml.XmlBuilder;
import com.intellij.psi.impl.source.parsing.xml.XmlBuilderDriver;
import com.intellij.util.Consumer;
import com.intellij.util.containers.Stack;
import com.intellij.util.containers.StringInterner;
import gnu.trove.THashMap;
import gnu.trove.TObjectLongHashMap;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
public class SwcCatalogXmlUtil {
/**
* <code><b>Pair.first</b></code> is modification stamp of <i>catalog.xml</i> file when this user data was put<br>
* <code><b>Pair.second</b></code> is <code>Map<String, TObjectLongHashMap<String>></code> where:<br>
* <code><b>key</b></code> is swf file name inside swc file (so far I have seen only <i>library.swf</i> name, but swc format allows any file name, it is mentioned in <i>catalog.xml</i>)<br>
* <code><b>value</b></code> is map from FQN (of JSQualifiedElement) to its timestamp as written inside <i>catalog.xml</i>.
*/
private static final Key<Pair<Long, THashMap<String, TObjectLongHashMap<String>>>> MOD_STAMP_AND_SWF_NAME_TO_QNAME_WITH_TIMESTAMP_MAP =
Key.create("MOD_STAMP_AND_SWF_NAME_TO_QNAME_WITH_TIMESTAMP_MAP");
private static final Key<Long> TIMESTAMP_IN_CATALOG_XML = Key.create("TIMESTAMP_IN_CATALOG_XML");
private static final Key<Pair<Long, ComponentFromCatalogXml[]>> MOD_STAMP_AND_COMPONENTS_FROM_CATALOG_XML =
Key.create("MOD_STAMP_AND_COMPONENTS_FROM_CATALOG_XML");
private static final Key<Pair<Long, ComponentFromManifest[]>> MOD_STAMP_AND_COMPONENTS_FROM_MANIFEST =
Key.create("MOD_STAMP_AND_COMPONENTS_FROM_MANIFEST");
private SwcCatalogXmlUtil() {
}
public static class ComponentFromCatalogXml {
public final @NotNull String myName;
public final @NotNull String myClassFqn;
public final @NotNull String myUri;
public final @Nullable String myIcon;
private ComponentFromCatalogXml(final @NotNull String name,
final @NotNull String classFqn,
final @NotNull String uri,
final @Nullable String icon) {
myName = name;
myClassFqn = classFqn;
myUri = uri.intern(); // memory optimization
myIcon = icon;
}
}
public static class ComponentFromManifest {
public final @NotNull String myComponentName;
public final @NotNull String myClassFqn;
private ComponentFromManifest(final @NotNull String componentName, final @NotNull String classFqn) {
myComponentName = componentName;
myClassFqn = classFqn;
}
}
public static class XmlBuilderAdapter implements XmlBuilder {
private final Stack<String> myLocation = new Stack<>();
public XmlBuilderAdapter() {
myLocation.push("");
}
public String getLocation() {
return myLocation.peek();
}
@Override
public void doctype(@Nullable final CharSequence publicId, @Nullable final CharSequence systemId, final int start, final int end) {
}
@Override
public ProcessingOrder startTag(final CharSequence localName,
final String namespace,
final int start,
final int end,
final int headerEnd) {
myLocation.push(myLocation.peek() + "." + localName);
return ProcessingOrder.TAGS_AND_ATTRIBUTES;
}
@Override
public void endTag(final CharSequence localName, final String namespace, final int start, final int end) {
myLocation.pop();
}
@Override
public void attribute(final CharSequence name, final CharSequence value, final int start, final int end) {
}
@Override
public void textElement(final CharSequence display, final CharSequence physical, final int start, final int end) {
}
@Override
public void entityRef(final CharSequence ref, final int start, final int end) {
}
@Override
public void error(final String message, final int start, final int end) {
}
}
public static long getTimestampFromCatalogXml(final @NotNull PsiElement psiElement) {
final Long cachedTimestamp = psiElement.getUserData(TIMESTAMP_IN_CATALOG_XML);
if (cachedTimestamp != null) {
return cachedTimestamp;
}
if (!(psiElement instanceof JSQualifiedNamedElement)) {
return -1;
}
final String qName = ((JSQualifiedNamedElement)psiElement).getQualifiedName();
if (StringUtil.isEmpty(qName)) {
return -1;
}
final PsiFile psiFile = psiElement.getContainingFile();
if (JavaScriptIndex.ECMASCRIPT_JS2.equals(psiFile.getName())) return Integer.MIN_VALUE;
final VirtualFile swfFile = psiFile.getVirtualFile();
final VirtualFile dir = swfFile != null && "swf".equalsIgnoreCase(swfFile.getExtension()) ? swfFile.getParent() : null;
final VirtualFile catalogFile = dir == null ? null : dir.findChild("catalog.xml");
if (catalogFile == null) {
return -1;
}
Pair<Long, THashMap<String, TObjectLongHashMap<String>>> modStampAndSwfNameToQnameWithTimestampMap =
catalogFile.getUserData(MOD_STAMP_AND_SWF_NAME_TO_QNAME_WITH_TIMESTAMP_MAP);
if (modStampAndSwfNameToQnameWithTimestampMap == null
|| modStampAndSwfNameToQnameWithTimestampMap.first != catalogFile.getModificationStamp()) {
final THashMap<String, TObjectLongHashMap<String>> swfNameToQnameWithTimestampMap = parseTimestampsFromCatalogXml(catalogFile);
modStampAndSwfNameToQnameWithTimestampMap = Pair.create(catalogFile.getModificationStamp(), swfNameToQnameWithTimestampMap);
catalogFile.putUserData(MOD_STAMP_AND_SWF_NAME_TO_QNAME_WITH_TIMESTAMP_MAP, modStampAndSwfNameToQnameWithTimestampMap);
}
final TObjectLongHashMap<String> qnameWithTimestampMap = modStampAndSwfNameToQnameWithTimestampMap.second.get(swfFile.getName());
final long timestamp = qnameWithTimestampMap == null ? -1 : qnameWithTimestampMap.get(qName);
psiElement.putUserData(TIMESTAMP_IN_CATALOG_XML, timestamp);
return timestamp;
}
@SuppressWarnings({"unchecked"})
private static THashMap<String, TObjectLongHashMap<String>> parseTimestampsFromCatalogXml(final @NotNull VirtualFile catalogFile) {
// <swc xmlns="http://www.adobe.com/flash/swccatalog/9">
// <libraries>
// <library path="library.swf"> take swf name here
// <script name="flash/sampler/StackFrame" mod="1256700285949" signatureChecksum="121164004" > name attribute is not FQN, take only mod here
// <def id="flash.sampler:Sample" /> multiple defs possible
// <def id="flash.sampler:clearSamples" />
// ...
final THashMap<String, TObjectLongHashMap<String>> swfNameToQnameWithTimestampMap = new THashMap<>(1);
try {
final Element rootElement = JDOMUtil.load(catalogFile.getInputStream());
if (rootElement != null && "swc".equals(rootElement.getName())) {
for (final Element librariesElement : rootElement.getChildren("libraries", rootElement.getNamespace())) {
for (final Element libraryElement : librariesElement.getChildren("library", librariesElement.getNamespace())) {
final String swfName = libraryElement.getAttributeValue("path");
if (StringUtil.isEmpty(swfName)) {
continue;
}
final TObjectLongHashMap<String> qNameWithTimestampMap = new TObjectLongHashMap<>();
swfNameToQnameWithTimestampMap.put(swfName, qNameWithTimestampMap);
for (final Element scriptElement : libraryElement.getChildren("script", libraryElement.getNamespace())) {
final String mod = scriptElement.getAttributeValue("mod");
if (StringUtil.isEmpty(mod)) {
continue;
}
try {
final long timestamp = Long.parseLong(mod);
for (final Element defElement : scriptElement.getChildren("def", scriptElement.getNamespace())) {
final String id = defElement.getAttributeValue("id");
if (!StringUtil.isEmpty(id)) {
final String fqn = id.replace(':', '.');
qNameWithTimestampMap.put(fqn, timestamp);
}
}
}
catch (NumberFormatException ignored) {/*ignore*/}
}
}
}
}
}
catch (JDOMException ignored) {/*ignore*/}
catch (IOException ignored) {/*ignore*/}
return swfNameToQnameWithTimestampMap;
}
public static void processComponentsFromCatalogXml(final VirtualFile catalogFile, final Consumer<ComponentFromCatalogXml> consumer) {
Pair<Long, ComponentFromCatalogXml[]> modStampAndComponents = catalogFile.getUserData(MOD_STAMP_AND_COMPONENTS_FROM_CATALOG_XML);
if (modStampAndComponents == null || modStampAndComponents.first != catalogFile.getModificationStamp()) {
final ComponentFromCatalogXml[] componentsFromCatalogXml = parseComponentsFromCatalogXml(catalogFile);
modStampAndComponents = Pair.create(catalogFile.getModificationStamp(), componentsFromCatalogXml);
catalogFile.putUserData(MOD_STAMP_AND_COMPONENTS_FROM_CATALOG_XML, modStampAndComponents);
}
for (final ComponentFromCatalogXml componentFromCatalogXml : modStampAndComponents.second) {
consumer.consume(componentFromCatalogXml);
}
}
private static ComponentFromCatalogXml[] parseComponentsFromCatalogXml(final VirtualFile catalogFile) {
final Collection<ComponentFromCatalogXml> result = new ArrayList<>();
final XmlBuilder xmlBuilder = new XmlBuilderAdapter() {
private static final String COMPONENT_LOCATION = ".swc.components.component";
private static final String NAME = "name";
private static final String CLASS_NAME = "className";
private static final String URI = "uri";
private static final String ICON = "icon";
private String myNameAttr = null;
private String myClassNameAttr = null;
private String myUriAttr = null;
private String myIconAttr = null;
@Override
public void attribute(CharSequence name, CharSequence value, int start, int end) {
if (COMPONENT_LOCATION.equals(getLocation())) {
if (NAME.equals(name)) {
myNameAttr = value.toString().trim();
}
else if (CLASS_NAME.equals(name)) {
myClassNameAttr = value.toString().trim();
}
else if (URI.equals(name)) {
myUriAttr = value.toString().trim();
}
else if (ICON.equals(name)) {
myIconAttr = value.toString().trim();
}
}
}
private final StringInterner myStringInterner = new StringInterner();
@Override
public void endTag(CharSequence localName, String namespace, int start, int end) {
if (COMPONENT_LOCATION.equals(getLocation())) {
if (StringUtil.isNotEmpty(myNameAttr) && StringUtil.isNotEmpty(myClassNameAttr) && StringUtil.isNotEmpty(myUriAttr)) {
result.add(
new ComponentFromCatalogXml(
new String(myNameAttr),
new String(myClassNameAttr.replace(":", ".")),
myStringInterner.intern(new String(myUriAttr)),
myIconAttr != null ? new String(myIconAttr):null
)
);
}
myNameAttr = null;
myClassNameAttr = null;
myUriAttr = null;
myIconAttr = null;
}
super.endTag(localName, namespace, start, end);
}
};
try {
new XmlBuilderDriver(VfsUtilCore.loadText(catalogFile)).build(xmlBuilder);
}
catch (IOException ignored) {/*ignore*/}
return result.toArray(new ComponentFromCatalogXml[result.size()]);
}
public static void processManifestFile(final VirtualFile manifestFile, final Consumer<ComponentFromManifest> consumer) {
Pair<Long, ComponentFromManifest[]> modStampAndComponents = manifestFile.getUserData(MOD_STAMP_AND_COMPONENTS_FROM_MANIFEST);
if (modStampAndComponents == null || modStampAndComponents.first != manifestFile.getModificationStamp()) {
final ComponentFromManifest[] componentsFromManifests = parseManifestFile(manifestFile);
modStampAndComponents = Pair.create(manifestFile.getModificationStamp(), componentsFromManifests);
manifestFile.putUserData(MOD_STAMP_AND_COMPONENTS_FROM_MANIFEST, modStampAndComponents);
}
for (final ComponentFromManifest componentFromManifest : modStampAndComponents.second) {
consumer.consume(componentFromManifest);
}
}
private static ComponentFromManifest[] parseManifestFile(final VirtualFile manifestFile) {
final Collection<ComponentFromManifest> result = new ArrayList<>();
final XmlBuilder builder = new XmlBuilderAdapter() {
private static final String COMPONENT = "component";
private static final String ID = "id";
private static final String CLASS = "class";
private String idAttr = null;
private String classAttr = null;
@Override
public void attribute(final CharSequence name, final CharSequence value, final int start, final int end) {
if (ID.equals(name.toString())) {
idAttr = value.toString().trim();
}
else if (CLASS.equals(name.toString())) {
classAttr = value.toString().trim();
}
}
@Override
public void endTag(final CharSequence localName, final String namespace, final int start, final int end) {
if (COMPONENT.equals(localName)) {
if (StringUtil.isNotEmpty(classAttr)) {
final String classFqn = classAttr.replace(":", ".");
final String componentName = idAttr != null ? idAttr : classAttr.substring(classFqn.lastIndexOf('.') + 1);
result.add(new ComponentFromManifest(new String(componentName), new String(classFqn)));
}
}
idAttr = null;
classAttr = null;
super.endTag(localName, namespace, start, end);
}
};
try {
new XmlBuilderDriver(VfsUtilCore.loadText(manifestFile)).build(builder);
}
catch (IOException ignored) {/*ignore*/}
return result.toArray(new ComponentFromManifest[result.size()]);
}
}