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

com.android.resources.base.RepositoryLoader Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2019 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.resources.base;

import static com.android.SdkConstants.ANDROID_NS_NAME;
import static com.android.SdkConstants.ATTR_FORMAT;
import static com.android.SdkConstants.ATTR_ID;
import static com.android.SdkConstants.ATTR_INDEX;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.ATTR_PARENT;
import static com.android.SdkConstants.ATTR_QUANTITY;
import static com.android.SdkConstants.ATTR_TYPE;
import static com.android.SdkConstants.ATTR_VALUE;
import static com.android.SdkConstants.DOT_AAR;
import static com.android.SdkConstants.DOT_JAR;
import static com.android.SdkConstants.DOT_XML;
import static com.android.SdkConstants.DOT_ZIP;
import static com.android.SdkConstants.FD_RES_VALUES;
import static com.android.SdkConstants.NEW_ID_PREFIX;
import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
import static com.android.SdkConstants.PREFIX_THEME_REF;
import static com.android.SdkConstants.TAG_ATTR;
import static com.android.SdkConstants.TAG_EAT_COMMENT;
import static com.android.SdkConstants.TAG_ENUM;
import static com.android.SdkConstants.TAG_FLAG;
import static com.android.SdkConstants.TAG_ITEM;
import static com.android.SdkConstants.TAG_PUBLIC;
import static com.android.SdkConstants.TAG_PUBLIC_GROUP;
import static com.android.SdkConstants.TAG_RESOURCES;
import static com.android.SdkConstants.TAG_SKIP;
import static com.android.SdkConstants.TAG_STAGING_PUBLIC_GROUP;
import static com.android.SdkConstants.TAG_STAGING_PUBLIC_GROUP_FINAL;
import static com.android.SdkConstants.TOOLS_URI;
import static com.android.ide.common.resources.AndroidAaptIgnoreKt.ANDROID_AAPT_IGNORE;
import static com.android.ide.common.resources.ResourceItem.ATTR_EXAMPLE;
import static com.android.ide.common.resources.ResourceItem.XLIFF_G_TAG;
import static com.android.ide.common.resources.ResourceItem.XLIFF_NAMESPACE_PREFIX;
import static com.intellij.util.io.URLUtil.JAR_PROTOCOL;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.android.ide.common.rendering.api.AttrResourceValue;
import com.android.ide.common.rendering.api.AttributeFormat;
import com.android.ide.common.rendering.api.DensityBasedResourceValue;
import com.android.ide.common.rendering.api.ResourceNamespace;
import com.android.ide.common.rendering.api.StyleItemResourceValue;
import com.android.ide.common.rendering.api.StyleItemResourceValueImpl;
import com.android.ide.common.resources.AndroidAaptIgnore;
import com.android.ide.common.resources.PatternBasedFileFilter;
import com.android.ide.common.resources.ResourceItem;
import com.android.ide.common.resources.ResourceRepository;
import com.android.ide.common.resources.ResourcesUtil;
import com.android.ide.common.resources.ValueResourceNameValidator;
import com.android.ide.common.resources.ValueXmlHelper;
import com.android.ide.common.resources.configuration.DensityQualifier;
import com.android.ide.common.resources.configuration.FolderConfiguration;
import com.android.ide.common.util.PathString;
import com.android.io.CancellableFileIo;
import com.android.resources.Arity;
import com.android.resources.Density;
import com.android.resources.FolderTypeRelationship;
import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
import com.android.resources.ResourceVisibility;
import com.android.utils.SdkUtils;
import com.android.utils.XmlUtils;
import com.google.common.base.Preconditions;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.collect.Table;
import com.google.common.collect.Tables;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.io.URLUtil;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.kxml2.io.KXmlParser;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

public abstract class RepositoryLoader implements FileFilter {
  private static final Logger LOG = Logger.getInstance(RepositoryLoader.class);
  /** The set of attribute formats that is used when no formats are explicitly specified and the attribute is not a flag or enum. */
  private final Set DEFAULT_ATTR_FORMATS = Sets.immutableEnumSet(
      AttributeFormat.BOOLEAN,
      AttributeFormat.COLOR,
      AttributeFormat.DIMENSION,
      AttributeFormat.FLOAT,
      AttributeFormat.FRACTION,
      AttributeFormat.INTEGER,
      AttributeFormat.REFERENCE,
      AttributeFormat.STRING);
  private final PatternBasedFileFilter myFileFilter
    = new PatternBasedFileFilter(new AndroidAaptIgnore(System.getenv(ANDROID_AAPT_IGNORE)));

  @NotNull private final Map> myPublicResources = new EnumMap<>(ResourceType.class);
  @NotNull private final ListMultimap myAttrs = ArrayListMultimap.create();
  @NotNull private final ListMultimap myAttrCandidates = ArrayListMultimap.create();
  @NotNull private final ListMultimap myStyleables = ArrayListMultimap.create();
  @NotNull protected ResourceVisibility myDefaultVisibility = ResourceVisibility.PRIVATE;
  /** Cache of FolderConfiguration instances, keyed by qualifier strings (see {@link FolderConfiguration#getQualifierString()}). */
  @NotNull protected final Map myFolderConfigCache = new HashMap<>();
  @NotNull private final Map myConfigCache = new HashMap<>();
  @NotNull private final ValueResourceXmlParser myParser = new ValueResourceXmlParser();
  @NotNull private final XmlTextExtractor myTextExtractor = new XmlTextExtractor();
  @NotNull private final ResourceUrlParser myUrlParser = new ResourceUrlParser();
  // Used to keep track of resources defined in the current value resource file.
  @NotNull private final Table myValueFileResources =
      Tables.newCustomTable(new EnumMap<>(ResourceType.class), LinkedHashMap::new);
  @NotNull protected final Path myResourceDirectoryOrFile;
  @NotNull private final PathString myResourceDirectoryOrFilePath;
  private final boolean myLoadingFromZipArchive;

  @NotNull private final ResourceNamespace myNamespace;
  @Nullable private final Collection myResourceFilesAndFolders;
  @Nullable protected ZipFile myZipFile;

  public RepositoryLoader(@NotNull Path resourceDirectoryOrFile, @Nullable Collection resourceFilesAndFolders,
                          @NotNull ResourceNamespace namespace) {
    myResourceDirectoryOrFile = resourceDirectoryOrFile;
    myResourceDirectoryOrFilePath = new PathString(myResourceDirectoryOrFile);
    myLoadingFromZipArchive = isZipArchive(resourceDirectoryOrFile);
    myNamespace = namespace;
    myResourceFilesAndFolders = resourceFilesAndFolders;
  }

  @NotNull
  public final Path getResourceDirectoryOrFile() {
    return myResourceDirectoryOrFile;
  }

  public final boolean isLoadingFromZipArchive() {
    return myLoadingFromZipArchive;
  }

  @NotNull
  public final ResourceNamespace getNamespace() {
    return myNamespace;
  }

  public void loadRepositoryContents(@NotNull T repository) {
    if (myLoadingFromZipArchive) {
      loadFromZip(repository);
    }
    else {
      loadFromResFolder(repository);
    }
  }

  public List getPublicXmlFileNames() {
    return ImmutableList.of("public.xml");
  }

  protected void loadFromZip(@NotNull T repository) {
    try (ZipFile zipFile = new ZipFile(myResourceDirectoryOrFile.toFile())) {
      myZipFile = zipFile;
      loadPublicResourceNames();
      boolean shouldParseResourceIds = !loadIdsFromRTxt();

      zipFile.stream().forEach(zipEntry -> {
        if (!zipEntry.isDirectory()) {
          PathString path = new PathString(zipEntry.getName());
          loadResourceFile(path, repository, shouldParseResourceIds);
        }
      });
    }
    catch (ProcessCanceledException e) {
      throw e;
    }
    catch (Exception e) {
      LOG.error("Failed to load resources from " + myResourceDirectoryOrFile.toString(), e);
    }
    finally {
      myZipFile = null;
    }

    finishLoading(repository);
  }

  protected void loadFromResFolder(@NotNull T repository) {
    try {
      if (CancellableFileIo.notExists(myResourceDirectoryOrFile)) {
        return; // Don't report errors if the resource directory doesn't exist. This happens in some tests.
      }

      loadPublicResourceNames();
      boolean shouldParseResourceIds = !loadIdsFromRTxt();

      List sourceFilesAndFolders = myResourceFilesAndFolders == null ?
                                         ImmutableList.of(myResourceDirectoryOrFile) :
                                         ContainerUtil.map(myResourceFilesAndFolders, PathString::toPath);
      List resourceFiles = findResourceFiles(sourceFilesAndFolders);
      for (PathString file : resourceFiles) {
        loadResourceFile(file, repository, shouldParseResourceIds);
      }
    }
    catch (ProcessCanceledException e) {
      throw e;
    }
    catch (Exception e) {
      LOG.error("Failed to load resources from " + myResourceDirectoryOrFile.toString(), e);
    }

    finishLoading(repository);
  }

  protected final void loadResourceFile(@NotNull PathString file, @NotNull T repository, boolean shouldParseResourceIds) {
    String folderName = file.getParentFileName();
    if (folderName != null) {
      FolderInfo folderInfo = FolderInfo.create(folderName, myFolderConfigCache);
      if (folderInfo != null) {
        RepositoryConfiguration configuration = getConfiguration(repository, folderInfo.configuration);
        loadResourceFile(file, folderInfo, configuration, shouldParseResourceIds);
      }
    }
  }

  protected void finishLoading(@NotNull T repository) {
    processAttrsAndStyleables();
  }

  @NotNull
  public final String getSourceFileProtocol() {
    if (myLoadingFromZipArchive) {
      return JAR_PROTOCOL;
    }
    else {
      return "file";
    }
  }

  @NotNull
  public final String getResourcePathPrefix() {
    if (myLoadingFromZipArchive) {
      return portableFileName(myResourceDirectoryOrFile.toString()) + URLUtil.JAR_SEPARATOR + "res/";
    }
    else {
      return portableFileName(myResourceDirectoryOrFile.toString()) + '/';
    }
  }

  @NotNull
  public final String getResourceUrlPrefix() {
    if (myLoadingFromZipArchive) {
      return JAR_PROTOCOL + "://" + portableFileName(myResourceDirectoryOrFile.toString()) + URLUtil.JAR_SEPARATOR + "res/";
    }
    else {
      return portableFileName(myResourceDirectoryOrFile.toString()) + '/';
    }
  }

  /**
   * A hook for loading resource IDs from a R.txt file. This implementation does nothing but subclasses may override.
   *
   * @return true if the IDs were successfully loaded from R.txt
   */
  protected boolean loadIdsFromRTxt() {
    return false;
  }

  @Override
  public boolean isIgnored(@NotNull Path fileOrDirectory, @NotNull BasicFileAttributes attrs) {
    if (fileOrDirectory.equals(myResourceDirectoryOrFile)) {
      return false;
    }

    return myFileFilter.isIgnored(fileOrDirectory.toString(), attrs.isDirectory());
  }

  /**
   * Loads names of the public resources and populates {@link #myPublicResources}.
   */
  protected void loadPublicResourceNames() {
    Path valuesFolder = myResourceDirectoryOrFile.resolve(FD_RES_VALUES);
    List fileNames = getPublicXmlFileNames();
    for (String fileName : fileNames) {
      Path publicXmlFile = valuesFolder.resolve(fileName);

      try (InputStream stream = new BufferedInputStream(CancellableFileIo.newInputStream(publicXmlFile))) {
        CommentTrackingXmlPullParser parser = new CommentTrackingXmlPullParser();
        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
        parser.setInput(stream, UTF_8.name());

        String groupTag = null;
        ResourceType groupType = null;
        ResourceType lastType = null;
        String lastTypeName = "";
        while (true) {
          int event = parser.nextToken();
          if (event == XmlPullParser.START_TAG) {
            if (parser.getName().equals(TAG_PUBLIC)) {
              String name = null;
              String typeName = groupType == null ? null : groupType.getName();
              for (int i = 0, n = parser.getAttributeCount(); i < n; i++) {
                String attribute = parser.getAttributeName(i);

                if (attribute.equals(ATTR_NAME)) {
                  name = parser.getAttributeValue(i);
                  if (typeName != null) {
                    // Skip attributes other than "type" and "name".
                    break;
                  }
                }
                else if (attribute.equals(ATTR_TYPE)) {
                  typeName = parser.getAttributeValue(i);
                }
              }

              if (name != null && !name.startsWith("__removed") && (typeName != null || groupType != null) &&
                  (parser.getLastComment() == null || !containsWord(parser.getLastComment(), "@hide"))) {
                ResourceType type;
                if (groupType != null) {
                  type = groupType;
                }
                else {
                  if (typeName.equals(lastTypeName)) {
                    type = lastType;
                  }
                  else {
                    type = ResourceType.fromXmlValue(typeName);
                    lastType = type;
                    lastTypeName = typeName;
                  }
                }

                if (type != null) {
                  addPublicResourceName(type, name);
                }
                else {
                  LOG.error("Public resource declaration \"" + name + "\" of type " + typeName + " points to unknown resource type.");
                }
              }
            }
            else if (isPublicGroupTag(parser.getName())) {
              groupTag = parser.getName();
              String typeName = parser.getAttributeValue(null, ATTR_TYPE);
              groupType = typeName == null ? null : ResourceType.fromXmlValue(typeName);
            }
          }
          else if (event == XmlPullParser.END_TAG) {
            if (groupTag != null && groupTag.equals(parser.getName())) {
              groupTag = null;
              groupType = null;
            }
          }
          else if (event == XmlPullParser.END_DOCUMENT) {
            break;
          }
        }
      }
      catch (ProcessCanceledException e) {
        throw e;
      }
      catch (NoSuchFileException e) {
        // There is no public.xml. This not considered an error.
      }
      catch (Exception e) {
        LOG.error("Can't read and parse " + publicXmlFile, e);
      }
    }
  }

  private boolean isPublicGroupTag(@NotNull String tag) {
    return tag.equals(TAG_PUBLIC_GROUP) ||
           tag.equals(TAG_STAGING_PUBLIC_GROUP) ||
           tag.equals(TAG_STAGING_PUBLIC_GROUP_FINAL);
  }

  protected final void addPublicResourceName(ResourceType type, String name) {
    Set names = myPublicResources.computeIfAbsent(type, t -> new HashSet<>());
    names.add(name);
  }

  /**
   * Checks if the given text contains the given word.
   */
  private static boolean containsWord(@NotNull String text, @SuppressWarnings("SameParameterValue") @NotNull String word) {
    int end = 0;
    while (true) {
      int start = text.indexOf(word, end);
      if (start < 0) {
        return false;
      }
      end = start + word.length();
      if ((start == 0 || Character.isWhitespace(text.charAt(start))) &&
          (end == text.length() || Character.isWhitespace(text.charAt(end)))) {
        return true;
      }
    }
  }

  @NotNull
  private List findResourceFiles(@NotNull List filesOrFolders) {
    ResourceFileCollector fileCollector = new ResourceFileCollector(this);
    for (Path file : filesOrFolders) {
      try {
        CancellableFileIo.walkFileTree(file, fileCollector);
      }
      catch (IOException e) {
        // All IOExceptions are logged by ResourceFileCollector.
      }
    }
    for (IOException e : fileCollector.ioErrors) {
      LOG.error("Error loading resources from " + myResourceDirectoryOrFile.toString(), e);
    }
    Collections.sort(fileCollector.resourceFiles); // Make sure that the files are in canonical order.
    return fileCollector.resourceFiles;
  }

  @NotNull
  protected final RepositoryConfiguration getConfiguration(@NotNull T repository, @NotNull FolderConfiguration folderConfiguration) {
    RepositoryConfiguration repositoryConfiguration = myConfigCache.get(folderConfiguration);
    if (repositoryConfiguration != null) {
      return repositoryConfiguration;
    }

    repositoryConfiguration = new RepositoryConfiguration(repository, folderConfiguration);
    myConfigCache.put(folderConfiguration, repositoryConfiguration);
    return repositoryConfiguration;
  }

  private void loadResourceFile(@NotNull PathString file, @NotNull FolderInfo folderInfo, @NotNull RepositoryConfiguration configuration,
                                boolean shouldParseResourceIds) {
    if (folderInfo.resourceType == null) {
      if (isXmlFile(file)) {
        parseValueResourceFile(file, configuration);
      }
    }
    else {
      if (shouldParseResourceIds && folderInfo.isIdGenerating && isXmlFile(file)) {
        parseIdGeneratingResourceFile(file, configuration);
      }

      BasicFileResourceItem item = createFileResourceItem(file, folderInfo.resourceType, configuration);
      addResourceItem(item);
    }
  }

  protected static boolean isXmlFile(@NotNull PathString file) {
    return isXmlFile(file.getFileName());
  }

  protected static boolean isXmlFile(@NotNull String filename) {
    return SdkUtils.endsWithIgnoreCase(filename, DOT_XML);
  }

  @SuppressWarnings("unchecked")
  private void addResourceItem(@NotNull BasicResourceItemBase item) {
    addResourceItem(item, (T)item.getRepository());
  }

  protected abstract void addResourceItem(@NotNull BasicResourceItem item, @NotNull T repository);

  protected final void parseValueResourceFile(@NotNull PathString file, @NotNull RepositoryConfiguration configuration) {
    try (InputStream stream = getInputStream(file)) {
      ResourceSourceFile sourceFile = createResourceSourceFile(file, configuration);
      myParser.setInput(stream, null);

      int event;
      do {
        event = myParser.nextToken();
        int depth = myParser.getDepth();
        if (event == XmlPullParser.START_TAG) {
          if (myParser.getPrefix() != null) {
            continue;
          }
          String tagName = myParser.getName();
          assert depth <= 2; // Deeper tags should be consumed by the createResourceItem method.
          if (depth == 1) {
            if (!tagName.equals(TAG_RESOURCES)) {
              break;
            }
          }
          else if (depth > 1) {
            ResourceType resourceType = getResourceType(tagName, file);
            if (resourceType != null && resourceType != ResourceType.PUBLIC) {
              String resourceName = myParser.getAttributeValue(null, ATTR_NAME);
              if (resourceName != null) {
                validateResourceName(resourceName, resourceType, file);
                BasicValueResourceItemBase item = createResourceItem(resourceType, resourceName, sourceFile);
                addValueResourceItem(item);
              } else {
                // Skip the subtags when the tag of a valid resource type doesn't have a name.
                skipSubTags();
              }
            }
            else {
              skipSubTags();
            }
          }
        }
      } while (event != XmlPullParser.END_DOCUMENT);
    }
    catch (ProcessCanceledException e) {
      throw e;
    }
    // KXmlParser throws RuntimeException for an undefined prefix and an illegal attribute name.
    catch (IOException | XmlPullParserException | XmlSyntaxException | RuntimeException e) {
      handleParsingError(file, e);
    }

    addValueFileResources();
  }

  @NotNull
  protected ResourceSourceFile createResourceSourceFile(@NotNull PathString file, @NotNull RepositoryConfiguration configuration) {
    return new ResourceSourceFileImpl(getResRelativePath(file), configuration);
  }

  private void addValueResourceItem(@NotNull BasicValueResourceItemBase item) {
    ResourceType resourceType = item.getType();
    // Add attr and styleable resources to intermediate maps to post-process them in the processAttrsAndStyleables
    // method after all resources are loaded.
    if (resourceType == ResourceType.ATTR) {
      addAttr((BasicAttrResourceItem)item, myAttrs);
    }
    else if (resourceType == ResourceType.STYLEABLE) {
      myStyleables.put(item.getName(), (BasicStyleableResourceItem)item);
    }
    else {
      // For compatibility with resource merger code we add value resources first to a file-specific map,
      // then move them to the global resource table. In case when there are multiple definitions of
      // the same resource in a single XML file, this algorithm preserves only the last definition.
      myValueFileResources.put(resourceType, item.getName(), item);
    }
  }

  protected final void addValueFileResources() {
    for (BasicValueResourceItemBase item : myValueFileResources.values()) {
      addResourceItem(item);
    }
    myValueFileResources.clear();
  }

  protected final void parseIdGeneratingResourceFile(@NotNull PathString file, @NotNull RepositoryConfiguration configuration) {
    try (InputStream stream = getInputStream(file)) {
      ResourceSourceFile sourceFile = createResourceSourceFile(file, configuration);
      XmlPullParser parser = new KXmlParser();
      parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
      parser.setInput(stream, null);

      int event;
      do {
        event = parser.nextToken();
        if (event == XmlPullParser.START_TAG) {
          int numAttributes = parser.getAttributeCount();
          for (int i = 0; i < numAttributes; i++) {
            String idValue = parser.getAttributeValue(i);
            if (idValue.startsWith(NEW_ID_PREFIX) && idValue.length() > NEW_ID_PREFIX.length()) {
              String resourceName = idValue.substring(NEW_ID_PREFIX.length());
              addIdResourceItem(resourceName, sourceFile);
            }
          }
        }
      } while (event != XmlPullParser.END_DOCUMENT);
    }
    catch (ProcessCanceledException e) {
      throw e;
    }
    // KXmlParser throws RuntimeException for an undefined prefix and an illegal attribute name.
    catch (IOException | XmlPullParserException | RuntimeException e) {
      handleParsingError(file, e);
    }

    addValueFileResources();
  }

  protected void handleParsingError(@NotNull PathString file, @NotNull Exception e) {
    LOG.warn("Failed to parse " + file.toString(), e);
  }

  @NotNull
  protected InputStream getInputStream(@NotNull PathString file) throws IOException {
    if (myZipFile == null) {
      Path path = file.toPath();
      Preconditions.checkArgument(path != null);
      return new BufferedInputStream(CancellableFileIo.newInputStream(path));
    }
    else {
      ProgressManager.checkCanceled();
      ZipEntry entry = myZipFile.getEntry(file.getPortablePath());
      if (entry == null) {
        throw new NoSuchFileException(file.getPortablePath());
      }
      return new BufferedInputStream(myZipFile.getInputStream(entry));
    }
  }

  protected final void addIdResourceItem(@NotNull String resourceName, @NotNull ResourceSourceFile sourceFile) {
    ResourceVisibility visibility = getVisibility(ResourceType.ID, resourceName);
    BasicValueResourceItem item = new BasicValueResourceItem(ResourceType.ID, resourceName, sourceFile, visibility, null);
    if (!resourceAlreadyDefined(item)) { // Don't create duplicate ID resources.
      addValueResourceItem(item);
    }
  }

  @NotNull
  private BasicFileResourceItem createFileResourceItem(
      @NotNull PathString file, @NotNull ResourceType resourceType, @NotNull RepositoryConfiguration configuration) {
    String resourceName = SdkUtils.fileNameToResourceName(file.getFileName());
    ResourceVisibility visibility = getVisibility(resourceType, resourceName);
    Density density = null;
    if (DensityBasedResourceValue.isDensityBasedResourceType(resourceType)) {
      DensityQualifier densityQualifier = configuration.getFolderConfiguration().getDensityQualifier();
      if (densityQualifier != null) {
        density = densityQualifier.getValue();
      }
    }
    return createFileResourceItem(file, resourceType, resourceName, configuration, visibility, density);
  }

  @NotNull
  protected final BasicFileResourceItem createFileResourceItem(@NotNull PathString file,
                                                               @NotNull ResourceType type,
                                                               @NotNull String name,
                                                               @NotNull RepositoryConfiguration configuration,
                                                               @NotNull ResourceVisibility visibility,
                                                               @Nullable Density density) {
    String relativePath = getResRelativePath(file);
    return density == null ?
           new BasicFileResourceItem(type, name, configuration, visibility, relativePath) :
           new BasicDensityBasedFileResourceItem(type, name, configuration, visibility, relativePath, density);
  }

  @NotNull
  private BasicValueResourceItemBase createResourceItem(
      @NotNull ResourceType type, @NotNull String name, @NotNull ResourceSourceFile sourceFile)
      throws IOException, XmlPullParserException, XmlSyntaxException {
    switch (type) {
      case ARRAY:
        return createArrayItem(name, sourceFile);

      case ATTR:
        return createAttrItem(name, sourceFile);

      case PLURALS:
        return createPluralsItem(name, sourceFile);

      case STRING:
        return createStringItem(type, name, sourceFile, true);

      case STYLE:
        return createStyleItem(name, sourceFile);

      case STYLEABLE:
        return createStyleableItem(name, sourceFile);

      case ANIMATOR:
      case DRAWABLE:
      case INTERPOLATOR:
      case LAYOUT:
      case MENU:
      case MIPMAP:
      case TRANSITION:
        return createFileReferenceItem(type, name, sourceFile);

      default:
        return createStringItem(type, name, sourceFile, false);
    }
  }

  @NotNull
  private BasicArrayResourceItem createArrayItem(@NotNull String name, @NotNull ResourceSourceFile sourceFile)
      throws IOException, XmlPullParserException, XmlSyntaxException {
    String indexValue = myParser.getAttributeValue(TOOLS_URI, ATTR_INDEX);
    ResourceNamespace.Resolver namespaceResolver = myParser.getNamespaceResolver();
    List values = new ArrayList<>();
    forSubTags(TAG_ITEM, () -> {
      String text = myTextExtractor.extractText(myParser, false);
      values.add(text);
    });
    int index = 0;
    if (indexValue != null) {
      try {
        index = Integer.parseUnsignedInt(indexValue);
      }
      catch (NumberFormatException e) {
        throw new XmlSyntaxException(
            "The value of the " + namespaceResolver.prefixToUri(TOOLS_URI) + ':' + ATTR_INDEX + " attribute is not a valid number.",
            myParser, getDisplayName(sourceFile));
      }
      if (index >= values.size()) {
        throw new XmlSyntaxException(
            "The value of the " + namespaceResolver.prefixToUri(TOOLS_URI) + ':' + ATTR_INDEX + " attribute is out of bounds.",
            myParser, getDisplayName(sourceFile));
      }
    }
    ResourceVisibility visibility = getVisibility(ResourceType.ARRAY, name);
    BasicArrayResourceItem item = new BasicArrayResourceItem(name, sourceFile, visibility, values, index);
    item.setNamespaceResolver(namespaceResolver);
    return item;
  }

  @NotNull
  private BasicAttrResourceItem createAttrItem(@NotNull String name, @NotNull ResourceSourceFile sourceFile)
      throws IOException, XmlPullParserException, XmlSyntaxException {
    ResourceNamespace.Resolver namespaceResolver = myParser.getNamespaceResolver();
    ResourceNamespace attrNamespace;
    myUrlParser.parseResourceUrl(name);
    if (myUrlParser.hasNamespacePrefix(ANDROID_NS_NAME)) {
      attrNamespace = ResourceNamespace.ANDROID;
    } else {
      String prefix = myUrlParser.getNamespacePrefix();
      attrNamespace = ResourceNamespace.fromNamespacePrefix(prefix, myNamespace, myParser.getNamespaceResolver());
      if (attrNamespace == null) {
        throw new XmlSyntaxException("Undefined prefix of attr resource name \"" + name + "\"", myParser, getDisplayName(sourceFile));
      }
    }
    name = myUrlParser.getName();

    String description = myParser.getLastComment();
    String groupName = myParser.getAttrGroupComment();
    String formatString = myParser.getAttributeValue(null, ATTR_FORMAT);
    Set formats =
      StringUtil.isEmpty(formatString) ? EnumSet.noneOf(AttributeFormat.class) : AttributeFormat.parse(formatString);

    // The average number of enum or flag values is 7 for Android framework, so start with small maps.
    Map valueMap = Maps.newHashMapWithExpectedSize(8);
    Map descriptionMap = Maps.newHashMapWithExpectedSize(8);
    forSubTags(null, () -> {
      if (myParser.getPrefix() == null) {
        String tagName = myParser.getName();
        AttributeFormat format =
            tagName.equals(TAG_ENUM) ? AttributeFormat.ENUM : tagName.equals(TAG_FLAG) ? AttributeFormat.FLAGS : null;
        if (format != null) {
          formats.add(format);
          String valueName = myParser.getAttributeValue(null, ATTR_NAME);
          if (valueName != null) {
            String valueDescription = myParser.getLastComment();
            if (valueDescription != null) {
              descriptionMap.put(valueName, valueDescription);
            }
            String value = myParser.getAttributeValue(null, ATTR_VALUE);
            Integer numericValue = null;
            if (value != null) {
              try {
                // Integer.decode/parseInt can't deal with hex value > 0x7FFFFFFF so we use Long.decode instead.
                numericValue = Long.decode(value).intValue();
              }
              catch (NumberFormatException ignored) {
              }
            }
            valueMap.put(valueName, numericValue);
          }
        }
      }
    });

    BasicAttrResourceItem item;
    if (attrNamespace.equals(myNamespace)) {
      ResourceVisibility visibility = getVisibility(ResourceType.ATTR, name);
      item = new BasicAttrResourceItem(name, sourceFile, visibility, description, groupName, formats, valueMap, descriptionMap);
    }
    else {
      item = new BasicForeignAttrResourceItem(attrNamespace, name, sourceFile, description, groupName, formats, valueMap, descriptionMap);
    }

    item.setNamespaceResolver(namespaceResolver);
    return item;
  }

  @NotNull
  private BasicPluralsResourceItem createPluralsItem(@NotNull String name, @NotNull ResourceSourceFile sourceFile)
      throws IOException, XmlPullParserException, XmlSyntaxException {
    String defaultQuantity = myParser.getAttributeValue(TOOLS_URI, ATTR_QUANTITY);
    ResourceNamespace.Resolver namespaceResolver = myParser.getNamespaceResolver();
    EnumMap values = new EnumMap<>(Arity.class);
    forSubTags(TAG_ITEM, () -> {
      String quantityValue = myParser.getAttributeValue(null, ATTR_QUANTITY);
      if (quantityValue != null) {
        Arity quantity = Arity.getEnum(quantityValue);
        if (quantity != null) {
          String text = myTextExtractor.extractText(myParser, false);
          values.put(quantity, text);
        }
      }
    });
    Arity defaultArity = null;
    if (defaultQuantity != null) {
      defaultArity = Arity.getEnum(defaultQuantity);
      if (defaultArity == null || !values.containsKey(defaultArity)) {
        throw new XmlSyntaxException(
            "Invalid value of the " + namespaceResolver.prefixToUri(TOOLS_URI) + ':' + ATTR_QUANTITY + " attribute.", myParser,
            getDisplayName(sourceFile));
      }
    }
    ResourceVisibility visibility = getVisibility(ResourceType.PLURALS, name);
    BasicPluralsResourceItem item = new BasicPluralsResourceItem(name, sourceFile, visibility, values, defaultArity);
    item.setNamespaceResolver(namespaceResolver);
    return item;
  }

  @NotNull
  private BasicValueResourceItem createStringItem(
      @NotNull ResourceType type, @NotNull String name, @NotNull ResourceSourceFile sourceFile, boolean withRowXml)
      throws IOException, XmlPullParserException {
    ResourceNamespace.Resolver namespaceResolver = myParser.getNamespaceResolver();
    String text = type == ResourceType.ID ? null : myTextExtractor.extractText(myParser, withRowXml);
    String rawXml = type == ResourceType.ID ? null : myTextExtractor.getRawXml();
    assert withRowXml || rawXml == null; // Text extractor doesn't extract raw XML unless asked to do it.
    ResourceVisibility visibility = getVisibility(type, name);
    BasicValueResourceItem item = rawXml == null ?
                                  new BasicValueResourceItem(type, name, sourceFile, visibility, text) :
                                  new BasicTextValueResourceItem(type, name, sourceFile, visibility, text, rawXml);
    item.setNamespaceResolver(namespaceResolver);
    return item;
  }

  @NotNull
  private BasicStyleResourceItem createStyleItem(@NotNull String name, @NotNull ResourceSourceFile sourceFile)
      throws IOException, XmlPullParserException {
    ResourceNamespace.Resolver namespaceResolver = myParser.getNamespaceResolver();
    String parentStyle = myParser.getAttributeValue(null, ATTR_PARENT);
    if (parentStyle != null && !parentStyle.isEmpty()) {
      myUrlParser.parseResourceUrl(parentStyle);
      parentStyle = myUrlParser.getQualifiedName();
    }
    List styleItems = new ArrayList<>();
    forSubTags(TAG_ITEM, () -> {
      ResourceNamespace.Resolver itemNamespaceResolver = myParser.getNamespaceResolver();
      String itemName = myParser.getAttributeValue(null, ATTR_NAME);
      if (itemName != null) {
        String text = myTextExtractor.extractText(myParser, false);
        StyleItemResourceValueImpl styleItem =
            new StyleItemResourceValueImpl(myNamespace, itemName, text, sourceFile.getRepository().getLibraryName());
        styleItem.setNamespaceResolver(itemNamespaceResolver);
        styleItems.add(styleItem);
      }
    });
    ResourceVisibility visibility = getVisibility(ResourceType.STYLE, name);
    BasicStyleResourceItem item = new BasicStyleResourceItem(name, sourceFile, visibility, parentStyle, styleItems);
    item.setNamespaceResolver(namespaceResolver);
    return item;
  }

  @NotNull
  private BasicStyleableResourceItem createStyleableItem(@NotNull String name, @NotNull ResourceSourceFile sourceFile)
      throws IOException, XmlPullParserException {
    ResourceNamespace.Resolver namespaceResolver = myParser.getNamespaceResolver();
    List attrs = new ArrayList<>();
    forSubTags(TAG_ATTR, () -> {
      String attrName = myParser.getAttributeValue(null, ATTR_NAME);
      if (attrName != null) {
        try {
          BasicAttrResourceItem attr = createAttrItem(attrName, sourceFile);
          // Mimic behavior of AAPT2 and put an attr reference inside a styleable resource.
          attrs.add(attr.getFormats().isEmpty() ? attr : attr.createReference());

          // Don't create top-level attr resources in a foreign namespace, or for attr references in the res-auto namespace.
          // The second condition is determined by the fact that the attr in the res-auto namespace may have an explicit definition
          // outside of this resource repository.
          if (attr.getNamespace().equals(myNamespace) && (myNamespace != ResourceNamespace.RES_AUTO || !attr.getFormats().isEmpty())) {
            addAttr(attr, myAttrCandidates);
          }
        }
        catch (XmlSyntaxException e) {
          LOG.error(e);
        }
      }
    });
    // AAPT2 treats all styleable resources as public.
    // See https://android.googlesource.com/platform/frameworks/base/+/master/tools/aapt2/ResourceParser.cpp#1539
    BasicStyleableResourceItem item = new BasicStyleableResourceItem(name, sourceFile, ResourceVisibility.PUBLIC, attrs);
    item.setNamespaceResolver(namespaceResolver);
    return item;
  }

  private static void addAttr(@NotNull BasicAttrResourceItem attr, @NotNull ListMultimap map) {
    List attrs = map.get(attr.getName());
    int i = findResourceWithSameNameAndConfiguration(attr, attrs);
    if (i >= 0) {
      // Found a matching attr definition.
      BasicAttrResourceItem existing = attrs.get(i);
      if (!attr.getFormats().isEmpty()) {
        if (existing.getFormats().isEmpty()) {
          attrs.set(i, attr); // Use the new attr since it contains more information than the existing one.
        }
        else if (!attr.getFormats().equals(existing.getFormats())) {
          // Both, the existing and the new attr contain formats, but they are not the same.
          // Assign union of formats to both attr definitions.
          if (attr.getFormats().containsAll(existing.getFormats())) {
            existing.setFormats(attr.getFormats());
          }
          else if (existing.getFormats().containsAll(attr.getFormats())) {
            attr.setFormats(existing.getFormats());
          }
          else {
            Set formats = EnumSet.copyOf(attr.getFormats());
            formats.addAll(existing.getFormats());
            formats = ImmutableSet.copyOf(formats);
            attr.setFormats(formats);
            existing.setFormats(formats);
          }
        }
      }
      if (existing.getFormats().isEmpty() && !attr.getFormats().isEmpty()) {
        attrs.set(i, attr); // Use the new attr since it contains more information than the existing one.
      }
    }
    else {
      attrs.add(attr);
    }
  }

  /**
   * Adds attr definitions from {@link #myAttrs}, and attr definition candidates from {@link #myAttrCandidates}
   * if they don't match the attr definitions present in {@link #myAttrs}.
   */
  private void processAttrsAndStyleables() {
    for (BasicAttrResourceItem attr : myAttrs.values()) {
      addAttrWithAdjustedFormats(attr);
    }

    for (BasicAttrResourceItem attr : myAttrCandidates.values()) {
      List attrs = myAttrs.get(attr.getName());
      int i = findResourceWithSameNameAndConfiguration(attr, attrs);
      if (i < 0) {
        addAttrWithAdjustedFormats(attr);
      }
    }

    // Resolve attribute references where it can be done without loosing any data to reduce resource memory footprint.
    for (BasicStyleableResourceItem styleable : myStyleables.values()) {
      addResourceItem(resolveAttrReferences(styleable));
    }
  }

  /**
   * Returns a styleable with attr references replaced by attr definitions returned by
   * the {@link BasicStyleableResourceItem#getCanonicalAttr} method.
   */
  @NotNull
  public static BasicStyleableResourceItem resolveAttrReferences(@NotNull BasicStyleableResourceItem styleable) {
    ResourceRepository repository = styleable.getRepository();
    List attributes = styleable.getAllAttributes();
    List resolvedAttributes = null;
    for (int i = 0; i < attributes.size(); i++) {
      AttrResourceValue attr = attributes.get(i);
      AttrResourceValue canonicalAttr = BasicStyleableResourceItem.getCanonicalAttr(attr, repository);
      if (canonicalAttr != attr) {
        if (resolvedAttributes == null) {
          resolvedAttributes = new ArrayList<>(attributes.size());
          for (int j = 0; j < i; j++) {
            resolvedAttributes.add(attributes.get(j));
          }
        }
        resolvedAttributes.add(canonicalAttr);
      }
      else if (resolvedAttributes != null) {
        resolvedAttributes.add(attr);
      }
    }

    if (resolvedAttributes != null) {
      ResourceNamespace.Resolver namespaceResolver = styleable.getNamespaceResolver();
      styleable =
          new BasicStyleableResourceItem(styleable.getName(), styleable.getSourceFile(), styleable.getVisibility(), resolvedAttributes);
      styleable.setNamespaceResolver(namespaceResolver);
    }
    return styleable;
  }

  private void addAttrWithAdjustedFormats(@NotNull BasicAttrResourceItem attr) {
    if (attr.getFormats().isEmpty()) {
      attr = new BasicAttrResourceItem(attr.getName(), attr.getSourceFile(), attr.getVisibility(), attr.getDescription(),
                                       attr.getGroupName(), DEFAULT_ATTR_FORMATS, Collections.emptyMap(), Collections.emptyMap());
    }
    addResourceItem(attr);
  }

  /**
   * Checks if resource with the same name, type and configuration has already been defined.
   *
   * @param resource the resource to check
   * @return true if a matching resource already exists
   */
  private static boolean resourceAlreadyDefined(@NotNull BasicResourceItemBase resource) {
    ResourceRepository repository = resource.getRepository();
    List items = repository.getResources(resource.getNamespace(), resource.getType(), resource.getName());
    return findResourceWithSameNameAndConfiguration(resource, items) >= 0;
  }

  private static int findResourceWithSameNameAndConfiguration(@NotNull ResourceItem resource, @NotNull List items) {
    for (int i = 0; i < items.size(); i++) {
      ResourceItem item = items.get(i);
      if (item.getConfiguration().equals(resource.getConfiguration())) {
        return i;
      }
    }
    return -1;
  }

  @NotNull
  private BasicValueResourceItem createFileReferenceItem(
      @NotNull ResourceType type, @NotNull String name, @NotNull ResourceSourceFile sourceFile)
      throws IOException, XmlPullParserException {
    ResourceNamespace.Resolver namespaceResolver = myParser.getNamespaceResolver();
    String text = myTextExtractor.extractText(myParser, false).trim();
    if (!text.isEmpty() && !text.startsWith(PREFIX_RESOURCE_REF) && !text.startsWith(PREFIX_THEME_REF)) {
      text = text.replace('/', File.separatorChar);
    }
    ResourceVisibility visibility = getVisibility(type, name);
    BasicValueResourceItem item = new BasicValueResourceItem(type, name, sourceFile, visibility, text);
    item.setNamespaceResolver(namespaceResolver);
    return item;
  }

  @Nullable
  private ResourceType getResourceType(@NotNull String tagName, @NotNull PathString file) throws XmlSyntaxException {
    ResourceType type = ResourceType.fromXmlTagName(tagName);

    if (type == null) {
      if (TAG_EAT_COMMENT.equals(tagName) || TAG_SKIP.equals(tagName)) {
        return null;
      }

      if ("java-symbol".equals(tagName)) {
        // java-symbol is only used within framework and does not provide any public
        // information so we can safely ignore it.
        return null;
      }

      if (tagName.equals(TAG_ITEM)) {
        String typeAttr = myParser.getAttributeValue(null, ATTR_TYPE);
        if (typeAttr != null) {
          type = ResourceType.fromClassName(typeAttr);
          if (type != null) {
            return type;
          }

          LOG.warn("Unrecognized type attribute \"" + typeAttr + "\" at " + getDisplayName(file) + " line " + myParser.getLineNumber());
        }
      }
      else {
        LOG.warn("Unrecognized tag name \"" + tagName + "\" at " + getDisplayName(file) + " line " + myParser.getLineNumber());
      }
    }

    return type;
  }

  /**
   * If {@code tagName} is null, calls {@code subtagVisitor.visitTag()} for every subtag of the current tag.
   * If {@code tagName} is not null, calls {@code subtagVisitor.visitTag()} for every subtag of the current tag
   * which name doesn't have a prefix and matches {@code tagName}.
   */
  private void forSubTags(@Nullable String tagName, @NotNull XmlTagVisitor subtagVisitor) throws IOException, XmlPullParserException {
    int elementDepth = myParser.getDepth();
    int event;
    do {
      event = myParser.nextToken();
      if (event == XmlPullParser.START_TAG && (tagName == null || tagName.equals(myParser.getName()) && myParser.getPrefix() == null)) {
        subtagVisitor.visitTag();
      }
    } while (event != XmlPullParser.END_DOCUMENT && (event != XmlPullParser.END_TAG || myParser.getDepth() > elementDepth));
  }

  /**
   * Skips all subtags of the current tag. When the method returns, the parser is positioned at the end tag
   * of the current element.
   */
  private void skipSubTags() throws IOException, XmlPullParserException {
    int elementDepth = myParser.getDepth();
    int event;
    do {
      event = myParser.nextToken();
    } while (event != XmlPullParser.END_DOCUMENT && (event != XmlPullParser.END_TAG || myParser.getDepth() > elementDepth));
  }

  private void validateResourceName(@NotNull String resourceName, @NotNull ResourceType resourceType, @NotNull PathString file)
      throws XmlSyntaxException {
    String error = ValueResourceNameValidator.getErrorText(resourceName, resourceType);
    if (error != null) {
      throw new XmlSyntaxException(error, myParser, getDisplayName(file));
    }
  }

  @NotNull
  private String getDisplayName(@NotNull PathString file) {
    return file.isAbsolute() ? file.getNativePath() : file.getPortablePath() + " in " + myResourceDirectoryOrFile.toString();
  }

  @NotNull
  private String getDisplayName(@NotNull ResourceSourceFile sourceFile) {
    String relativePath = sourceFile.getRelativePath();
    Preconditions.checkArgument(relativePath != null);
    return getDisplayName(new PathString(relativePath));
  }

  @NotNull
  protected final ResourceVisibility getVisibility(@NotNull ResourceType resourceType, @NotNull String resourceName) {
    Set names = myPublicResources.get(resourceType);
    return names != null && names.contains(getKeyForVisibilityLookup(resourceName)) ? ResourceVisibility.PUBLIC : myDefaultVisibility;
  }

  /**
   * Transforms the given resource name to a key for lookup in myPublicResources.
   */
  @NotNull
  protected String getKeyForVisibilityLookup(@NotNull String resourceName) {
    // In public.txt all resource names are transformed by replacing dots, colons and dashes with underscores.
    return ResourcesUtil.resourceNameToFieldName(resourceName);
  }

  @NotNull
  protected final String getResRelativePath(@NotNull PathString file) {
    if (file.isAbsolute()) {
      return myResourceDirectoryOrFilePath.relativize(file).getPortablePath();
    }

    // The path is already relative, drop the first "res" segment.
    assert file.getNameCount() != 0;
    assert file.segment(0).equals("res");
    return file.subpath(1, file.getNameCount()).getPortablePath();
  }

  private static boolean isZipArchive(@NotNull Path resourceDirectoryOrFile) {
    String filename = resourceDirectoryOrFile.getFileName().toString();
    return SdkUtils.endsWithIgnoreCase(filename, DOT_AAR) ||
           SdkUtils.endsWithIgnoreCase(filename, DOT_JAR) ||
           SdkUtils.endsWithIgnoreCase(filename, DOT_ZIP);
  }

  @NotNull
  public static String portableFileName(@NotNull String fileName) {
    return fileName.replace(File.separatorChar, '/');
  }

  private interface XmlTagVisitor {
    /** Is called when the parser is positioned at a {@link XmlPullParser#START_TAG}. */
    void visitTag() throws IOException, XmlPullParserException;
  }

  /**
   * Information about a resource folder.
   */
  protected static class FolderInfo {
    @NotNull public final ResourceFolderType folderType;
    @NotNull public final FolderConfiguration configuration;
    @Nullable public final ResourceType resourceType;
    public final boolean isIdGenerating;

    private FolderInfo(@NotNull ResourceFolderType folderType,
                       @NotNull FolderConfiguration configuration,
                       @Nullable ResourceType resourceType,
                       boolean isIdGenerating) {
      this.configuration = configuration;
      this.resourceType = resourceType;
      this.folderType = folderType;
      this.isIdGenerating = isIdGenerating;
    }

    /**
     * Returns a FolderInfo for the given folder name.
     *
     * @param folderName the name of a resource folder
     * @param folderConfigCache the cache of FolderConfiguration objects keyed by qualifier strings
     * @return the FolderInfo object, or null if folderName is not a valid name of a resource folder
     */
    @Nullable
    public static FolderInfo create(@NotNull String folderName, @NotNull Map folderConfigCache) {
      ResourceFolderType folderType = ResourceFolderType.getFolderType(folderName);
      if (folderType == null) {
        return null;
      }

      String qualifier = FolderConfiguration.getQualifier(folderName);
      FolderConfiguration config = folderConfigCache.computeIfAbsent(qualifier, FolderConfiguration::getConfigForQualifierString);
      if (config == null) {
        return null;
      }
      config.normalizeByRemovingRedundantVersionQualifier();

      ResourceType resourceType;
      boolean isIdGenerating;
      if (folderType == ResourceFolderType.VALUES) {
        resourceType = null;
        isIdGenerating = false;
      }
      else {
        resourceType = FolderTypeRelationship.getNonIdRelatedResourceType(folderType);
        isIdGenerating = FolderTypeRelationship.isIdGeneratingFolderType(folderType);
      }

      return new FolderInfo(folderType, config, resourceType, isIdGenerating);
    }
  }

  private static class ResourceFileCollector implements FileVisitor {
    @NotNull final List resourceFiles = new ArrayList<>();
    @NotNull final List ioErrors = new ArrayList<>();
    @NotNull final FileFilter fileFilter;

    private ResourceFileCollector(@NotNull FileFilter filter) {
      fileFilter = filter;
    }

    @Override
    @NotNull
    public FileVisitResult preVisitDirectory(@NotNull Path dir, @NotNull BasicFileAttributes attrs) {
      if (fileFilter.isIgnored(dir, attrs)) {
        return FileVisitResult.SKIP_SUBTREE;
      }
      return FileVisitResult.CONTINUE;
    }

    @Override
    @NotNull
    public FileVisitResult visitFile(@NotNull Path file, @NotNull BasicFileAttributes attrs) {
      if (fileFilter.isIgnored(file, attrs)) {
        return FileVisitResult.SKIP_SUBTREE;
      }
      resourceFiles.add(new PathString(file));
      return FileVisitResult.CONTINUE;
    }

    @Override
    @NotNull
    public FileVisitResult visitFileFailed(@NotNull Path file, @NotNull IOException exc) {
      ioErrors.add(exc);
      return FileVisitResult.CONTINUE;
    }

    @Override
    @NotNull
    public FileVisitResult postVisitDirectory(@NotNull Path dir, @Nullable IOException exc) {
      return FileVisitResult.CONTINUE;
    }
  }

  private static class XmlTextExtractor {
    @NotNull private final StringBuilder text = new StringBuilder();
    @NotNull private final StringBuilder rawXml = new StringBuilder();
    @NotNull private final Deque textInclusionState = new ArrayDeque<>();
    private boolean nontrivialRawXml;

    @NotNull
    String extractText(@NotNull XmlPullParser parser, boolean withRawXml) throws IOException, XmlPullParserException {
      text.setLength(0);
      rawXml.setLength(0);
      textInclusionState.clear();
      nontrivialRawXml = false;

      int elementDepth = parser.getDepth();
      int event;
      loop:
      do {
        event = parser.nextToken();
        switch (event) {
          case XmlPullParser.START_TAG: {
            String tagName = parser.getName();
            if (XLIFF_G_TAG.equals(tagName) && isXliffNamespace(parser.getNamespace())) {
              boolean includeNestedText = getTextInclusionState();
              String example = parser.getAttributeValue(null, ATTR_EXAMPLE);
              if (example != null) {
                text.append('(').append(example).append(')');
                includeNestedText = false;
              }
              else {
                String id = parser.getAttributeValue(null, ATTR_ID);
                if (id != null && !id.equals("id")) {
                  text.append('$').append('{').append(id).append('}');
                  includeNestedText = false;
                }
              }
              textInclusionState.addLast(includeNestedText);
            }
            if (withRawXml) {
              nontrivialRawXml = true;
              rawXml.append('<');
              String prefix = parser.getPrefix();
              if (prefix != null) {
                rawXml.append(prefix).append(':');
              }
              rawXml.append(tagName);
              int numAttr = parser.getAttributeCount();
              for (int i = 0; i < numAttr; i++) {
                rawXml.append(' ');
                String attributePrefix = parser.getAttributePrefix(i);
                if (attributePrefix != null) {
                  rawXml.append(attributePrefix).append(':');
                }
                rawXml.append(parser.getAttributeName(i)).append('=').append('"');
                XmlUtils.appendXmlAttributeValue(rawXml, parser.getAttributeValue(i));
                rawXml.append('"');
              }
              rawXml.append('>');
            }
            break;
          }

          case XmlPullParser.END_TAG: {
            if (parser.getDepth() <= elementDepth) {
              break loop;
            }
            String tagName = parser.getName();
            if (withRawXml) {
              rawXml.append('<').append('/');
              String prefix = parser.getPrefix();
              if (prefix != null) {
                rawXml.append(prefix).append(':');
              }
              rawXml.append(tagName).append('>');
            }
            if (XLIFF_G_TAG.equals(tagName) && isXliffNamespace(parser.getNamespace())) {
              textInclusionState.removeLast();
            }
            break;
          }

          case XmlPullParser.ENTITY_REF:
          case XmlPullParser.TEXT: {
            String textPiece = parser.getText();
            if (getTextInclusionState()) {
              text.append(textPiece);
            }
            if (withRawXml) {
              rawXml.append(textPiece);
            }
            break;
          }

          case XmlPullParser.CDSECT: {
            String textPiece = parser.getText();
            if (getTextInclusionState()) {
              text.append(textPiece);
            }
            if (withRawXml) {
              nontrivialRawXml = true;
              rawXml.append("");
            }
            break;
          }
        }
      } while (event != XmlPullParser.END_DOCUMENT);

      return ValueXmlHelper.unescapeResourceString(text.toString(), false, true);
    }

    private boolean getTextInclusionState() {
      return textInclusionState.isEmpty() || textInclusionState.getLast();
    }

    @Nullable
    String getRawXml() {
      return nontrivialRawXml ? rawXml.toString() : null;
    }

    private static boolean isXliffNamespace(@Nullable String namespaceUri) {
      return namespaceUri != null && namespaceUri.startsWith(XLIFF_NAMESPACE_PREFIX);
    }
  }

  private static class XmlSyntaxException extends Exception {
    XmlSyntaxException(@NotNull String error, @NotNull XmlPullParser parser, @NotNull String filename) {
      super(error + " at " + filename + " line " + parser.getLineNumber());
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy