| /* |
| * 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; |
| } |
| } |
| |