/* * Copyright (C) 2013 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; import com.android.sdklib.repository.FullRevision; import com.android.tools.idea.actions.NewAndroidComponentAction; import com.android.utils.XmlUtils; import com.google.common.base.Charsets; import com.google.common.collect.*; import com.google.common.io.Files; import com.intellij.ide.actions.NonEmptyActionGroup; import com.intellij.ide.IdeView; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.application.PathManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.module.Module; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.platform.templates.github.ZipUtil; import icons.AndroidIcons; import org.jetbrains.android.facet.AndroidFacet; import org.jetbrains.android.sdk.AndroidSdkData; import org.jetbrains.android.sdk.AndroidSdkUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.w3c.dom.Document; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.util.*; import static com.android.SdkConstants.*; import static com.android.tools.idea.templates.Template.TEMPLATE_XML_NAME; import static com.android.tools.idea.templates.TemplateUtils.listFiles; /** * Handles locating templates and providing template metadata */ public class TemplateManager { private static final Logger LOG = Logger.getInstance("#" + TemplateManager.class.getName()); /** * A directory relative to application home folder where we can find an extra template folder. This lets us ship more up-to-date * templates with the application instead of waiting for SDK updates. */ private static final String BUNDLED_TEMPLATE_PATH = "/plugins/android/lib/templates"; private static final String[] DEVELOPMENT_TEMPLATE_PATHS = {"/../../tools/base/templates", "/android/tools-base/templates", "/community/android/tools-base/templates"}; private static final String EXPLODED_AAR_PATH = "build/intermediates/exploded-aar"; public static final String CATEGORY_OTHER = "Other"; private static final String ACTION_ID_PREFIX = "template.create."; private static final boolean USE_SDK_TEMPLATES = false; private static final Set<String> EXCLUDED_CATEGORIES = ImmutableSet.of("Application", "Applications"); public static final Set<String> EXCLUDED_TEMPLATES = ImmutableSet.of("Empty Activity"); private static final String TEMPLATE_ZIP_NAME = "templates.zip"; /** * Cache for {@link #getTemplate(File)} */ private Map<File, TemplateMetadata> myTemplateMap; /** Table mapping (Category, Template Name) -> Template File */ private Table<String, String, File> myCategoryTable; /** * Cache location for templates pulled from exploded-aars */ private File myAarCache; private static TemplateManager ourInstance = new TemplateManager(); private DefaultActionGroup myTopGroup; private TemplateManager() { } public static TemplateManager getInstance() { return ourInstance; } /** * @return the root folder containing templates */ @Nullable public static File getTemplateRootFolder() { String homePath = FileUtil.toSystemIndependentName(PathManager.getHomePath()); // Release build? VirtualFile root = LocalFileSystem.getInstance().findFileByPath(FileUtil.toSystemIndependentName(homePath + BUNDLED_TEMPLATE_PATH)); if (root == null) { // Development build? for (String path : DEVELOPMENT_TEMPLATE_PATHS) { root = LocalFileSystem.getInstance().findFileByPath(FileUtil.toSystemIndependentName(homePath + path)); if (root != null) { break; } } } if (root != null) { File rootFile = VfsUtilCore.virtualToIoFile(root); if (templateRootIsValid(rootFile)) { return rootFile; } } // Fall back to SDK template root AndroidSdkData sdkData = AndroidSdkUtils.tryToChooseAndroidSdk(); if (sdkData != null) { File location = sdkData.getLocation(); File folder = new File(location, FD_TOOLS + File.separator + FD_TEMPLATES); if (folder.isDirectory()) { return folder; } } return null; } /** * @return A list of root folders containing extra templates */ @NotNull public static List<File> getExtraTemplateRootFolders() { List<File> folders = new ArrayList<File>(); // Check in various locations in the SDK AndroidSdkData sdkData = AndroidSdkUtils.tryToChooseAndroidSdk(); if (sdkData != null) { File location = sdkData.getLocation(); if (USE_SDK_TEMPLATES) { // Look in SDK/tools/templates File toolsTemplatesFolder = new File(location, FileUtil.join(FD_TOOLS, FD_TEMPLATES)); if (toolsTemplatesFolder.isDirectory()) { File[] templateRoots = toolsTemplatesFolder.listFiles(new FileFilter() { @Override public boolean accept(File pathname) { return pathname.isDirectory(); } }); if (templateRoots != null) { Collections.addAll(folders, templateRoots); } } } // Look in SDK/extras/* File extras = new File(location, FD_EXTRAS); if (extras.isDirectory()) { for (File vendor : listFiles(extras)) { if (!vendor.isDirectory()) { continue; } for (File pkg : listFiles(vendor)) { if (pkg.isDirectory()) { File folder = new File(pkg, FD_TEMPLATES); if (folder.isDirectory()) { folders.add(folder); } } } } // Legacy File folder = new File(extras, FD_TEMPLATES); if (folder.isDirectory()) { folders.add(folder); } } // Look in SDK/add-ons File addOns = new File(location, FD_ADDONS); if (addOns.isDirectory()) { for (File addOn : listFiles(addOns)) { if (!addOn.isDirectory()) { continue; } File folder = new File(addOn, FD_TEMPLATES); if (folder.isDirectory()) { folders.add(folder); } } } } // Look for source tree files String homePath = FileUtil.toSystemIndependentName(PathManager.getHomePath()); // Release build? VirtualFile root = LocalFileSystem.getInstance().findFileByPath(FileUtil.toSystemIndependentName(homePath + BUNDLED_TEMPLATE_PATH)); if (root == null) { // Development build? for (String path : DEVELOPMENT_TEMPLATE_PATHS) { root = LocalFileSystem.getInstance().findFileByPath(FileUtil.toSystemIndependentName(homePath + path)); if (root != null) { break; } } } if (root == null) { // error message tailored for release build file layout LOG.error("Templates not found in: " + homePath + BUNDLED_TEMPLATE_PATH + " or " + homePath + Arrays.toString(DEVELOPMENT_TEMPLATE_PATHS)); } else { File templateDir = new File(root.getCanonicalPath()).getAbsoluteFile(); if (templateDir.isDirectory()) { folders.add(templateDir); } } return folders; } /** * Returns all the templates with the given prefix * * @param folder the folder prefix * @return the available templates */ @NotNull public List<File> getTemplates(@NotNull String folder) { List<File> templates = new ArrayList<File>(); Map<String, File> templateNames = Maps.newHashMap(); File root = getTemplateRootFolder(); if (root != null) { File[] files = new File(root, folder).listFiles(); if (files != null) { for (File file : files) { if (file.isDirectory() && (new File(file, TEMPLATE_XML_NAME)).exists()) { // Avoid .DS_Store etc, & non Freemarker templates templates.add(file); templateNames.put(file.getName(), file); } } } } // Add in templates from extras/ as well. for (File extra : getExtraTemplateRootFolders()) { for (File file : listFiles(new File(extra, folder))) { if (file.isDirectory() && (new File(file, TEMPLATE_XML_NAME)).exists()) { File replaces = templateNames.get(file.getName()); if (replaces != null) { int compare = compareTemplates(replaces, file); if (compare > 0) { int index = templates.indexOf(replaces); if (index != -1) { templates.set(index, file); } else { templates.add(file); } } } else { templates.add(file); } } } } // Sort by file name (not path as is File's default) if (templates.size() > 1) { Collections.sort(templates, new Comparator<File>() { @Override public int compare(File file1, File file2) { return file1.getName().compareTo(file2.getName()); } }); } return templates; } @NotNull public static List<File> getTemplatesFromDirectory(@NotNull File externalDirectory, boolean recursive) { List<File> templates = Lists.newArrayList(); if (new File(externalDirectory, TEMPLATE_XML_NAME).exists()) { templates.add(externalDirectory); } if (recursive) { File[] files = externalDirectory.listFiles(); if (files != null) { for (File file : files) { if (file.isDirectory()) { templates.addAll(getTemplatesFromDirectory(file, true)); } } } } return templates; } @NotNull public List<File> getTemplateDirectoriesFromAars(@Nullable Project project) { List<File> templateDirectories = Lists.newArrayList(); if (project != null && project.getBaseDir() != null) { if (myAarCache == null) { try { myAarCache = FileUtil.createTempDirectory(project.getName(), "aar_cache"); } catch (IOException e) { LOG.error(e); return templateDirectories; } } File aarRoot = new File(project.getBaseDir().getPath(), FileUtil.toSystemDependentName(EXPLODED_AAR_PATH)); if (aarRoot.isDirectory()) { for (File artifactPackage : listFiles(aarRoot)) { if (artifactPackage.isDirectory() && !artifactPackage.isHidden()) { for (File artifactName : listFiles(artifactPackage)) { if (artifactName.isDirectory() && !artifactName.isHidden()) { templateDirectories.addAll(getHighestVersionedTemplateRoot(artifactName)); } } } } } } return templateDirectories; } @NotNull private List<File> getHighestVersionedTemplateRoot(@NotNull File artifactNameRoot) { List<File> templateDirectories = Lists.newArrayList(); File highestVersionDir = null; FullRevision highestVersionNumber = null; for (File versionDir : listFiles(artifactNameRoot)) { if (!versionDir.isDirectory() || versionDir.isHidden()) { continue; } // Find the highest version of this AAR FullRevision revision; try { revision = FullRevision.parseRevision(versionDir.getName()); } catch (NumberFormatException e) { // Revision was not parse-able, consider it to be the lowest version revision revision = FullRevision.NOT_SPECIFIED; } if (highestVersionNumber == null || revision.compareTo(highestVersionNumber) > 0) { highestVersionNumber = revision; highestVersionDir = versionDir; } } if (highestVersionDir != null) { String name = artifactNameRoot.getName() + "-" + highestVersionNumber.toString(); File inflated = new File(myAarCache, name); if (!inflated.isDirectory()) { // Only unzip once File zipFile = new File(highestVersionDir, TEMPLATE_ZIP_NAME); if (zipFile.isFile()) { try { ZipUtil.unzip(null, inflated, zipFile, null, null, true); } catch (IOException e) { LOG.error(e); } } } if (inflated.isDirectory()) { templateDirectories.add(inflated); } } return templateDirectories; } /** * @return a list of template files that declare the given category. */ @NotNull public List<File> getTemplatesInCategory(@NotNull String category) { if (getCategoryTable().containsRow(category)) { return Lists.newArrayList(getCategoryTable().row(category).values()); } else { return Lists.newArrayList(); } } @Nullable public ActionGroup getTemplateCreationMenu(@Nullable Project project) { refreshDynamicTemplateMenu(project); return myTopGroup; } public void refreshDynamicTemplateMenu(@Nullable Project project) { if (myTopGroup == null) { myTopGroup = new DefaultActionGroup("AndroidTemplateGroup", false); } else { myTopGroup.removeAll(); } myTopGroup.addSeparator(); ActionManager am = ActionManager.getInstance(); for (final String category : getCategoryTable(true, project).rowKeySet()) { if (EXCLUDED_CATEGORIES.contains(category)) { continue; } // Create the menu group item NonEmptyActionGroup categoryGroup = new NonEmptyActionGroup() { @Override public void update(AnActionEvent e) { IdeView view = LangDataKeys.IDE_VIEW.getData(e.getDataContext()); final Module module = LangDataKeys.MODULE.getData(e.getDataContext()); final AndroidFacet facet = module != null ? AndroidFacet.getInstance(module) : null; Presentation presentation = e.getPresentation(); boolean isProjectReady = facet != null && facet.getIdeaAndroidProject() != null; presentation.setText(category + (isProjectReady ? "" : " (Project not ready)")); presentation.setVisible(getChildrenCount() > 0 && view != null && facet != null && facet.isGradleProject()); } }; categoryGroup.setPopup(true); Presentation presentation = categoryGroup.getTemplatePresentation(); presentation.setIcon(AndroidIcons.Android); presentation.setText(category); Map<String, File> categoryRow = myCategoryTable.row(category); for (String templateName : categoryRow.keySet()) { if (EXCLUDED_TEMPLATES.contains(templateName)) { continue; } TemplateMetadata metadata = getTemplate(myCategoryTable.get(category, templateName)); NewAndroidComponentAction templateAction = new NewAndroidComponentAction(category, templateName, metadata); String actionId = ACTION_ID_PREFIX + category + templateName; am.unregisterAction(actionId); am.registerAction(actionId, templateAction); categoryGroup.add(templateAction); } myTopGroup.add(categoryGroup); } } private Table<String, String, File> getCategoryTable() { return getCategoryTable(false, null); } private Table<String, String, File> getCategoryTable(boolean forceReload, @Nullable Project project) { if (myCategoryTable== null || forceReload) { if (myTemplateMap != null) { myTemplateMap.clear(); } myCategoryTable = TreeBasedTable.create(); for (File categoryDirectory : listFiles(getTemplateRootFolder())) { for (File newTemplate : listFiles(categoryDirectory)) { addTemplateToTable(newTemplate); } } for (File rootDirectory : getExtraTemplateRootFolders()) { for (File categoryDirectory : listFiles(rootDirectory)) { for (File newTemplate : listFiles(categoryDirectory)) { addTemplateToTable(newTemplate); } } } for (File aarDirectory : getTemplateDirectoriesFromAars(project)) { for (File newTemplate : listFiles(aarDirectory)) { addTemplateToTable(newTemplate); } } } return myCategoryTable; } private void addTemplateToTable(@NotNull File newTemplate) { TemplateMetadata newMetadata = getTemplate(newTemplate); if (newMetadata != null) { String title = newMetadata.getTitle(); if (title == null || (newMetadata.getCategory() == null && myCategoryTable.columnKeySet().contains(title) && myCategoryTable.get(CATEGORY_OTHER, title) == null)) { // If this template is uncategorized, and we already have a template of this name that has a category, // that is NOT "Other," then ignore this new template since it's undoubtedly older. return; } String category = newMetadata.getCategory() != null ? newMetadata.getCategory() : CATEGORY_OTHER; File existingTemplate = myCategoryTable.get(category, title); if (existingTemplate == null || compareTemplates(existingTemplate, newTemplate) > 0) { myCategoryTable.put(category, title, newTemplate); } } } /** * Compare two files, and return the one with the HIGHEST revision, and if * the same, most recently modified */ private int compareTemplates(@NotNull File file1, @NotNull File file2) { TemplateMetadata template1 = getTemplate(file1); TemplateMetadata template2 = getTemplate(file2); if (template1 == null) { return 1; } else if (template2 == null) { return -1; } else { int delta = template2.getRevision() - template1.getRevision(); if (delta == 0) { delta = (int)(file2.lastModified() - file1.lastModified()); } return delta; } } @Nullable public File getTemplateFile(@Nullable String category, @Nullable String templateName) { return getCategoryTable().get(category, templateName); } @Nullable public TemplateMetadata getTemplate(@Nullable String category, @Nullable String templateName) { File templateDir = getTemplateFile(category, templateName); return templateDir != null ? getTemplate(templateDir) : null; } @Nullable public TemplateMetadata getTemplate(@NotNull File templateDir) { if (myTemplateMap != null) { TemplateMetadata metadata = myTemplateMap.get(templateDir); if (metadata != null) { return metadata; } } else { myTemplateMap = Maps.newHashMap(); } try { File templateFile = new File(templateDir, TEMPLATE_XML_NAME); if (templateFile.isFile()) { String xml = Files.toString(templateFile, Charsets.UTF_8); Document doc = XmlUtils.parseDocumentSilently(xml, true); if (doc != null && doc.getDocumentElement() != null) { TemplateMetadata metadata = new TemplateMetadata(doc); myTemplateMap.put(templateDir, metadata); return metadata; } } } catch (IOException e) { LOG.warn(e); } return null; } /** * Do a sanity check to see if we have templates that look compatible, otherwise we get really strange problems. The existence * of a gradle wrapper in the templates directory is a good sign. * @return whether the templates pass the check or not */ public static boolean templatesAreValid() { try { File templateRootFolder = getTemplateRootFolder(); if (templateRootFolder == null) { return false; } return templateRootIsValid(templateRootFolder); } catch (Exception e) { return false; } } public static File getWrapperLocation(@NotNull File templateRootFolder) { return new File(templateRootFolder, FD_GRADLE_WRAPPER); } public static boolean templateRootIsValid(@NotNull File templateRootFolder) { return new File(getWrapperLocation(templateRootFolder), FN_GRADLE_WRAPPER_UNIX).exists(); } }