com.android.tools.idea.templates.recipe.RecipeContext.java Source code

Java tutorial

Introduction

Here is the source code for com.android.tools.idea.templates.recipe.RecipeContext.java

Source

/*
 * 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 com.android.tools.idea.templates.recipe;

import com.android.SdkConstants;
import com.android.tools.idea.gradle.project.GradleProjectImporter;
import com.android.tools.idea.gradle.util.GradleUtil;
import com.android.tools.idea.templates.*;
import com.google.common.base.Charsets;
import com.google.common.io.Files;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.fileTypes.FileTypeRegistry;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileVisitor;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiFileFactory;
import com.intellij.psi.codeStyle.CodeStyleManager;
import freemarker.template.Configuration;
import freemarker.template.TemplateException;
import org.jetbrains.annotations.NotNull;

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;

import static com.android.SdkConstants.*;
import static com.android.tools.idea.gradle.util.Projects.isBuildWithGradle;
import static com.android.tools.idea.templates.FreemarkerUtils.processFreemarkerTemplate;
import static com.android.tools.idea.templates.TemplateUtils.*;

/**
 * Context for a recipe that contains and accumulates state while executing its instructions and
 * modifying the project.
 */
public final class RecipeContext {

    private static final Logger LOG = Logger.getInstance(RecipeContext.class);

    /**
     * The settings.gradle lives at project root and points gradle at the build files for individual modules in their subdirectories
     */
    private static final String GRADLE_PROJECT_SETTINGS_FILE = "settings.gradle";

    @NotNull
    private final Project myProject;
    @NotNull
    private final PrefixTemplateLoader myLoader;
    @NotNull
    private final Configuration myFreemarker;
    @NotNull
    private final Map<String, Object> myParamMap;
    @NotNull
    private final File myTemplateRoot;
    @NotNull
    private final File myOutputRoot;
    @NotNull
    private final File myModuleRoot;
    private final boolean mySyncGradleIfNeeded; // User can disable gradle syncing if they know they're going to sync themselves anyway

    private boolean myNeedsGradleSync;

    public RecipeContext(@NotNull Project project, @NotNull PrefixTemplateLoader loader,
            @NotNull Configuration freemarker, @NotNull Map<String, Object> paramMap, @NotNull File templateRoot,
            @NotNull File outputRoot, @NotNull File moduleRoot, boolean syncGradleIfNeeded) {
        myProject = project;
        myLoader = loader;
        myFreemarker = freemarker;
        myParamMap = paramMap;
        myTemplateRoot = templateRoot;
        myOutputRoot = outputRoot;
        myModuleRoot = moduleRoot;
        mySyncGradleIfNeeded = syncGradleIfNeeded;
    }

    public RecipeContext(@NotNull Module module, @NotNull PrefixTemplateLoader loader,
            @NotNull Configuration freemarker, @NotNull Map<String, Object> paramMap, @NotNull File templateRoot,
            boolean syncGradleIfNeeded) {
        File moduleRoot = new File(module.getModuleFilePath()).getParentFile();

        myProject = module.getProject();
        myLoader = loader;
        myFreemarker = freemarker;
        myParamMap = paramMap;
        myTemplateRoot = templateRoot;
        myOutputRoot = moduleRoot;
        myModuleRoot = moduleRoot;
        mySyncGradleIfNeeded = syncGradleIfNeeded;
    }

    /**
     * Add a library dependency into the project.
     */
    public void addDependency(@NotNull String mavenUrl) {
        //noinspection unchecked
        List<String> dependencyList = (List<String>) myParamMap.get(TemplateMetadata.ATTR_DEPENDENCIES_LIST);
        dependencyList.add(mavenUrl);
    }

    /**
     * Copies the given source file into the given destination file (where the
     * source is allowed to be a directory, in which case the whole directory is
     * copied recursively)
     */
    public void copy(@NotNull File from, @NotNull File to) {
        try {
            copyTemplateResource(from, to);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Instantiates the given template file into the given output file (running the freemarker
     * engine over it)
     */
    public void instantiate(@NotNull File from, @NotNull File to) {
        try {
            // For now, treat extension-less files as directories... this isn't quite right
            // so I should refine this! Maybe with a unique attribute in the template file?
            boolean isDirectory = from.getName().indexOf('.') == -1;
            if (isDirectory) {
                // It's a directory
                copyTemplateResource(from, to);
            } else {
                from = getSourceFile(from);
                myLoader.setTemplateFile(from);
                String contents = processFreemarkerTemplate(myFreemarker, myParamMap, from);

                contents = format(contents, to);
                File targetFile = getTargetFile(to);
                VfsUtil.createDirectories(targetFile.getParentFile().getAbsolutePath());
                writeFile(this, contents, targetFile);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } catch (TemplateException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Merges the given source file into the given destination file (or it just copies it over if
     * the destination file does not exist).
     * <p/>
     * Only XML and Gradle files are currently supported.
     */
    public void merge(@NotNull File from, @NotNull File to) {
        try {
            String targetText = null;

            to = getTargetFile(to);
            if (!(hasExtension(to, DOT_XML) || hasExtension(to, DOT_GRADLE))) {
                throw new RuntimeException("Only XML or Gradle files can be merged at this point: " + to);
            }

            if (to.exists()) {
                targetText = Files.toString(to, Charsets.UTF_8);
            } else if (to.getParentFile() != null) {
                //noinspection ResultOfMethodCallIgnored
                checkedCreateDirectoryIfMissing(to.getParentFile());
            }

            if (targetText == null) {
                // The target file doesn't exist: don't merge, just copy
                boolean instantiate = hasExtension(from, DOT_FTL);
                if (instantiate) {
                    instantiate(from, to);
                } else {
                    copyTemplateResource(from, to);
                }
                return;
            }

            String sourceText;
            from = getSourceFile(from);
            if (hasExtension(from, DOT_FTL)) {
                // Perform template substitution of the template prior to merging
                myLoader.setTemplateFile(from);
                sourceText = processFreemarkerTemplate(myFreemarker, myParamMap, from);
            } else {
                sourceText = readTextFile(from);
                if (sourceText == null) {
                    return;
                }
            }

            String contents;
            if (to.getName().equals(GRADLE_PROJECT_SETTINGS_FILE)) {
                contents = RecipeMergeUtils.mergeGradleSettingsFile(sourceText, targetText);
                myNeedsGradleSync = true;
            } else if (to.getName().equals(SdkConstants.FN_BUILD_GRADLE)) {
                contents = GradleFileMerger.mergeGradleFiles(sourceText, targetText, myProject);
                myNeedsGradleSync = true;
            } else if (hasExtension(to, DOT_XML)) {
                contents = RecipeMergeUtils.mergeXml(myProject, sourceText, targetText, to);
            } else {
                throw new RuntimeException("Only XML or Gradle settings files can be merged at this point: " + to);
            }

            writeFile(this, contents, to);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } catch (TemplateException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Create a directory at the specified location (if not already present). This will also create
     * any parent directories that don't exist, as well.
     */
    public void mkDir(@NotNull File at) {
        try {
            checkedCreateDirectoryIfMissing(getTargetFile(at));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Open the target file in the editor.
     */
    public void open(@NotNull File file) {
        // Do nothing - it is up to an external class to query this recipe for files it should open
    }

    /**
     * Update the project's gradle build file and sync, if necessary. This should only be called
     * once and after all dependencies are already added.
     */
    public void updateAndSyncGradle() {
        // Handle dependencies
        if (myParamMap.containsKey(TemplateMetadata.ATTR_DEPENDENCIES_LIST)) {
            Object maybeDependencyList = myParamMap.get(TemplateMetadata.ATTR_DEPENDENCIES_LIST);
            if (maybeDependencyList instanceof List) {
                //noinspection unchecked
                List<String> dependencyList = (List<String>) maybeDependencyList;
                if (!dependencyList.isEmpty()) {
                    try {
                        mergeDependenciesIntoGradle();
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
        if (myNeedsGradleSync && mySyncGradleIfNeeded && !myProject.isDefault() && isBuildWithGradle(myProject)) {
            GradleProjectImporter.getInstance().requestProjectSync(myProject, null);
        }
    }

    /**
     * Returns the absolute path to the file which will get read from.
     */
    @NotNull
    public File getSourceFile(@NotNull File file) {
        if (file.isAbsolute()) {
            return file;
        } else {
            // If it's a relative file path, get the data from the template data directory
            return new File(myTemplateRoot, file.getPath());
        }
    }

    /**
     * Returns the absolute path to the file which will get written to.
     */
    @NotNull
    public File getTargetFile(@NotNull File file) throws IOException {
        if (file.isAbsolute()) {
            return file;
        }
        return new File(myOutputRoot, file.getPath());
    }

    /**
     * Merge the URLs from our gradle template into the target module's build.gradle file
     */
    private void mergeDependenciesIntoGradle() throws IOException, TemplateException {
        File gradleBuildFile = GradleUtil.getGradleBuildFilePath(myModuleRoot);
        String templateRoot = TemplateManager.getTemplateRootFolder().getPath();
        File gradleTemplate = new File(templateRoot, FileUtil.join("gradle", "utils", "dependencies.gradle.ftl"));
        myLoader.setTemplateFile(gradleTemplate);
        String contents = processFreemarkerTemplate(myFreemarker, myParamMap, gradleTemplate);
        String destinationContents = null;
        if (gradleBuildFile.exists()) {
            destinationContents = readTextFile(gradleBuildFile);
        }
        if (destinationContents == null) {
            destinationContents = "";
        }
        String result = GradleFileMerger.mergeGradleFiles(contents, destinationContents, myProject);
        writeFile(this, result, gradleBuildFile);
        myNeedsGradleSync = true;
    }

    /**
     * VfsUtil#copyDirectory messes up the undo stack, most likely by trying to
     * create a directory even if it already exists. This is an undo-friendly
     * replacement.
     */
    private void copyDirectory(@NotNull final VirtualFile src, @NotNull final VirtualFile dest) throws IOException {
        final File destinationFile = VfsUtilCore.virtualToIoFile(dest);
        VfsUtilCore.visitChildrenRecursively(src, new VirtualFileVisitor() {
            @Override
            public boolean visitFile(@NotNull VirtualFile file) {
                try {
                    return copyFile(file, src, destinationFile, dest);
                } catch (IOException e) {
                    throw new VisitorException(e);
                }
            }
        }, IOException.class);
    }

    private String format(@NotNull String contents, File to) {
        FileType type = FileTypeRegistry.getInstance().getFileTypeByFileName(to.getName());
        PsiFile file = PsiFileFactory.getInstance(myProject).createFileFromText(to.getName(), type,
                StringUtil.convertLineSeparators(contents));
        CodeStyleManager.getInstance(myProject).reformat(file);
        return file.getText();
    }

    private void copyTemplateResource(@NotNull File from, @NotNull File to) throws IOException {
        from = getSourceFile(from);
        to = getTargetFile(to);

        VirtualFile sourceFile = VfsUtil.findFileByIoFile(from, true);
        assert sourceFile != null : from;
        sourceFile.refresh(false, false);
        File destPath = (from.isDirectory() ? to : to.getParentFile());
        VirtualFile destFolder = checkedCreateDirectoryIfMissing(destPath);
        if (from.isDirectory()) {
            copyDirectory(sourceFile, destFolder);
        } else {
            Document document = FileDocumentManager.getInstance().getDocument(sourceFile);
            if (document != null) {
                writeFile(this, document.getText(), to);
            } else {
                VfsUtilCore.copyFile(this, sourceFile, destFolder, to.getName());
            }
        }
    }

    private boolean copyFile(VirtualFile file, VirtualFile src, File destinationFile, VirtualFile dest)
            throws IOException {
        String relativePath = VfsUtilCore.getRelativePath(file, src, File.separatorChar);
        if (relativePath == null) {
            LOG.error(file.getPath() + " is not a child of " + src, new Exception());
            return false;
        }
        if (file.isDirectory()) {
            checkedCreateDirectoryIfMissing(new File(destinationFile, relativePath));
        } else {
            VirtualFile targetDir = dest;
            if (relativePath.indexOf(File.separatorChar) > 0) {
                String directories = relativePath.substring(0, relativePath.lastIndexOf(File.separatorChar));
                File newParent = new File(destinationFile, directories);
                targetDir = checkedCreateDirectoryIfMissing(newParent);
            }
            VfsUtilCore.copyFile(this, file, targetDir);
        }
        return true;
    }
}