/* * Copyright (C) 2015 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 android.databinding.tool; import com.google.common.escape.Escaper; import org.apache.commons.io.FileUtils; import org.xml.sax.SAXException; import android.databinding.BindingBuildInfo; import android.databinding.tool.store.LayoutFileParser; import android.databinding.tool.store.ResourceBundle; import android.databinding.tool.util.L; import android.databinding.tool.util.Preconditions; import android.databinding.tool.util.SourceCodeEscapers; import android.databinding.tool.writer.JavaFileWriter; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.UUID; import javax.xml.bind.JAXBException; import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPathExpressionException; /** * Processes the layout XML, stripping the binding attributes and elements * and writes the information into an annotated class file for the annotation * processor to work with. */ public class LayoutXmlProcessor { // hardcoded in baseAdapters public static final String RESOURCE_BUNDLE_PACKAGE = "android.databinding.layouts"; public static final String CLASS_NAME = "DataBindingInfo"; private final JavaFileWriter mFileWriter; private final ResourceBundle mResourceBundle; private final int mMinSdk; private boolean mProcessingComplete; private boolean mWritten; private final boolean mIsLibrary; private final String mBuildId = UUID.randomUUID().toString(); private final OriginalFileLookup mOriginalFileLookup; public LayoutXmlProcessor(String applicationPackage, JavaFileWriter fileWriter, int minSdk, boolean isLibrary, OriginalFileLookup originalFileLookup) { mFileWriter = fileWriter; mResourceBundle = new ResourceBundle(applicationPackage); mMinSdk = minSdk; mIsLibrary = isLibrary; mOriginalFileLookup = originalFileLookup; } private static void processIncrementalInputFiles(ResourceInput input, ProcessFileCallback callback) throws IOException, ParserConfigurationException, XPathExpressionException, SAXException { processExistingIncrementalFiles(input.getRootInputFolder(), input.getAdded(), callback); processExistingIncrementalFiles(input.getRootInputFolder(), input.getChanged(), callback); processRemovedIncrementalFiles(input.getRootInputFolder(), input.getRemoved(), callback); } private static void processExistingIncrementalFiles(File inputRoot, List files, ProcessFileCallback callback) throws IOException, XPathExpressionException, SAXException, ParserConfigurationException { for (File file : files) { File parent = file.getParentFile(); if (inputRoot.equals(parent)) { callback.processOtherRootFile(file); } else if (layoutFolderFilter.accept(parent, parent.getName())) { callback.processLayoutFile(file); } else { callback.processOtherFile(parent, file); } } } private static void processRemovedIncrementalFiles(File inputRoot, List files, ProcessFileCallback callback) throws IOException { for (File file : files) { File parent = file.getParentFile(); if (inputRoot.equals(parent)) { callback.processRemovedOtherRootFile(file); } else if (layoutFolderFilter.accept(parent, parent.getName())) { callback.processRemovedLayoutFile(file); } else { callback.processRemovedOtherFile(parent, file); } } } private static void processAllInputFiles(ResourceInput input, ProcessFileCallback callback) throws IOException, XPathExpressionException, SAXException, ParserConfigurationException { FileUtils.deleteDirectory(input.getRootOutputFolder()); Preconditions.check(input.getRootOutputFolder().mkdirs(), "out dir should be re-created"); Preconditions.check(input.getRootInputFolder().isDirectory(), "it must be a directory"); for (File firstLevel : input.getRootInputFolder().listFiles()) { if (firstLevel.isDirectory()) { if (layoutFolderFilter.accept(firstLevel, firstLevel.getName())) { callback.processLayoutFolder(firstLevel); for (File xmlFile : firstLevel.listFiles(xmlFileFilter)) { callback.processLayoutFile(xmlFile); } } else { callback.processOtherFolder(firstLevel); for (File file : firstLevel.listFiles()) { callback.processOtherFile(firstLevel, file); } } } else { callback.processOtherRootFile(firstLevel); } } } /** * used by the studio plugin */ public ResourceBundle getResourceBundle() { return mResourceBundle; } public boolean processResources(final ResourceInput input) throws ParserConfigurationException, SAXException, XPathExpressionException, IOException { if (mProcessingComplete) { return false; } final LayoutFileParser layoutFileParser = new LayoutFileParser(); final URI inputRootUri = input.getRootInputFolder().toURI(); ProcessFileCallback callback = new ProcessFileCallback() { private File convertToOutFile(File file) { final String subPath = toSystemDependentPath(inputRootUri .relativize(file.toURI()).getPath()); return new File(input.getRootOutputFolder(), subPath); } @Override public void processLayoutFile(File file) throws ParserConfigurationException, SAXException, XPathExpressionException, IOException { final File output = convertToOutFile(file); final ResourceBundle.LayoutFileBundle bindingLayout = layoutFileParser .parseXml(file, output, mResourceBundle.getAppPackage(), mOriginalFileLookup); if (bindingLayout != null && !bindingLayout.isEmpty()) { mResourceBundle.addLayoutBundle(bindingLayout); } } @Override public void processOtherFile(File parentFolder, File file) throws IOException { final File outParent = convertToOutFile(parentFolder); FileUtils.copyFile(file, new File(outParent, file.getName())); } @Override public void processRemovedLayoutFile(File file) { mResourceBundle.addRemovedFile(file); final File out = convertToOutFile(file); FileUtils.deleteQuietly(out); } @Override public void processRemovedOtherFile(File parentFolder, File file) throws IOException { final File outParent = convertToOutFile(parentFolder); FileUtils.deleteQuietly(new File(outParent, file.getName())); } @Override public void processOtherFolder(File folder) { //noinspection ResultOfMethodCallIgnored convertToOutFile(folder).mkdirs(); } @Override public void processLayoutFolder(File folder) { //noinspection ResultOfMethodCallIgnored convertToOutFile(folder).mkdirs(); } @Override public void processOtherRootFile(File file) throws IOException { final File outFile = convertToOutFile(file); if (file.isDirectory()) { FileUtils.copyDirectory(file, outFile); } else { FileUtils.copyFile(file, outFile); } } @Override public void processRemovedOtherRootFile(File file) throws IOException { final File outFile = convertToOutFile(file); FileUtils.deleteQuietly(outFile); } }; if (input.isIncremental()) { processIncrementalInputFiles(input, callback); } else { processAllInputFiles(input, callback); } mProcessingComplete = true; return true; } public static String toSystemDependentPath(String path) { if (File.separatorChar != '/') { path = path.replace('/', File.separatorChar); } return path; } public void writeLayoutInfoFiles(File xmlOutDir) throws JAXBException { if (mWritten) { return; } for (List layouts : mResourceBundle.getLayoutBundles() .values()) { for (ResourceBundle.LayoutFileBundle layout : layouts) { writeXmlFile(xmlOutDir, layout); } } for (File file : mResourceBundle.getRemovedFiles()) { String exportFileName = generateExportFileName(file); FileUtils.deleteQuietly(new File(xmlOutDir, exportFileName)); } mWritten = true; } private void writeXmlFile(File xmlOutDir, ResourceBundle.LayoutFileBundle layout) throws JAXBException { String filename = generateExportFileName(layout); mFileWriter.writeToFile(new File(xmlOutDir, filename), layout.toXML()); } public String getInfoClassFullName() { return RESOURCE_BUNDLE_PACKAGE + "." + CLASS_NAME; } /** * Generates a string identifier that can uniquely identify the given layout bundle. * This identifier can be used when we need to export data about this layout bundle. */ private static String generateExportFileName(ResourceBundle.LayoutFileBundle layout) { return generateExportFileName(layout.getFileName(), layout.getDirectory()); } private static String generateExportFileName(File file) { final String fileName = file.getName(); return generateExportFileName(fileName.substring(0, fileName.lastIndexOf('.')), file.getParentFile().getName()); } public static String generateExportFileName(String fileName, String dirName) { return fileName + '-' + dirName + ".xml"; } public static String exportLayoutNameFromInfoFileName(String infoFileName) { return infoFileName.substring(0, infoFileName.indexOf('-')); } public void writeInfoClass(/*Nullable*/ File sdkDir, File xmlOutDir, /*Nullable*/ File exportClassListTo) { writeInfoClass(sdkDir, xmlOutDir, exportClassListTo, false, false); } public String getPackage() { return mResourceBundle.getAppPackage(); } public void writeInfoClass(/*Nullable*/ File sdkDir, File xmlOutDir, File exportClassListTo, boolean enableDebugLogs, boolean printEncodedErrorLogs) { Escaper javaEscaper = SourceCodeEscapers.javaCharEscaper(); final String sdkPath = sdkDir == null ? null : javaEscaper.escape(sdkDir.getAbsolutePath()); final Class annotation = BindingBuildInfo.class; final String layoutInfoPath = javaEscaper.escape(xmlOutDir.getAbsolutePath()); final String exportClassListToPath = exportClassListTo == null ? "" : javaEscaper.escape(exportClassListTo.getAbsolutePath()); String classString = "package " + RESOURCE_BUNDLE_PACKAGE + ";\n\n" + "import " + annotation.getCanonicalName() + ";\n\n" + "@" + annotation.getSimpleName() + "(buildId=\"" + mBuildId + "\", " + "modulePackage=\"" + mResourceBundle.getAppPackage() + "\", " + "sdkRoot=" + "\"" + (sdkPath == null ? "" : sdkPath) + "\"," + "layoutInfoDir=\"" + layoutInfoPath + "\"," + "exportClassListTo=\"" + exportClassListToPath + "\"," + "isLibrary=" + mIsLibrary + "," + "minSdk=" + mMinSdk + "," + "enableDebugLogs=" + enableDebugLogs + "," + "printEncodedError=" + printEncodedErrorLogs + ")\n" + "public class " + CLASS_NAME + " {}\n"; mFileWriter.writeToFile(RESOURCE_BUNDLE_PACKAGE + "." + CLASS_NAME, classString); } private static final FilenameFilter layoutFolderFilter = new FilenameFilter() { @Override public boolean accept(File dir, String name) { return name.startsWith("layout"); } }; private static final FilenameFilter xmlFileFilter = new FilenameFilter() { @Override public boolean accept(File dir, String name) { return name.toLowerCase().endsWith(".xml"); } }; /** * Helper interface that can find the original copy of a resource XML. */ public interface OriginalFileLookup { /** * @param file The intermediate build file * @return The original file or null if original File cannot be found. */ File getOriginalFileFor(File file); } /** * API agnostic class to get resource changes incrementally. */ public static class ResourceInput { private final boolean mIncremental; private final File mRootInputFolder; private final File mRootOutputFolder; private List mAdded = new ArrayList(); private List mRemoved = new ArrayList(); private List mChanged = new ArrayList(); public ResourceInput(boolean incremental, File rootInputFolder, File rootOutputFolder) { mIncremental = incremental; mRootInputFolder = rootInputFolder; mRootOutputFolder = rootOutputFolder; } public void added(File file) { mAdded.add(file); } public void removed(File file) { mRemoved.add(file); } public void changed(File file) { mChanged.add(file); } public boolean shouldCopy() { return !mRootInputFolder.equals(mRootOutputFolder); } List getAdded() { return mAdded; } List getRemoved() { return mRemoved; } List getChanged() { return mChanged; } File getRootInputFolder() { return mRootInputFolder; } File getRootOutputFolder() { return mRootOutputFolder; } public boolean isIncremental() { return mIncremental; } @Override public String toString() { StringBuilder out = new StringBuilder(); out.append("ResourceInput{") .append("mIncremental=").append(mIncremental) .append(", mRootInputFolder=").append(mRootInputFolder) .append(", mRootOutputFolder=").append(mRootOutputFolder); logFiles(out, "added", mAdded); logFiles(out, "removed", mRemoved); logFiles(out, "changed", mChanged); return out.toString(); } private static void logFiles(StringBuilder out, String name, List files) { out.append("\n ").append(name); for (File file : files) { out.append("\n - ").append(file.getAbsolutePath()); } } } private interface ProcessFileCallback { void processLayoutFile(File file) throws ParserConfigurationException, SAXException, XPathExpressionException, IOException; void processOtherFile(File parentFolder, File file) throws IOException; void processRemovedLayoutFile(File file); void processRemovedOtherFile(File parentFolder, File file) throws IOException; void processOtherFolder(File folder); void processLayoutFolder(File folder); void processOtherRootFile(File file) throws IOException; void processRemovedOtherRootFile(File file) throws IOException; } }