| /* |
| * 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.ide.common.res2; |
| |
| import static com.android.SdkConstants.ATTR_NAME; |
| import static com.android.SdkConstants.ATTR_TYPE; |
| import static com.android.SdkConstants.DOT_9PNG; |
| import static com.android.SdkConstants.DOT_PNG; |
| import static com.android.SdkConstants.DOT_XML; |
| import static com.android.SdkConstants.RES_QUALIFIER_SEP; |
| import static com.android.SdkConstants.TAG_EAT_COMMENT; |
| import static com.android.SdkConstants.TAG_RESOURCES; |
| import static com.android.utils.SdkUtils.createPathComment; |
| import static com.google.common.base.Preconditions.checkState; |
| |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.ide.common.internal.PngCruncher; |
| import com.android.ide.common.internal.PngException; |
| import com.android.resources.ResourceFolderType; |
| import com.android.resources.ResourceType; |
| import com.android.utils.SdkUtils; |
| import com.android.utils.XmlUtils; |
| import com.google.common.base.Charsets; |
| import com.google.common.collect.ArrayListMultimap; |
| import com.google.common.collect.ListMultimap; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Sets; |
| import com.google.common.io.Files; |
| |
| import org.w3c.dom.Document; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.Node; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.concurrent.Callable; |
| |
| import javax.xml.parsers.DocumentBuilder; |
| import javax.xml.parsers.DocumentBuilderFactory; |
| |
| /** |
| * A {@link MergeWriter} for assets, using {@link ResourceItem}. |
| */ |
| public class MergedResourceWriter extends MergeWriter<ResourceItem> { |
| |
| @NonNull |
| private final PngCruncher mCruncher; |
| |
| @NonNull |
| private final ResourcePreprocessor mPreprocessor; |
| |
| /** |
| * If non-null, points to a File that we should write public.txt to |
| */ |
| private final File mPublicFile; |
| |
| private DocumentBuilderFactory mFactory; |
| |
| private boolean mInsertSourceMarkers = true; |
| |
| private final boolean mCrunchPng; |
| |
| private final boolean mProcess9Patch; |
| |
| private final int mCruncherKey; |
| |
| /** |
| * map of XML values files to write after parsing all the files. the key is the qualifier. |
| */ |
| private ListMultimap<String, ResourceItem> mValuesResMap; |
| |
| /** |
| * Set of qualifier that had a previously written resource now gone. This is to keep a list of |
| * values files that must be written out even with no touched or updated resources, in case one |
| * or more resources were removed. |
| */ |
| private Set<String> mQualifierWithDeletedValues; |
| |
| public MergedResourceWriter(@NonNull File rootFolder, |
| @NonNull PngCruncher pngRunner, |
| boolean crunchPng, |
| boolean process9Patch, |
| @Nullable File publicFile, |
| @NonNull ResourcePreprocessor preprocessor) { |
| super(rootFolder); |
| mCruncher = pngRunner; |
| mCruncherKey = mCruncher.start(); |
| mCrunchPng = crunchPng; |
| mProcess9Patch = process9Patch; |
| mPublicFile = publicFile; |
| mPreprocessor = preprocessor; |
| } |
| |
| /** |
| * Sets whether this manifest merger will insert source markers into the merged source |
| * |
| * @param insertSourceMarkers if true, insert source markers |
| */ |
| public void setInsertSourceMarkers(boolean insertSourceMarkers) { |
| mInsertSourceMarkers = insertSourceMarkers; |
| } |
| |
| /** |
| * Returns whether this manifest merger will insert source markers into the merged source |
| * |
| * @return whether this manifest merger will insert source markers into the merged source |
| */ |
| public boolean isInsertSourceMarkers() { |
| return mInsertSourceMarkers; |
| } |
| |
| @Override |
| public void start(@NonNull DocumentBuilderFactory factory) throws ConsumerException { |
| super.start(factory); |
| mValuesResMap = ArrayListMultimap.create(); |
| mQualifierWithDeletedValues = Sets.newHashSet(); |
| mFactory = factory; |
| } |
| |
| @Override |
| public void end() throws ConsumerException { |
| // Make sure all PNGs are generated first. |
| super.end(); |
| try { |
| // Wait for all PNGs to be crunched. |
| mCruncher.end(mCruncherKey); |
| } catch (InterruptedException e) { |
| throw new ConsumerException(e); |
| } |
| |
| mValuesResMap = null; |
| mQualifierWithDeletedValues = null; |
| mFactory = null; |
| } |
| |
| @Override |
| public boolean ignoreItemInMerge(ResourceItem item) { |
| return item.getIgnoredFromDiskMerge(); |
| } |
| |
| @Override |
| public void addItem(@NonNull final ResourceItem item) throws ConsumerException { |
| final ResourceFile.FileType type = item.getSourceType(); |
| |
| if (type == ResourceFile.FileType.XML_VALUES) { |
| // this is a resource for the values files |
| |
| // just add the node to write to the map based on the qualifier. |
| // We'll figure out later if the files needs to be written or (not) |
| mValuesResMap.put(item.getQualifiers(), item); |
| } else { |
| checkState(item.getSource() != null); |
| // This is a single value file or a set of generated files. Only write it if the state |
| // is TOUCHED. |
| if (item.isTouched()) { |
| getExecutor().execute(new Callable<Void>() { |
| @Override |
| public Void call() throws Exception { |
| File file = item.getFile(); |
| |
| String filename = file.getName(); |
| String folderName = getFolderName(item); |
| File typeFolder = new File(getRootFolder(), folderName); |
| try { |
| createDir(typeFolder); |
| } catch (IOException ioe) { |
| throw MergingException.wrapException(ioe).withFile(typeFolder).build(); |
| } |
| |
| File outFile = new File(typeFolder, filename); |
| |
| if (type == DataFile.FileType.GENERATED_FILES) { |
| mPreprocessor.generateFile(file, item.getSource().getFile()); |
| } |
| |
| try { |
| if (item.getType() == ResourceType.RAW) { |
| // Don't crunch, don't insert source comments, etc - leave alone. |
| Files.copy(file, outFile); |
| } else if (filename.endsWith(DOT_PNG)) { |
| if (mCrunchPng && mProcess9Patch) { |
| mCruncher.crunchPng(mCruncherKey, file, outFile); |
| } else { |
| // we should not crunch the png files, but we should still |
| // process the nine patch. |
| if (mProcess9Patch && filename.endsWith(DOT_9PNG)) { |
| mCruncher.crunchPng(mCruncherKey, file, outFile); |
| } else { |
| Files.copy(file, outFile); |
| } |
| } |
| } else if (mInsertSourceMarkers && filename.endsWith(DOT_XML)) { |
| SdkUtils.copyXmlWithSourceReference(file, outFile); |
| } else { |
| Files.copy(file, outFile); |
| } |
| } catch (PngException e) { |
| throw MergingException.wrapException(e).withFile(file).build(); |
| } catch (IOException ioe) { |
| throw MergingException.wrapException(ioe).withFile(file).build(); |
| } |
| return null; |
| } |
| }); |
| } |
| } |
| } |
| |
| @Override |
| public void removeItem(@NonNull ResourceItem removedItem, @Nullable ResourceItem replacedBy) |
| throws ConsumerException { |
| ResourceFile.FileType removedType = removedItem.getSourceType(); |
| ResourceFile.FileType replacedType = replacedBy != null |
| ? replacedBy.getSourceType() |
| : null; |
| |
| switch (removedType) { |
| case SINGLE_FILE: // Fall through. |
| case GENERATED_FILES: |
| if (replacedType == DataFile.FileType.SINGLE_FILE |
| || replacedType == DataFile.FileType.GENERATED_FILES) { |
| // Save one IO operation and don't delete a file that will be overwritten |
| // anyway. |
| break; |
| } |
| removeOutFile(removedItem); |
| break; |
| case XML_VALUES: |
| mQualifierWithDeletedValues.add(removedItem.getQualifiers()); |
| break; |
| default: |
| throw new IllegalStateException(); |
| } |
| } |
| |
| @Override |
| protected void postWriteAction() throws ConsumerException { |
| |
| // now write the values files. |
| for (String key : mValuesResMap.keySet()) { |
| // the key is the qualifier. |
| |
| // check if we have to write the file due to deleted values. |
| // also remove it from that list anyway (to detect empty qualifiers later). |
| boolean mustWriteFile = mQualifierWithDeletedValues.remove(key); |
| |
| // get the list of items to write |
| List<ResourceItem> items = mValuesResMap.get(key); |
| |
| // now check if we really have to write it |
| if (!mustWriteFile) { |
| for (ResourceItem item : items) { |
| if (item.isTouched()) { |
| mustWriteFile = true; |
| break; |
| } |
| } |
| } |
| |
| if (mustWriteFile) { |
| String folderName = key.isEmpty() ? |
| ResourceFolderType.VALUES.getName() : |
| ResourceFolderType.VALUES.getName() + RES_QUALIFIER_SEP + key; |
| |
| File valuesFolder = new File(getRootFolder(), folderName); |
| // Name of the file is the same as the folder as AAPT gets confused with name |
| // collision when not normalizing folders name. |
| File outFile = new File(valuesFolder, folderName + DOT_XML); |
| ResourceFile currentFile = null; |
| try { |
| createDir(valuesFolder); |
| |
| DocumentBuilder builder = mFactory.newDocumentBuilder(); |
| Document document = builder.newDocument(); |
| final String publicTag = ResourceType.PUBLIC.getName(); |
| List<Node> publicNodes = null; |
| |
| Node rootNode = document.createElement(TAG_RESOURCES); |
| document.appendChild(rootNode); |
| |
| Collections.sort(items); |
| |
| for (ResourceItem item : items) { |
| Node nodeValue = item.getValue(); |
| if (nodeValue != null && publicTag.equals(nodeValue.getNodeName())) { |
| if (publicNodes == null) { |
| publicNodes = Lists.newArrayList(); |
| } |
| publicNodes.add(nodeValue); |
| continue; |
| } |
| |
| // add a carriage return so that the nodes are not all on the same line. |
| // also add an indent of 4 spaces. |
| rootNode.appendChild(document.createTextNode("\n ")); |
| |
| ResourceFile source = item.getSource(); |
| if (source != currentFile && source != null && mInsertSourceMarkers) { |
| currentFile = source; |
| File file = source.getFile(); |
| rootNode.appendChild(document.createComment( |
| createPathComment(file, true))); |
| rootNode.appendChild(document.createTextNode("\n ")); |
| // Add an <eat-comment> element to ensure that this comment won't |
| // get merged into a potential comment from the next child (or |
| // even added as the sole comment in the R class) |
| rootNode.appendChild(document.createElement(TAG_EAT_COMMENT)); |
| rootNode.appendChild(document.createTextNode("\n ")); |
| } |
| |
| Node adoptedNode = NodeUtils.adoptNode(document, nodeValue); |
| rootNode.appendChild(adoptedNode); |
| } |
| |
| // finish with a carriage return |
| rootNode.appendChild(document.createTextNode("\n")); |
| |
| currentFile = null; |
| |
| String content = XmlUtils.toXml(document); |
| Files.write(content, outFile, Charsets.UTF_8); |
| |
| if (publicNodes != null && mPublicFile != null) { |
| // Generate public.txt: |
| int size = publicNodes.size(); |
| StringBuilder sb = new StringBuilder(size * 80); |
| for (Node node : publicNodes) { |
| if (node.getNodeType() == Node.ELEMENT_NODE) { |
| Element element = (Element) node; |
| String name = element.getAttribute(ATTR_NAME); |
| String type = element.getAttribute(ATTR_TYPE); |
| if (!name.isEmpty() && !type.isEmpty()) { |
| sb.append(type).append(' ').append(name).append('\n'); |
| } |
| } |
| } |
| File parentFile = mPublicFile.getParentFile(); |
| if (!parentFile.exists()) { |
| boolean mkdirs = parentFile.mkdirs(); |
| if (!mkdirs) { |
| throw new IOException("Could not create " + parentFile); |
| } |
| } |
| String text = sb.toString(); |
| Files.write(text, mPublicFile, Charsets.UTF_8); |
| } |
| } catch (Throwable t) { |
| ConsumerException exception = new ConsumerException(t, |
| currentFile != null ? currentFile.getFile() : outFile); |
| throw exception; |
| } |
| } |
| } |
| |
| // now remove empty values files. |
| for (String key : mQualifierWithDeletedValues) { |
| String folderName = key != null && !key.isEmpty() ? |
| ResourceFolderType.VALUES.getName() + RES_QUALIFIER_SEP + key : |
| ResourceFolderType.VALUES.getName(); |
| |
| removeOutFile(folderName, folderName + DOT_XML); |
| } |
| } |
| |
| /** |
| * Removes a file that already exists in the out res folder. This has to be a non value file. |
| * |
| * @param resourceItem the source item that created the file to remove. |
| * @return true if success. |
| */ |
| private boolean removeOutFile(ResourceItem resourceItem) { |
| return removeOutFile(getFolderName(resourceItem), resourceItem.getFile().getName()); |
| } |
| |
| /** |
| * Removes a file from a folder based on a sub folder name and a filename |
| * |
| * @param folderName the sub folder name |
| * @param fileName the file name. |
| * @return true if success. |
| */ |
| private boolean removeOutFile(String folderName, String fileName) { |
| File valuesFolder = new File(getRootFolder(), folderName); |
| File outFile = new File(valuesFolder, fileName); |
| return outFile.delete(); |
| } |
| |
| private synchronized void createDir(File folder) throws IOException { |
| if (!folder.isDirectory() && !folder.mkdirs()) { |
| throw new IOException("Failed to create directory: " + folder); |
| } |
| } |
| |
| /** |
| * Calculates the right folder name give a resource item. |
| * |
| * @param resourceItem the resource item to calculate the folder name from. |
| * @return a relative folder name |
| */ |
| @NonNull |
| private static String getFolderName(ResourceItem resourceItem) { |
| ResourceType itemType = resourceItem.getType(); |
| String folderName = itemType.getName(); |
| String qualifiers = resourceItem.getQualifiers(); |
| if (!qualifiers.isEmpty()) { |
| folderName = folderName + RES_QUALIFIER_SEP + qualifiers; |
| } |
| return folderName; |
| } |
| } |