blob: 41436f004d2a74d1f17c55f624d8ac289d191c3c [file] [log] [blame]
/*
* 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<File> 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<File> 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<ResourceBundle.LayoutFileBundle> 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<File> mAdded = new ArrayList<File>();
private List<File> mRemoved = new ArrayList<File>();
private List<File> mChanged = new ArrayList<File>();
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<File> getAdded() {
return mAdded;
}
List<File> getRemoved() {
return mRemoved;
}
List<File> 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<File> 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;
}
}