All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.android.tools.idea.rendering.PsiResourceItem Maven / Gradle / Ivy

/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * 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.android.tools.idea.rendering;

import com.android.annotations.NonNull;
import com.android.ide.common.rendering.api.*;
import com.android.ide.common.res2.ResourceFile;
import com.android.ide.common.res2.ResourceItem;
import com.android.ide.common.res2.ValueXmlHelper;
import com.android.ide.common.resources.configuration.DensityQualifier;
import com.android.ide.common.resources.configuration.FolderConfiguration;
import com.android.resources.Density;
import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
import com.google.common.base.Splitter;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.util.Computable;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.xml.XmlElementType;
import com.intellij.psi.xml.XmlTag;
import com.intellij.psi.xml.XmlText;
import com.intellij.psi.xml.XmlTokenType;
import org.jetbrains.annotations.Nullable;

import java.util.Collections;

import static com.android.SdkConstants.*;
import static com.android.ide.common.resources.ResourceResolver.*;

public class PsiResourceItem extends ResourceItem {
  private final XmlTag myTag;
  private PsiFile myFile;

  PsiResourceItem(@NonNull String name, @NonNull ResourceType type, @Nullable XmlTag tag, @NonNull PsiFile file) {
    super(name, type, null);
    myTag = tag;
    myFile = file;
  }

  @Override
  public FolderConfiguration getConfiguration() {
    PsiResourceFile source = (PsiResourceFile)super.getSource();

    // Temporary safety workaround
    if (source == null) {
      if (myFile != null) {
        PsiDirectory parent = myFile.getParent();
        if (parent != null) {
          String name = parent.getName();
          FolderConfiguration configuration = FolderConfiguration.getConfigForFolder(name);
          if (configuration != null) {
            return configuration;
          }
        }
      }

      String qualifiers = getQualifiers();
      if (qualifiers.isEmpty()) {
        return new FolderConfiguration();
      }
      FolderConfiguration fromQualifiers = FolderConfiguration.getConfigFromQualifiers(Splitter.on('-').split(qualifiers));
      if (fromQualifiers == null) {
        return new FolderConfiguration();
      }
      return fromQualifiers;
    }
    return source.getFolderConfiguration();
  }

  @Nullable
  @Override
  public ResourceFile getSource() {
    ResourceFile source = super.getSource();

    // Temporary safety workaround
    if (source == null && myFile != null && myFile.getParent() != null) {
      PsiDirectory parent = myFile.getParent();
      if (parent != null) {
        String name = parent.getName();
        ResourceFolderType folderType = ResourceFolderType.getFolderType(name);
        FolderConfiguration configuration = FolderConfiguration.getConfigForFolder(name);
        int index = name.indexOf('-');
        String qualifiers = index == -1 ? "" : name.substring(index + 1);
        source = new PsiResourceFile(myFile, Collections.singletonList(this), qualifiers, folderType,
                                     configuration);
        setSource(source);
      }
    }

    return source;
  }

  @Nullable
  @Override
  public ResourceValue getResourceValue(boolean isFrameworks) {
    if (mResourceValue == null) {
      //noinspection VariableNotUsedInsideIf
      if (myTag == null) {
        // Density based resource value?
        ResourceType type = getType();
        Density density = type == ResourceType.DRAWABLE ? getFolderDensity() : null;
        if (density != null) {
          mResourceValue = new DensityBasedResourceValue(type, getName(), getSource().getFile().getAbsolutePath(), density, isFrameworks);
        } else {
          mResourceValue = new ResourceValue(type, getName(), getSource().getFile().getAbsolutePath(), isFrameworks);
        }
      } else {
        mResourceValue = parseXmlToResourceValue(isFrameworks);
      }
    }

    return mResourceValue;
  }

  @Nullable
  private Density getFolderDensity() {
    FolderConfiguration configuration = getConfiguration();
    if (configuration != null) {
      DensityQualifier densityQualifier = configuration.getDensityQualifier();
      if (densityQualifier != null) {
        return densityQualifier.getValue();
      }
    }
    return null;
  }

  @Nullable
  private ResourceValue parseXmlToResourceValue(boolean isFrameworks) {
    assert myTag != null;

    if (!myTag.isValid()) {
      return null;
    }

    ResourceType type = getType();
    String name = getName();

    ResourceValue value;
    switch (type) {
      case STYLE:
        String parent = getAttributeValue(myTag, ATTR_PARENT);
        value = parseStyleValue(new StyleResourceValue(type, name, parent, isFrameworks));
        break;
      case DECLARE_STYLEABLE:
        //noinspection deprecation
        value = parseDeclareStyleable(new DeclareStyleableResourceValue(type, name, isFrameworks));
        break;
      case ATTR:
        value = parseAttrValue(new AttrResourceValue(type, name, isFrameworks));
        break;
      case ARRAY:
        value = parseArrayValue(new ArrayResourceValue(name, isFrameworks) {
          // Allow the user to specify a specific element to use via tools:index
          @Override
          protected int getDefaultIndex() {
            String index = myTag.getAttributeValue(ATTR_INDEX, TOOLS_URI);
            if (index != null) {
              return Integer.parseInt(index);
            }
            return super.getDefaultIndex();
          }
        });
        break;
      case PLURALS:
        value = parsePluralsValue(new PluralsResourceValue(name, isFrameworks) {
          // Allow the user to specify a specific quantity to use via tools:quantity
          @Override
          public String getValue() {
            String quantity = myTag.getAttributeValue(ATTR_QUANTITY, TOOLS_URI);
            if (quantity != null) {
              String value = getValue(quantity);
              if (value != null) {
                return value;
              }
            }
            return super.getValue();
          }
        });
        break;
      case STRING:
        value = parseTextValue(new PsiTextResourceValue(type, name, isFrameworks));
        break;
      default:
        value = parseValue(new ResourceValue(type, name, isFrameworks));
        break;
    }

    return value;
  }

  @Nullable
  private static String getAttributeValue(XmlTag tag, String attributeName) {
    return tag.getAttributeValue(attributeName);
  }

  @SuppressWarnings("deprecation") // support for deprecated (but supported) API
  @NonNull
  private ResourceValue parseDeclareStyleable(@NonNull DeclareStyleableResourceValue declareStyleable) {
    assert myTag != null;
    for (XmlTag child : myTag.getSubTags()) {
      String name = getAttributeValue(child, ATTR_NAME);
      if (name != null) {
        // is the attribute in the android namespace?
        boolean isFrameworkAttr = declareStyleable.isFramework();
        if (name.startsWith(ANDROID_NS_NAME_PREFIX)) {
          name = name.substring(ANDROID_NS_NAME_PREFIX_LEN);
          isFrameworkAttr = true;
        }

        AttrResourceValue attr = parseAttrValue(child, new AttrResourceValue(ResourceType.ATTR, name, isFrameworkAttr));
        declareStyleable.addValue(attr);
      }
    }

    return declareStyleable;
  }

  @NonNull
  private ResourceValue parseStyleValue(@NonNull StyleResourceValue styleValue) {
    assert myTag != null;
    for (XmlTag child : myTag.getSubTags()) {
      String name = getAttributeValue(child, ATTR_NAME);
      if (name != null) {
        // is the attribute in the android namespace?
        boolean isFrameworkAttr = styleValue.isFramework();
        if (name.startsWith(ANDROID_NS_NAME_PREFIX)) {
          name = name.substring(ANDROID_NS_NAME_PREFIX_LEN);
          isFrameworkAttr = true;
        }

        ItemResourceValue resValue = new ItemResourceValue(name, isFrameworkAttr, styleValue.isFramework());
        resValue.setValue(ValueXmlHelper.unescapeResourceString(getTextContent(child), true, true));
        styleValue.addItem(resValue);
      }
    }

    return styleValue;
  }

  @NonNull
  private AttrResourceValue parseAttrValue(@NonNull AttrResourceValue attrValue) {
    assert myTag != null;
    return parseAttrValue(myTag, attrValue);
  }

  @NonNull
  private static AttrResourceValue parseAttrValue(@NonNull XmlTag myTag, @NonNull AttrResourceValue attrValue) {
    for (XmlTag child : myTag.getSubTags()) {
      String name = getAttributeValue(child, ATTR_NAME);
      if (name != null) {
        String value = getAttributeValue(child, ATTR_VALUE);
        if (value != null) {
          try {
            // Integer.decode/parseInt can't deal with hex value > 0x7FFFFFFF so we
            // use Long.decode instead.
            attrValue.addValue(name, (int)(long)Long.decode(value));
          } catch (NumberFormatException e) {
            // pass, we'll just ignore this value
          }
        }
      }
    }

    return attrValue;
  }

  private ResourceValue parseArrayValue(ArrayResourceValue arrayValue) {
    assert myTag != null;
    for (XmlTag child : myTag.getSubTags()) {
      String text = ValueXmlHelper.unescapeResourceString(getTextContent(child), true, true);
      arrayValue.addElement(text);
    }

    return arrayValue;
  }

  private ResourceValue parsePluralsValue(PluralsResourceValue value) {
    assert myTag != null;
    for (XmlTag child : myTag.getSubTags()) {
      String quantity = child.getAttributeValue(ATTR_QUANTITY);
      if (quantity != null) {
        String text = ValueXmlHelper.unescapeResourceString(getTextContent(child), true, true);
        value.addPlural(quantity, text);
      }
    }

    return value;
  }

  @NonNull
  private ResourceValue parseValue(@NonNull ResourceValue value) {
    assert myTag != null;
    String text = getTextContent(myTag);
    text = ValueXmlHelper.unescapeResourceString(text, true, true);
    value.setValue(text);
    return value;
  }

  /**
   * Returns the text content of a given tag
   */
  public static String getTextContent(@NonNull XmlTag tag) {
    // We can't just use tag.getValue().getTrimmedText() here because we need to remove
    // intermediate elements such as  text:
    // TODO: Make sure I correct handle HTML content for XML items in  nodes!
    // For example, for the following string we want to compute "Share with %s":
    // Share with %s
    XmlTag[] subTags = tag.getSubTags();
    XmlText[] textElements = tag.getValue().getTextElements();
    if (subTags.length == 0) {
      if (textElements.length == 1) {
        return getXmlTextValue(textElements[0]);
      } else if (textElements.length == 0) {
        return "";
      }
    }
    StringBuilder sb = new StringBuilder(40);
    appendText(sb, tag);
    return sb.toString();
  }

  @NonNull
  private PsiTextResourceValue parseTextValue(@NonNull PsiTextResourceValue value) {
    assert myTag != null;
    String text = getTextContent(myTag);
    text = ValueXmlHelper.unescapeResourceString(text, true, true);
    value.setValue(text);
    return value;
  }

  private static String getXmlTextValue(XmlText element) {
    PsiElement current = element.getFirstChild();
    if (current != null) {
      if (current.getNextSibling() != null) {
        StringBuilder sb = new StringBuilder();
        for (; current != null; current = current.getNextSibling()) {
          IElementType type = current.getNode().getElementType();
          if (type == XmlElementType.XML_CDATA) {
            PsiElement[] children = current.getChildren();
            if (children.length == 3) { // XML_CDATA_START, XML_DATA_CHARACTERS, XML_CDATA_END
              assert children[1].getNode().getElementType() == XmlTokenType.XML_DATA_CHARACTERS;
              sb.append(children[1].getText());
            }
            continue;
          }
          sb.append(current.getText());
        }
        return sb.toString();
      } else if (current.getNode().getElementType() == XmlElementType.XML_CDATA) {
        PsiElement[] children = current.getChildren();
        if (children.length == 3) { // XML_CDATA_START, XML_DATA_CHARACTERS, XML_CDATA_END
          assert children[1].getNode().getElementType() == XmlTokenType.XML_DATA_CHARACTERS;
          return children[1].getText();
        }
      }
    }

    return element.getText();
  }

  private static void appendText(@NonNull StringBuilder sb, @NonNull XmlTag tag) {
    PsiElement[] children = tag.getChildren();
    for (PsiElement child : children) {
      if (child instanceof XmlText) {
        XmlText text = (XmlText)child;
        sb.append(getXmlTextValue(text));
      } else if (child instanceof XmlTag) {
        XmlTag childTag = (XmlTag)child;
        // xliff support
        if (XLIFF_G_TAG.equals(childTag.getLocalName()) && childTag.getNamespace().startsWith(XLIFF_NAMESPACE_PREFIX)) {
          String example = childTag.getAttributeValue(ATTR_EXAMPLE);
          if (example != null) {
            // %d minutes => "(7) minutes"
            sb.append('(').append(example).append(')');
            continue;
          } else {
            String id = childTag.getAttributeValue(ATTR_ID);
            if (id != null) {
              // Step %1$d => Step ${step_number}
              sb.append('$').append('{').append(id).append('}');
              continue;
            }
          }
        }
        appendText(sb, childTag);
      }
    }
  }

  @NonNull
  PsiFile getPsiFile() {
    return myFile;
  }

  /** Clears the cached value, if any, and returns true if the value was cleared */
  public boolean recomputeValue() {
    if (mResourceValue != null) {
      // Force recompute in getResourceValue
      mResourceValue = null;
      return true;
    } else {
      return false;
    }
  }

  @Nullable
  public XmlTag getTag() {
    return myTag;
  }

  @Override
  public boolean equals(Object o) {
    // Only reference equality; we need to be able to distinguish duplicate elements which can happen during editing
    // for incremental updating to handle temporarily aliasing items.
    return this == o;
  }

  @Override
  public int hashCode() {
    return getName().hashCode();
  }

  @Override
  public String toString() {
    return super.toString() + ": " + (myTag != null ? getTextContent(myTag) : "null" + (myFile != null ? ":" + myFile.getName() : ""));
  }

  private class PsiTextResourceValue extends TextResourceValue {
    public PsiTextResourceValue(ResourceType type, String name, boolean isFramework) {
      super(type, name, isFramework);
    }

    @Override
    public String getRawXmlValue() {
      if (myTag != null && myTag.isValid()) {
        if (!ApplicationManager.getApplication().isReadAccessAllowed()) {
          return ApplicationManager.getApplication().runReadAction(new Computable() {
            @Override
            public String compute() {
              return myTag.getValue().getText();
            }
          });
        }
        return myTag.getValue().getText();
      }
      else {
        return getValue();
      }
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy