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

android.databinding.tool.LayoutXmlProcessor Maven / Gradle / Ivy

There is a newer version: 2.3.0
Show newest version
/*
 * 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;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy