blob: 03e28fb006fd620d17bae61e6e1ef91ef47ce23f [file] [log] [blame]
package org.jetbrains.android;
import com.android.annotations.VisibleForTesting;
import com.android.resources.ResourceType;
import com.google.common.collect.Sets;
import com.intellij.openapi.fileTypes.StdFileTypes;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import com.intellij.psi.XmlRecursiveElementVisitor;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.containers.HashMap;
import com.intellij.util.containers.HashSet;
import com.intellij.util.indexing.*;
import com.intellij.util.io.DataExternalizer;
import com.intellij.util.io.KeyDescriptor;
import com.intellij.util.text.CharArrayUtil;
import com.intellij.util.xml.NanoXmlUtil;
import org.jetbrains.android.util.AndroidCommonUtils;
import org.jetbrains.android.util.ResourceEntry;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
/**
* @author Eugene.Kudelevsky
*/
public class AndroidValueResourcesIndex extends FileBasedIndexExtension<ResourceEntry, Set<AndroidValueResourcesIndex.MyResourceInfo>> {
public static final ID<ResourceEntry, Set<MyResourceInfo>> INDEX_ID = ID.create("android.value.resources.index");
@NonNls private static final String RESOURCES_ROOT_TAG = "resources";
@NonNls private static final String NAME_ATTRIBUTE_VALUE = "name";
@NonNls private static final String TYPE_ATTRIBUTE_VALUE = "type";
private final DataIndexer<ResourceEntry, Set<MyResourceInfo>, FileContent> myIndexer =
new DataIndexer<ResourceEntry, Set<MyResourceInfo>, FileContent>() {
@Override
@NotNull
public Map<ResourceEntry, Set<MyResourceInfo>> map(@NotNull FileContent inputData) {
if (!isSimilarFile(inputData)) {
return Collections.emptyMap();
}
final PsiFile file = inputData.getPsiFile();
if (!(file instanceof XmlFile)) {
return Collections.emptyMap();
}
final Map<ResourceEntry, Set<MyResourceInfo>> result = new HashMap<ResourceEntry, Set<MyResourceInfo>>();
file.accept(new XmlRecursiveElementVisitor() {
@Override
public void visitXmlTag(XmlTag tag) {
super.visitXmlTag(tag);
final String resName = tag.getAttributeValue(NAME_ATTRIBUTE_VALUE);
if (resName == null) {
return;
}
final String tagName = tag.getName();
final String resTypeStr;
if ("item".equals(tagName)) {
resTypeStr = tag.getAttributeValue(TYPE_ATTRIBUTE_VALUE);
}
else {
resTypeStr = AndroidCommonUtils.getResourceTypeByTagName(tagName);
}
final ResourceType resType = resTypeStr != null ? ResourceType.getEnum(resTypeStr) : null;
if (resType == null) {
return;
}
final int offset = tag.getTextRange().getStartOffset();
if (resType == ResourceType.ATTR) {
final XmlTag parentTag = tag.getParentTag();
final String contextName = parentTag != null ? parentTag.getAttributeValue(NAME_ATTRIBUTE_VALUE) : null;
processResourceEntry(new ResourceEntry(resTypeStr, resName, contextName != null ? contextName : ""), result, offset);
}
else {
processResourceEntry(new ResourceEntry(resTypeStr, resName, ""), result, offset);
}
}
});
return result;
}
};
private static boolean isSimilarFile(FileContent inputData) {
if (CharArrayUtil.indexOf(inputData.getContentAsText(), "<" + RESOURCES_ROOT_TAG, 0) < 0) {
return false;
}
final boolean[] ourRootTag = {false};
NanoXmlUtil.parse(CharArrayUtil.readerFromCharSequence(inputData.getContentAsText()), new NanoXmlUtil.IXMLBuilderAdapter() {
@Override
public void startElement(String name, String nsPrefix, String nsURI, String systemID, int lineNr)
throws Exception {
ourRootTag[0] = RESOURCES_ROOT_TAG.equals(name) && nsPrefix == null;
stop();
}
});
return ourRootTag[0];
}
private static void processResourceEntry(@NotNull ResourceEntry entry,
@NotNull Map<ResourceEntry, Set<MyResourceInfo>> result,
int offset) {
final MyResourceInfo info = new MyResourceInfo(entry, offset);
result.put(entry, Collections.singleton(info));
addEntryToMap(info, createTypeMarkerKey(entry.getType()), result);
addEntryToMap(info, createTypeNameMarkerKey(entry.getType(), entry.getName()), result);
}
private static void addEntryToMap(MyResourceInfo info, ResourceEntry marker, Map<ResourceEntry, Set<MyResourceInfo>> result) {
Set<MyResourceInfo> set = result.get(marker);
if (set == null) {
set = new HashSet<MyResourceInfo>();
result.put(marker, set);
}
set.add(info);
}
@NotNull
public static ResourceEntry createTypeMarkerKey(String type) {
return createTypeNameMarkerKey(type, "TYPE_MARKER_RESOURCE");
}
@NotNull
public static ResourceEntry createTypeNameMarkerKey(String type, String name) {
return new ResourceEntry(type, normalizeDelimiters(name), "TYPE_MARKER_CONTEXT");
}
@VisibleForTesting
static String normalizeDelimiters(String s) {
int length = s.length();
for (int j = 0, n = length; j < n; j++) {
final char ch = s.charAt(j);
if (!Character.isLetterOrDigit(ch) && ch != '_') {
StringBuilder result = new StringBuilder(length);
for (int i = 0; i < n; i++) {
final char c = s.charAt(i);
if (Character.isLetterOrDigit(c)) {
result.append(c);
}
else {
result.append('_');
}
}
return result.toString();
}
}
return s;
}
private final KeyDescriptor<ResourceEntry> myKeyDescriptor = new KeyDescriptor<ResourceEntry>() {
@Override
public void save(@NotNull DataOutput out, ResourceEntry value) throws IOException {
out.writeUTF(value.getType());
out.writeUTF(value.getName());
out.writeUTF(value.getContext());
}
@Override
public ResourceEntry read(@NotNull DataInput in) throws IOException {
final String resType = in.readUTF();
final String resName = in.readUTF();
final String resContext = in.readUTF();
return new ResourceEntry(resType, resName, resContext);
}
@Override
public int getHashCode(ResourceEntry value) {
return value.hashCode();
}
@Override
public boolean isEqual(ResourceEntry val1, ResourceEntry val2) {
return val1.equals(val2);
}
};
private final DataExternalizer<Set<MyResourceInfo>> myValueExternalizer = new DataExternalizer<Set<MyResourceInfo>>() {
@Override
public void save(@NotNull DataOutput out, Set<MyResourceInfo> value) throws IOException {
out.writeInt(value.size());
for (MyResourceInfo entry : value) {
out.writeUTF(entry.getResourceEntry().getType());
out.writeUTF(entry.getResourceEntry().getName());
out.writeUTF(entry.getResourceEntry().getContext());
out.writeInt(entry.getOffset());
}
}
@Nullable
@Override
public Set<MyResourceInfo> read(@NotNull DataInput in) throws IOException {
final int size = in.readInt();
if (size < 0 || size > 65535) {
// Something is very wrong; trigger an index rebuild
throw new IOException("Corrupt Index: Size " + size);
}
if (size == 0) {
return Collections.emptySet();
}
final Set<MyResourceInfo> result = Sets.newHashSetWithExpectedSize(size);
for (int i = 0; i < size; i++) {
final String type = in.readUTF();
final String name = in.readUTF();
final String context = in.readUTF();
final int offset = in.readInt();
result.add(new MyResourceInfo(new ResourceEntry(type, name, context), offset));
}
return result;
}
};
@NotNull
@Override
public ID<ResourceEntry, Set<MyResourceInfo>> getName() {
return INDEX_ID;
}
@NotNull
@Override
public DataIndexer<ResourceEntry, Set<MyResourceInfo>, FileContent> getIndexer() {
return myIndexer;
}
@NotNull
@Override
public KeyDescriptor<ResourceEntry> getKeyDescriptor() {
return myKeyDescriptor;
}
@NotNull
@Override
public DataExternalizer<Set<MyResourceInfo>> getValueExternalizer() {
return myValueExternalizer;
}
@NotNull
@Override
public FileBasedIndex.InputFilter getInputFilter() {
return new DefaultFileTypeSpecificInputFilter(StdFileTypes.XML) {
@Override
public boolean acceptInput(@NotNull final VirtualFile file) {
return file.isInLocalFileSystem();
}
};
}
@Override
public boolean dependsOnFileContent() {
return true;
}
@Override
public int getVersion() {
return 5;
}
public static class MyResourceInfo {
private final ResourceEntry myResourceEntry;
private final int myOffset;
private MyResourceInfo(@NotNull ResourceEntry resourceEntry, int offset) {
myResourceEntry = resourceEntry;
myOffset = offset;
}
@NotNull
public ResourceEntry getResourceEntry() {
return myResourceEntry;
}
public int getOffset() {
return myOffset;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
MyResourceInfo info = (MyResourceInfo)o;
if (myOffset != info.myOffset) {
return false;
}
if (!myResourceEntry.equals(info.myResourceEntry)) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = myResourceEntry.hashCode();
result = 31 * result + myOffset;
return result;
}
@Override
public String toString() {
return this.getClass().getDeclaringClass().getSimpleName() +
'.' +
this.getClass().getSimpleName() +
'@' +
Integer.toHexString(System.identityHashCode(this)) +
'(' +
myResourceEntry +
',' +
myOffset +
')';
}
}
}