aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateHandler.java
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateHandler.java')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateHandler.java1239
1 files changed, 1239 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateHandler.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateHandler.java
new file mode 100644
index 000000000..8e11841b4
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateHandler.java
@@ -0,0 +1,1239 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.internal.wizards.templates;
+
+import static com.android.SdkConstants.ATTR_PACKAGE;
+import static com.android.SdkConstants.DOT_AIDL;
+import static com.android.SdkConstants.DOT_FTL;
+import static com.android.SdkConstants.DOT_JAVA;
+import static com.android.SdkConstants.DOT_RS;
+import static com.android.SdkConstants.DOT_SVG;
+import static com.android.SdkConstants.DOT_TXT;
+import static com.android.SdkConstants.DOT_XML;
+import static com.android.SdkConstants.EXT_XML;
+import static com.android.SdkConstants.FD_NATIVE_LIBS;
+import static com.android.SdkConstants.XMLNS_PREFIX;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.InstallDependencyPage.SUPPORT_LIBRARY_NAME;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateManager.getTemplateRootFolder;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.common.xml.XmlFormatStyle;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.actions.AddSupportJarAction;
+import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlFormatPreferences;
+import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlPrettyPrinter;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.sdk.AdtManifestMergeCallback;
+import com.android.manifmerger.ManifestMerger;
+import com.android.manifmerger.MergerLog;
+import com.android.resources.ResourceFolderType;
+import com.android.utils.SdkUtils;
+import com.google.common.base.Charsets;
+import com.google.common.collect.Lists;
+import com.google.common.io.Files;
+
+import freemarker.cache.TemplateLoader;
+import freemarker.template.Configuration;
+import freemarker.template.DefaultObjectWrapper;
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.ToolFactory;
+import org.eclipse.jdt.core.formatter.CodeFormatter;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.NullChange;
+import org.eclipse.ltk.core.refactoring.TextFileChange;
+import org.eclipse.swt.SWT;
+import org.eclipse.text.edits.InsertEdit;
+import org.eclipse.text.edits.MultiTextEdit;
+import org.eclipse.text.edits.ReplaceEdit;
+import org.eclipse.text.edits.TextEdit;
+import org.osgi.framework.Constants;
+import org.osgi.framework.Version;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.lang.reflect.InvocationTargetException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+/**
+ * Handler which manages instantiating FreeMarker templates, copying resources
+ * and merging into existing files
+ */
+class TemplateHandler {
+ /** Highest supported format; templates with a higher number will be skipped
+ * <p>
+ * <ul>
+ * <li> 1: Initial format, supported by ADT 20 and up.
+ * <li> 2: ADT 21 and up. Boolean variables that have a default value and are not
+ * edited by the user would end up as strings in ADT 20; now they are always
+ * proper Booleans. Templates which rely on this should specify format >= 2.
+ * <li> 3: The wizard infrastructure passes the {@code isNewProject} boolean variable
+ * to indicate whether a wizard is created as part of a new blank project
+ * <li> 4: The templates now specify dependencies in the recipe file.
+ * </ul>
+ */
+ static final int CURRENT_FORMAT = 4;
+
+ /**
+ * Special marker indicating that this path refers to the special shared
+ * resource directory rather than being somewhere inside the root/ directory
+ * where all template specific resources are found
+ */
+ private static final String VALUE_TEMPLATE_DIR = "$TEMPLATEDIR"; //$NON-NLS-1$
+
+ /**
+ * Directory within the template which contains the resources referenced
+ * from the template.xml file
+ */
+ private static final String DATA_ROOT = "root"; //$NON-NLS-1$
+
+ /**
+ * Shared resource directory containing common resources shared among
+ * multiple templates
+ */
+ private static final String RESOURCE_ROOT = "resources"; //$NON-NLS-1$
+
+ /** Reserved filename which describes each template */
+ static final String TEMPLATE_XML = "template.xml"; //$NON-NLS-1$
+
+ // Various tags and attributes used in the template metadata files - template.xml,
+ // globals.xml.ftl, recipe.xml.ftl, etc.
+
+ static final String TAG_MERGE = "merge"; //$NON-NLS-1$
+ static final String TAG_EXECUTE = "execute"; //$NON-NLS-1$
+ static final String TAG_GLOBALS = "globals"; //$NON-NLS-1$
+ static final String TAG_GLOBAL = "global"; //$NON-NLS-1$
+ static final String TAG_PARAMETER = "parameter"; //$NON-NLS-1$
+ static final String TAG_COPY = "copy"; //$NON-NLS-1$
+ static final String TAG_INSTANTIATE = "instantiate"; //$NON-NLS-1$
+ static final String TAG_OPEN = "open"; //$NON-NLS-1$
+ static final String TAG_THUMB = "thumb"; //$NON-NLS-1$
+ static final String TAG_THUMBS = "thumbs"; //$NON-NLS-1$
+ static final String TAG_DEPENDENCY = "dependency"; //$NON-NLS-1$
+ static final String TAG_ICONS = "icons"; //$NON-NLS-1$
+ static final String TAG_FORMFACTOR = "formfactor"; //$NON-NLS-1$
+ static final String TAG_CATEGORY = "category"; //$NON-NLS-1$
+ static final String ATTR_FORMAT = "format"; //$NON-NLS-1$
+ static final String ATTR_REVISION = "revision"; //$NON-NLS-1$
+ static final String ATTR_VALUE = "value"; //$NON-NLS-1$
+ static final String ATTR_DEFAULT = "default"; //$NON-NLS-1$
+ static final String ATTR_SUGGEST = "suggest"; //$NON-NLS-1$
+ static final String ATTR_ID = "id"; //$NON-NLS-1$
+ static final String ATTR_NAME = "name"; //$NON-NLS-1$
+ static final String ATTR_DESCRIPTION = "description";//$NON-NLS-1$
+ static final String ATTR_TYPE = "type"; //$NON-NLS-1$
+ static final String ATTR_HELP = "help"; //$NON-NLS-1$
+ static final String ATTR_FILE = "file"; //$NON-NLS-1$
+ static final String ATTR_TO = "to"; //$NON-NLS-1$
+ static final String ATTR_FROM = "from"; //$NON-NLS-1$
+ static final String ATTR_CONSTRAINTS = "constraints";//$NON-NLS-1$
+ static final String ATTR_BACKGROUND = "background"; //$NON-NLS-1$
+ static final String ATTR_FOREGROUND = "foreground"; //$NON-NLS-1$
+ static final String ATTR_SHAPE = "shape"; //$NON-NLS-1$
+ static final String ATTR_TRIM = "trim"; //$NON-NLS-1$
+ static final String ATTR_PADDING = "padding"; //$NON-NLS-1$
+ static final String ATTR_SOURCE_TYPE = "source"; //$NON-NLS-1$
+ static final String ATTR_CLIPART_NAME = "clipartName";//$NON-NLS-1$
+ static final String ATTR_TEXT = "text"; //$NON-NLS-1$
+ static final String ATTR_SRC_DIR = "srcDir"; //$NON-NLS-1$
+ static final String ATTR_SRC_OUT = "srcOut"; //$NON-NLS-1$
+ static final String ATTR_RES_DIR = "resDir"; //$NON-NLS-1$
+ static final String ATTR_RES_OUT = "resOut"; //$NON-NLS-1$
+ static final String ATTR_MANIFEST_DIR = "manifestDir";//$NON-NLS-1$
+ static final String ATTR_MANIFEST_OUT = "manifestOut";//$NON-NLS-1$
+ static final String ATTR_PROJECT_DIR = "projectDir"; //$NON-NLS-1$
+ static final String ATTR_PROJECT_OUT = "projectOut"; //$NON-NLS-1$
+ static final String ATTR_MAVEN_URL = "mavenUrl"; //$NON-NLS-1$
+ static final String ATTR_DEBUG_KEYSTORE_SHA1 =
+ "debugKeystoreSha1"; //$NON-NLS-1$
+
+ static final String CATEGORY_ACTIVITIES = "activities";//$NON-NLS-1$
+ static final String CATEGORY_PROJECTS = "projects"; //$NON-NLS-1$
+ static final String CATEGORY_OTHER = "other"; //$NON-NLS-1$
+
+ static final String MAVEN_SUPPORT_V4 = "support-v4"; //$NON-NLS-1$
+ static final String MAVEN_SUPPORT_V13 = "support-v13"; //$NON-NLS-1$
+ static final String MAVEN_APPCOMPAT = "appcompat-v7"; //$NON-NLS-1$
+
+ /** Default padding to apply in wizards around the thumbnail preview images */
+ static final int PREVIEW_PADDING = 10;
+
+ /** Default width to scale thumbnail preview images in wizards to */
+ static final int PREVIEW_WIDTH = 200;
+
+ /**
+ * List of files to open after the wizard has been created (these are
+ * identified by {@link #TAG_OPEN} elements in the recipe file
+ */
+ private final List<String> mOpen = Lists.newArrayList();
+
+ /**
+ * List of actions to perform after the wizard has finished.
+ */
+ protected List<Runnable> mFinalizingActions = Lists.newArrayList();
+
+ /** Path to the directory containing the templates */
+ @NonNull
+ private final File mRootPath;
+
+ /** The changes being processed by the template handler */
+ private List<Change> mMergeChanges;
+ private List<Change> mTextChanges;
+ private List<Change> mOtherChanges;
+
+ /** The project to write the template into */
+ private IProject mProject;
+
+ /** The template loader which is responsible for finding (and sharing) template files */
+ private final MyTemplateLoader mLoader;
+
+ /** Agree to all file-overwrites from now on? */
+ private boolean mYesToAll = false;
+
+ /** Is writing the template cancelled? */
+ private boolean mNoToAll = false;
+
+ /**
+ * Should files that we merge contents into be backed up? If yes, will
+ * create emacs-style tilde-file backups (filename.xml~)
+ */
+ private boolean mBackupMergedFiles = true;
+
+ /**
+ * Template metadata
+ */
+ private TemplateMetadata mTemplate;
+
+ private final TemplateManager mManager;
+
+ /** Creates a new {@link TemplateHandler} for the given root path */
+ static TemplateHandler createFromPath(File rootPath) {
+ return new TemplateHandler(rootPath, new TemplateManager());
+ }
+
+ /** Creates a new {@link TemplateHandler} for the template name, which should
+ * be relative to the templates directory */
+ static TemplateHandler createFromName(String category, String name) {
+ TemplateManager manager = new TemplateManager();
+
+ // Use the TemplateManager iteration which should merge contents between the
+ // extras/templates/ and tools/templates folders and pick the most recent version
+ List<File> templates = manager.getTemplates(category);
+ for (File file : templates) {
+ if (file.getName().equals(name) && category.equals(file.getParentFile().getName())) {
+ return new TemplateHandler(file, manager);
+ }
+ }
+
+ return new TemplateHandler(new File(getTemplateRootFolder(),
+ category + File.separator + name), manager);
+ }
+
+ private TemplateHandler(File rootPath, TemplateManager manager) {
+ mRootPath = rootPath;
+ mManager = manager;
+ mLoader = new MyTemplateLoader();
+ mLoader.setPrefix(mRootPath.getPath());
+ }
+
+ public TemplateManager getManager() {
+ return mManager;
+ }
+
+ public void setBackupMergedFiles(boolean backupMergedFiles) {
+ mBackupMergedFiles = backupMergedFiles;
+ }
+
+ @NonNull
+ public List<Change> render(IProject project, Map<String, Object> args) {
+ mOpen.clear();
+
+ mProject = project;
+ mMergeChanges = new ArrayList<Change>();
+ mTextChanges = new ArrayList<Change>();
+ mOtherChanges = new ArrayList<Change>();
+
+ // Render the instruction list template.
+ Map<String, Object> paramMap = createParameterMap(args);
+ Configuration freemarker = new Configuration();
+ freemarker.setObjectWrapper(new DefaultObjectWrapper());
+ freemarker.setTemplateLoader(mLoader);
+
+ processVariables(freemarker, TEMPLATE_XML, paramMap);
+
+ // Add the changes in the order where merges are shown first, then text files,
+ // and finally other files (like jars and icons which don't have previews).
+ List<Change> changes = new ArrayList<Change>();
+ changes.addAll(mMergeChanges);
+ changes.addAll(mTextChanges);
+ changes.addAll(mOtherChanges);
+ return changes;
+ }
+
+ Map<String, Object> createParameterMap(Map<String, Object> args) {
+ final Map<String, Object> paramMap = createBuiltinMap();
+
+ // Wizard parameters supplied by user, specific to this template
+ paramMap.putAll(args);
+
+ return paramMap;
+ }
+
+ /** Data model for the templates */
+ static Map<String, Object> createBuiltinMap() {
+ // Create the data model.
+ final Map<String, Object> paramMap = new HashMap<String, Object>();
+
+ // Builtin conversion methods
+ paramMap.put("slashedPackageName", new FmSlashedPackageNameMethod()); //$NON-NLS-1$
+ paramMap.put("camelCaseToUnderscore", new FmCamelCaseToUnderscoreMethod()); //$NON-NLS-1$
+ paramMap.put("underscoreToCamelCase", new FmUnderscoreToCamelCaseMethod()); //$NON-NLS-1$
+ paramMap.put("activityToLayout", new FmActivityToLayoutMethod()); //$NON-NLS-1$
+ paramMap.put("layoutToActivity", new FmLayoutToActivityMethod()); //$NON-NLS-1$
+ paramMap.put("classToResource", new FmClassNameToResourceMethod()); //$NON-NLS-1$
+ paramMap.put("escapeXmlAttribute", new FmEscapeXmlStringMethod()); //$NON-NLS-1$
+ paramMap.put("escapeXmlText", new FmEscapeXmlStringMethod()); //$NON-NLS-1$
+ paramMap.put("escapeXmlString", new FmEscapeXmlStringMethod()); //$NON-NLS-1$
+ paramMap.put("extractLetters", new FmExtractLettersMethod()); //$NON-NLS-1$
+
+ // This should be handled better: perhaps declared "required packages" as part of the
+ // inputs? (It would be better if we could conditionally disable template based
+ // on availability)
+ Map<String, String> builtin = new HashMap<String, String>();
+ builtin.put("templatesRes", VALUE_TEMPLATE_DIR); //$NON-NLS-1$
+ paramMap.put("android", builtin); //$NON-NLS-1$
+
+ return paramMap;
+ }
+
+ static void addDirectoryParameters(Map<String, Object> parameters, IProject project) {
+ IPath srcDir = project.getFile(SdkConstants.SRC_FOLDER).getProjectRelativePath();
+ parameters.put(ATTR_SRC_DIR, srcDir.toString());
+
+ IPath resDir = project.getFile(SdkConstants.RES_FOLDER).getProjectRelativePath();
+ parameters.put(ATTR_RES_DIR, resDir.toString());
+
+ IPath manifestDir = project.getProjectRelativePath();
+ parameters.put(ATTR_MANIFEST_DIR, manifestDir.toString());
+ parameters.put(ATTR_MANIFEST_OUT, manifestDir.toString());
+
+ parameters.put(ATTR_PROJECT_DIR, manifestDir.toString());
+ parameters.put(ATTR_PROJECT_OUT, manifestDir.toString());
+
+ parameters.put(ATTR_DEBUG_KEYSTORE_SHA1, "");
+ }
+
+ @Nullable
+ public TemplateMetadata getTemplate() {
+ if (mTemplate == null) {
+ mTemplate = mManager.getTemplate(mRootPath);
+ }
+
+ return mTemplate;
+ }
+
+ @NonNull
+ public String getResourcePath(String templateName) {
+ return new File(mRootPath.getPath(), templateName).getPath();
+ }
+
+ /**
+ * Load a text resource for the given relative path within the template
+ *
+ * @param relativePath relative path within the template
+ * @return the string contents of the template text file
+ */
+ @Nullable
+ public String readTemplateTextResource(@NonNull String relativePath) {
+ try {
+ return Files.toString(new File(mRootPath,
+ relativePath.replace('/', File.separatorChar)), Charsets.UTF_8);
+ } catch (IOException e) {
+ AdtPlugin.log(e, null);
+ return null;
+ }
+ }
+
+ @Nullable
+ public String readTemplateTextResource(@NonNull File file) {
+ assert file.isAbsolute();
+ try {
+ return Files.toString(file, Charsets.UTF_8);
+ } catch (IOException e) {
+ AdtPlugin.log(e, null);
+ return null;
+ }
+ }
+
+ /**
+ * Reads the contents of a resource
+ *
+ * @param relativePath the path relative to the template directory
+ * @return the binary data read from the file
+ */
+ @Nullable
+ public byte[] readTemplateResource(@NonNull String relativePath) {
+ try {
+ return Files.toByteArray(new File(mRootPath, relativePath));
+ } catch (IOException e) {
+ AdtPlugin.log(e, null);
+ return null;
+ }
+ }
+
+ /**
+ * Most recent thrown exception during template instantiation. This should
+ * basically always be null. Used by unit tests to see if any template
+ * instantiation recorded a failure.
+ */
+ @VisibleForTesting
+ public static Exception sMostRecentException;
+
+ /** Read the given FreeMarker file and process the variable definitions */
+ private void processVariables(final Configuration freemarker,
+ String file, final Map<String, Object> paramMap) {
+ try {
+ String xml;
+ if (file.endsWith(DOT_XML)) {
+ // Just read the file
+ xml = readTemplateTextResource(file);
+ if (xml == null) {
+ return;
+ }
+ } else {
+ mLoader.setTemplateFile(new File(mRootPath, file));
+ Template inputsTemplate = freemarker.getTemplate(file);
+ StringWriter out = new StringWriter();
+ inputsTemplate.process(paramMap, out);
+ out.flush();
+ xml = out.toString();
+ }
+
+ SAXParserFactory factory = SAXParserFactory.newInstance();
+ SAXParser saxParser = factory.newSAXParser();
+ saxParser.parse(new ByteArrayInputStream(xml.getBytes()), new DefaultHandler() {
+ @Override
+ public void startElement(String uri, String localName, String name,
+ Attributes attributes)
+ throws SAXException {
+ if (TAG_PARAMETER.equals(name)) {
+ String id = attributes.getValue(ATTR_ID);
+ if (!paramMap.containsKey(id)) {
+ String value = attributes.getValue(ATTR_DEFAULT);
+ Object mapValue = value;
+ if (value != null && !value.isEmpty()) {
+ String type = attributes.getValue(ATTR_TYPE);
+ if ("boolean".equals(type)) { //$NON-NLS-1$
+ mapValue = Boolean.valueOf(value);
+ }
+ }
+ paramMap.put(id, mapValue);
+ }
+ } else if (TAG_GLOBAL.equals(name)) {
+ String id = attributes.getValue(ATTR_ID);
+ if (!paramMap.containsKey(id)) {
+ paramMap.put(id, TypedVariable.parseGlobal(attributes));
+ }
+ } else if (TAG_GLOBALS.equals(name)) {
+ // Handle evaluation of variables
+ String path = attributes.getValue(ATTR_FILE);
+ if (path != null) {
+ processVariables(freemarker, path, paramMap);
+ } // else: <globals> root element
+ } else if (TAG_EXECUTE.equals(name)) {
+ String path = attributes.getValue(ATTR_FILE);
+ if (path != null) {
+ execute(freemarker, path, paramMap);
+ }
+ } else if (TAG_DEPENDENCY.equals(name)) {
+ String dependencyName = attributes.getValue(ATTR_NAME);
+ if (dependencyName.equals(SUPPORT_LIBRARY_NAME)) {
+ // We assume the revision requirement has been satisfied
+ // by the wizard
+ File path = AddSupportJarAction.getSupportJarFile();
+ if (path != null) {
+ IPath to = getTargetPath(FD_NATIVE_LIBS +'/' + path.getName());
+ try {
+ copy(path, to);
+ } catch (IOException ioe) {
+ AdtPlugin.log(ioe, null);
+ }
+ }
+ }
+ } else if (!name.equals("template") && !name.equals(TAG_CATEGORY) &&
+ !name.equals(TAG_FORMFACTOR) && !name.equals("option") &&
+ !name.equals(TAG_THUMBS) && !name.equals(TAG_THUMB) &&
+ !name.equals(TAG_ICONS)) {
+ System.err.println("WARNING: Unknown template directive " + name);
+ }
+ }
+ });
+ } catch (Exception e) {
+ sMostRecentException = e;
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private boolean canOverwrite(File file) {
+ if (file.exists()) {
+ // Warn that the file already exists and ask the user what to do
+ if (!mYesToAll) {
+ MessageDialog dialog = new MessageDialog(null, "File Already Exists", null,
+ String.format(
+ "%1$s already exists.\nWould you like to replace it?",
+ file.getPath()),
+ MessageDialog.QUESTION, new String[] {
+ // Yes will be moved to the end because it's the default
+ "Yes", "No", "Cancel", "Yes to All"
+ }, 0);
+ int result = dialog.open();
+ switch (result) {
+ case 0:
+ // Yes
+ break;
+ case 3:
+ // Yes to all
+ mYesToAll = true;
+ break;
+ case 1:
+ // No
+ return false;
+ case SWT.DEFAULT:
+ case 2:
+ // Cancel
+ mNoToAll = true;
+ return false;
+ }
+ }
+
+ if (mBackupMergedFiles) {
+ return makeBackup(file);
+ } else {
+ return file.delete();
+ }
+ }
+
+ return true;
+ }
+
+ /** Executes the given recipe file: copying, merging, instantiating, opening files etc */
+ private void execute(
+ final Configuration freemarker,
+ String file,
+ final Map<String, Object> paramMap) {
+ try {
+ mLoader.setTemplateFile(new File(mRootPath, file));
+ Template freemarkerTemplate = freemarker.getTemplate(file);
+
+ StringWriter out = new StringWriter();
+ freemarkerTemplate.process(paramMap, out);
+ out.flush();
+ String xml = out.toString();
+
+ // Parse and execute the resulting instruction list.
+ SAXParserFactory factory = SAXParserFactory.newInstance();
+ SAXParser saxParser = factory.newSAXParser();
+
+ saxParser.parse(new ByteArrayInputStream(xml.getBytes()),
+ new DefaultHandler() {
+ @Override
+ public void startElement(String uri, String localName, String name,
+ Attributes attributes)
+ throws SAXException {
+ if (mNoToAll) {
+ return;
+ }
+
+ try {
+ boolean instantiate = TAG_INSTANTIATE.equals(name);
+ if (TAG_COPY.equals(name) || instantiate) {
+ String fromPath = attributes.getValue(ATTR_FROM);
+ String toPath = attributes.getValue(ATTR_TO);
+ if (toPath == null || toPath.isEmpty()) {
+ toPath = attributes.getValue(ATTR_FROM);
+ toPath = AdtUtils.stripSuffix(toPath, DOT_FTL);
+ }
+ IPath to = getTargetPath(toPath);
+ if (instantiate) {
+ instantiate(freemarker, paramMap, fromPath, to);
+ } else {
+ copyTemplateResource(fromPath, to);
+ }
+ } else if (TAG_MERGE.equals(name)) {
+ String fromPath = attributes.getValue(ATTR_FROM);
+ String toPath = attributes.getValue(ATTR_TO);
+ if (toPath == null || toPath.isEmpty()) {
+ toPath = attributes.getValue(ATTR_FROM);
+ toPath = AdtUtils.stripSuffix(toPath, DOT_FTL);
+ }
+ // Resources in template.xml are located within root/
+ IPath to = getTargetPath(toPath);
+ merge(freemarker, paramMap, fromPath, to);
+ } else if (name.equals(TAG_OPEN)) {
+ // The relative path here is within the output directory:
+ String relativePath = attributes.getValue(ATTR_FILE);
+ if (relativePath != null && !relativePath.isEmpty()) {
+ mOpen.add(relativePath);
+ }
+ } else if (TAG_DEPENDENCY.equals(name)) {
+ String dependencyUrl = attributes.getValue(ATTR_MAVEN_URL);
+ File path;
+ if (dependencyUrl.contains(MAVEN_SUPPORT_V4)) {
+ // We assume the revision requirement has been satisfied
+ // by the wizard
+ path = AddSupportJarAction.getSupportJarFile();
+ } else if (dependencyUrl.contains(MAVEN_SUPPORT_V13)) {
+ path = AddSupportJarAction.getSupport13JarFile();
+ } else if (dependencyUrl.contains(MAVEN_APPCOMPAT)) {
+ path = null;
+ mFinalizingActions.add(new Runnable() {
+ @Override
+ public void run() {
+ AddSupportJarAction.installAppCompatLibrary(mProject, true);
+ }
+ });
+ } else {
+ path = null;
+ System.err.println("WARNING: Unknown dependency type");
+ }
+
+ if (path != null) {
+ IPath to = getTargetPath(FD_NATIVE_LIBS +'/' + path.getName());
+ try {
+ copy(path, to);
+ } catch (IOException ioe) {
+ AdtPlugin.log(ioe, null);
+ }
+ }
+ } else if (!name.equals("recipe") && !name.equals(TAG_DEPENDENCY)) { //$NON-NLS-1$
+ System.err.println("WARNING: Unknown template directive " + name);
+ }
+ } catch (Exception e) {
+ sMostRecentException = e;
+ AdtPlugin.log(e, null);
+ }
+ }
+ });
+
+ } catch (Exception e) {
+ sMostRecentException = e;
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ @NonNull
+ private File getFullPath(@NonNull String fromPath) {
+ if (fromPath.startsWith(VALUE_TEMPLATE_DIR)) {
+ return new File(getTemplateRootFolder(), RESOURCE_ROOT + File.separator
+ + fromPath.substring(VALUE_TEMPLATE_DIR.length() + 1).replace('/',
+ File.separatorChar));
+ }
+ return new File(mRootPath, DATA_ROOT + File.separator + fromPath);
+ }
+
+ @NonNull
+ private IPath getTargetPath(@NonNull String relative) {
+ if (relative.indexOf('\\') != -1) {
+ relative = relative.replace('\\', '/');
+ }
+ return new Path(relative);
+ }
+
+ @NonNull
+ private IFile getTargetFile(@NonNull IPath path) {
+ return mProject.getFile(path);
+ }
+
+ private void merge(
+ @NonNull final Configuration freemarker,
+ @NonNull final Map<String, Object> paramMap,
+ @NonNull String relativeFrom,
+ @NonNull IPath toPath) throws IOException, TemplateException {
+
+ String currentXml = null;
+
+ IFile to = getTargetFile(toPath);
+ if (to.exists()) {
+ currentXml = AdtPlugin.readFile(to);
+ }
+
+ if (currentXml == null) {
+ // The target file doesn't exist: don't merge, just copy
+ boolean instantiate = relativeFrom.endsWith(DOT_FTL);
+ if (instantiate) {
+ instantiate(freemarker, paramMap, relativeFrom, toPath);
+ } else {
+ copyTemplateResource(relativeFrom, toPath);
+ }
+ return;
+ }
+
+ if (!to.getFileExtension().equals(EXT_XML)) {
+ throw new RuntimeException("Only XML files can be merged at this point: " + to);
+ }
+
+ String xml = null;
+ File from = getFullPath(relativeFrom);
+ if (relativeFrom.endsWith(DOT_FTL)) {
+ // Perform template substitution of the template prior to merging
+ mLoader.setTemplateFile(from);
+ Template template = freemarker.getTemplate(from.getName());
+ Writer out = new StringWriter();
+ template.process(paramMap, out);
+ out.flush();
+ xml = out.toString();
+ } else {
+ xml = readTemplateTextResource(from);
+ if (xml == null) {
+ return;
+ }
+ }
+
+ Document currentDocument = DomUtilities.parseStructuredDocument(currentXml);
+ assert currentDocument != null : currentXml;
+ Document fragment = DomUtilities.parseStructuredDocument(xml);
+ assert fragment != null : xml;
+
+ XmlFormatStyle formatStyle = XmlFormatStyle.MANIFEST;
+ boolean modified;
+ boolean ok;
+ String fileName = to.getName();
+ if (fileName.equals(SdkConstants.FN_ANDROID_MANIFEST_XML)) {
+ modified = ok = mergeManifest(currentDocument, fragment);
+ } else {
+ // Merge plain XML files
+ String parentFolderName = to.getParent().getName();
+ ResourceFolderType folderType = ResourceFolderType.getFolderType(parentFolderName);
+ if (folderType != null) {
+ formatStyle = EclipseXmlPrettyPrinter.getForFile(toPath);
+ } else {
+ formatStyle = XmlFormatStyle.FILE;
+ }
+
+ modified = mergeResourceFile(currentDocument, fragment, folderType, paramMap);
+ ok = true;
+ }
+
+ // Finally write out the merged file (formatting etc)
+ String contents = null;
+ if (ok) {
+ if (modified) {
+ contents = EclipseXmlPrettyPrinter.prettyPrint(currentDocument,
+ EclipseXmlFormatPreferences.create(), formatStyle, null,
+ currentXml.endsWith("\n")); //$NON-NLS-1$
+ }
+ } else {
+ // Just insert into file along with comment, using the "standard" conflict
+ // syntax that many tools and editors recognize.
+ String sep = SdkUtils.getLineSeparator();
+ contents =
+ "<<<<<<< Original" + sep
+ + currentXml + sep
+ + "=======" + sep
+ + xml
+ + ">>>>>>> Added" + sep;
+ }
+
+ if (contents != null) {
+ TextFileChange change = new TextFileChange("Merge " + fileName, to);
+ MultiTextEdit rootEdit = new MultiTextEdit();
+ rootEdit.addChild(new ReplaceEdit(0, currentXml.length(), contents));
+ change.setEdit(rootEdit);
+ change.setTextType(SdkConstants.EXT_XML);
+ mMergeChanges.add(change);
+ }
+ }
+
+ /** Merges the given resource file contents into the given resource file
+ * @param paramMap */
+ private static boolean mergeResourceFile(Document currentDocument, Document fragment,
+ ResourceFolderType folderType, Map<String, Object> paramMap) {
+ boolean modified = false;
+
+ // Copy namespace declarations
+ NamedNodeMap attributes = fragment.getDocumentElement().getAttributes();
+ if (attributes != null) {
+ for (int i = 0, n = attributes.getLength(); i < n; i++) {
+ Attr attribute = (Attr) attributes.item(i);
+ if (attribute.getName().startsWith(XMLNS_PREFIX)) {
+ currentDocument.getDocumentElement().setAttribute(attribute.getName(),
+ attribute.getValue());
+ }
+ }
+ }
+
+ // For layouts for example, I want to *append* inside the root all the
+ // contents of the new file.
+ // But for resources for example, I want to combine elements which specify
+ // the same name or id attribute.
+ // For elements like manifest files we need to insert stuff at the right
+ // location in a nested way (activities in the application element etc)
+ // but that doesn't happen for the other file types.
+ Element root = fragment.getDocumentElement();
+ NodeList children = root.getChildNodes();
+ List<Node> nodes = new ArrayList<Node>(children.getLength());
+ for (int i = children.getLength() - 1; i >= 0; i--) {
+ Node child = children.item(i);
+ nodes.add(child);
+ root.removeChild(child);
+ }
+ Collections.reverse(nodes);
+
+ root = currentDocument.getDocumentElement();
+
+ if (folderType == ResourceFolderType.VALUES) {
+ // Try to merge items of the same name
+ Map<String, Node> old = new HashMap<String, Node>();
+ NodeList newSiblings = root.getChildNodes();
+ for (int i = newSiblings.getLength() - 1; i >= 0; i--) {
+ Node child = newSiblings.item(i);
+ if (child.getNodeType() == Node.ELEMENT_NODE) {
+ Element element = (Element) child;
+ String name = getResourceId(element);
+ if (name != null) {
+ old.put(name, element);
+ }
+ }
+ }
+
+ for (Node node : nodes) {
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ Element element = (Element) node;
+ String name = getResourceId(element);
+ Node replace = name != null ? old.get(name) : null;
+ if (replace != null) {
+ // There is an existing item with the same id: just replace it
+ // ACTUALLY -- let's NOT change it.
+ // Let's say you've used the activity wizard once, and it
+ // emits some configuration parameter as a resource that
+ // it depends on, say "padding". Then the user goes and
+ // tweaks the padding to some other number.
+ // Now running the wizard a *second* time for some new activity,
+ // we should NOT go and set the value back to the template's
+ // default!
+ //root.replaceChild(node, replace);
+
+ // ... ON THE OTHER HAND... What if it's a parameter class
+ // (where the template rewrites a common attribute). Here it's
+ // really confusing if the new parameter is not set. This is
+ // really an error in the template, since we shouldn't have conflicts
+ // like that, but we need to do something to help track this down.
+ AdtPlugin.log(null,
+ "Warning: Ignoring name conflict in resource file for name %1$s",
+ name);
+ } else {
+ root.appendChild(node);
+ modified = true;
+ }
+ }
+ }
+ } else {
+ // In other file types, such as layouts, just append all the new content
+ // at the end.
+ for (Node node : nodes) {
+ root.appendChild(node);
+ modified = true;
+ }
+ }
+ return modified;
+ }
+
+ /** Merges the given manifest fragment into the given manifest file */
+ private static boolean mergeManifest(Document currentManifest, Document fragment) {
+ // TODO change MergerLog.wrapSdkLog by a custom IMergerLog that will create
+ // and maintain error markers.
+
+ // Transfer package element from manifest to merged in root; required by
+ // manifest merger
+ Element fragmentRoot = fragment.getDocumentElement();
+ Element manifestRoot = currentManifest.getDocumentElement();
+ if (fragmentRoot == null || manifestRoot == null) {
+ return false;
+ }
+ String pkg = fragmentRoot.getAttribute(ATTR_PACKAGE);
+ if (pkg == null || pkg.isEmpty()) {
+ pkg = manifestRoot.getAttribute(ATTR_PACKAGE);
+ if (pkg != null && !pkg.isEmpty()) {
+ fragmentRoot.setAttribute(ATTR_PACKAGE, pkg);
+ }
+ }
+
+ ManifestMerger merger = new ManifestMerger(
+ MergerLog.wrapSdkLog(AdtPlugin.getDefault()),
+ new AdtManifestMergeCallback()).setExtractPackagePrefix(true);
+ return currentManifest != null &&
+ fragment != null &&
+ merger.process(currentManifest, fragment);
+ }
+
+ /**
+ * Makes a backup of the given file, if it exists, by renaming it to name~
+ * (and removing an old name~ file if it exists)
+ */
+ private static boolean makeBackup(File file) {
+ if (!file.exists()) {
+ return true;
+ }
+ if (file.isDirectory()) {
+ return false;
+ }
+
+ File backupFile = new File(file.getParentFile(), file.getName() + '~');
+ if (backupFile.exists()) {
+ backupFile.delete();
+ }
+ return file.renameTo(backupFile);
+ }
+
+ private static String getResourceId(Element element) {
+ String name = element.getAttribute(ATTR_NAME);
+ if (name == null) {
+ name = element.getAttribute(ATTR_ID);
+ }
+
+ return name;
+ }
+
+ /** Instantiates the given template file into the given output file */
+ private void instantiate(
+ @NonNull final Configuration freemarker,
+ @NonNull final Map<String, Object> paramMap,
+ @NonNull String relativeFrom,
+ @NonNull IPath to) throws IOException, TemplateException {
+ // 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 = relativeFrom.indexOf('.') == -1;
+ if (isDirectory) {
+ // It's a directory
+ copyTemplateResource(relativeFrom, to);
+ } else {
+ File from = getFullPath(relativeFrom);
+ mLoader.setTemplateFile(from);
+ Template template = freemarker.getTemplate(from.getName());
+ Writer out = new StringWriter(1024);
+ template.process(paramMap, out);
+ out.flush();
+ String contents = out.toString();
+
+ contents = format(mProject, contents, to);
+ IFile targetFile = getTargetFile(to);
+ TextFileChange change = createNewFileChange(targetFile);
+ MultiTextEdit rootEdit = new MultiTextEdit();
+ rootEdit.addChild(new InsertEdit(0, contents));
+ change.setEdit(rootEdit);
+ mTextChanges.add(change);
+ }
+ }
+
+ private static String format(IProject project, String contents, IPath to) {
+ String name = to.lastSegment();
+ if (name.endsWith(DOT_XML)) {
+ XmlFormatStyle formatStyle = EclipseXmlPrettyPrinter.getForFile(to);
+ EclipseXmlFormatPreferences prefs = EclipseXmlFormatPreferences.create();
+ return EclipseXmlPrettyPrinter.prettyPrint(contents, prefs, formatStyle, null);
+ } else if (name.endsWith(DOT_JAVA)) {
+ Map<?, ?> options = null;
+ if (project != null && project.isAccessible()) {
+ try {
+ IJavaProject javaProject = BaseProjectHelper.getJavaProject(project);
+ if (javaProject != null) {
+ options = javaProject.getOptions(true);
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+ if (options == null) {
+ options = JavaCore.getOptions();
+ }
+
+ CodeFormatter formatter = ToolFactory.createCodeFormatter(options);
+
+ try {
+ IDocument doc = new org.eclipse.jface.text.Document();
+ // format the file (the meat and potatoes)
+ doc.set(contents);
+ TextEdit edit = formatter.format(
+ CodeFormatter.K_COMPILATION_UNIT | CodeFormatter.F_INCLUDE_COMMENTS,
+ contents, 0, contents.length(), 0, null);
+ if (edit != null) {
+ edit.apply(doc);
+ }
+
+ return doc.get();
+ } catch (Exception e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ return contents;
+ }
+
+ private static TextFileChange createNewFileChange(IFile targetFile) {
+ String fileName = targetFile.getName();
+ String message;
+ if (targetFile.exists()) {
+ message = String.format("Replace %1$s", fileName);
+ } else {
+ message = String.format("Create %1$s", fileName);
+ }
+
+ TextFileChange change = new TextFileChange(message, targetFile) {
+ @Override
+ protected IDocument acquireDocument(IProgressMonitor pm) throws CoreException {
+ IDocument document = super.acquireDocument(pm);
+
+ // In our case, we know we *always* use this TextFileChange
+ // to *create* files, we're not appending to existing files.
+ // However, due to the following bug we can end up with cached
+ // contents of previously deleted files that happened to have the
+ // same file name:
+ // https://bugs.eclipse.org/bugs/show_bug.cgi?id=390402
+ // Therefore, as a workaround, wipe out the cached contents here
+ if (document.getLength() > 0) {
+ try {
+ document.replace(0, document.getLength(), "");
+ } catch (BadLocationException e) {
+ // pass
+ }
+ }
+
+ return document;
+ }
+ };
+ change.setTextType(fileName.substring(fileName.lastIndexOf('.') + 1));
+ return change;
+ }
+
+ /**
+ * Returns the list of files to open when the template has been created
+ *
+ * @return the list of files to open
+ */
+ @NonNull
+ public List<String> getFilesToOpen() {
+ return mOpen;
+ }
+
+ /**
+ * Returns the list of actions to perform when the template has been created
+ *
+ * @return the list of actions to perform
+ */
+ @NonNull
+ public List<Runnable> getFinalizingActions() {
+ return mFinalizingActions;
+ }
+
+ /** Copy a template resource */
+ private final void copyTemplateResource(
+ @NonNull String relativeFrom,
+ @NonNull IPath output) throws IOException {
+ File from = getFullPath(relativeFrom);
+ copy(from, output);
+ }
+
+ /** Returns true if the given file contains the given bytes */
+ private static boolean isIdentical(@Nullable byte[] data, @NonNull IFile dest) {
+ assert dest.exists();
+ byte[] existing = AdtUtils.readData(dest);
+ return Arrays.equals(existing, data);
+ }
+
+ /**
+ * 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)
+ */
+ private void copy(File src, IPath path) throws IOException {
+ if (src.isDirectory()) {
+ File[] children = src.listFiles();
+ if (children != null) {
+ for (File child : children) {
+ copy(child, path.append(child.getName()));
+ }
+ }
+ } else {
+ IResource dest = mProject.getFile(path);
+ if (dest.exists() && !(dest instanceof IFile)) {// Don't attempt to overwrite a folder
+ assert false : dest.getClass().getName();
+ return;
+ }
+ IFile file = (IFile) dest;
+ String targetName = path.lastSegment();
+ if (dest instanceof IFile) {
+ if (dest.exists() && isIdentical(Files.toByteArray(src), file)) {
+ String label = String.format(
+ "Not overwriting %1$s because the files are identical", targetName);
+ NullChange change = new NullChange(label);
+ change.setEnabled(false);
+ mOtherChanges.add(change);
+ return;
+ }
+ }
+
+ if (targetName.endsWith(DOT_XML)
+ || targetName.endsWith(DOT_JAVA)
+ || targetName.endsWith(DOT_TXT)
+ || targetName.endsWith(DOT_RS)
+ || targetName.endsWith(DOT_AIDL)
+ || targetName.endsWith(DOT_SVG)) {
+
+ String newFile = Files.toString(src, Charsets.UTF_8);
+ newFile = format(mProject, newFile, path);
+
+ TextFileChange addFile = createNewFileChange(file);
+ addFile.setEdit(new InsertEdit(0, newFile));
+ mTextChanges.add(addFile);
+ } else {
+ // Write binary file: Need custom change for that
+ IPath workspacePath = mProject.getFullPath().append(path);
+ mOtherChanges.add(new CreateFileChange(targetName, workspacePath, src));
+ }
+ }
+ }
+
+ /**
+ * A custom {@link TemplateLoader} which locates and provides templates
+ * within the plugin .jar file
+ */
+ private static final class MyTemplateLoader implements TemplateLoader {
+ private String mPrefix;
+
+ public void setPrefix(String prefix) {
+ mPrefix = prefix;
+ }
+
+ public void setTemplateFile(File file) {
+ setTemplateParent(file.getParentFile());
+ }
+
+ public void setTemplateParent(File parent) {
+ mPrefix = parent.getPath();
+ }
+
+ @Override
+ public Reader getReader(Object templateSource, String encoding) throws IOException {
+ URL url = (URL) templateSource;
+ return new InputStreamReader(url.openStream(), encoding);
+ }
+
+ @Override
+ public long getLastModified(Object templateSource) {
+ return 0;
+ }
+
+ @Override
+ public Object findTemplateSource(String name) throws IOException {
+ String path = mPrefix != null ? mPrefix + '/' + name : name;
+ File file = new File(path);
+ if (file.exists()) {
+ return file.toURI().toURL();
+ }
+ return null;
+ }
+
+ @Override
+ public void closeTemplateSource(Object templateSource) throws IOException {
+ }
+ }
+
+ /**
+ * Validates this template to make sure it's supported
+ * @param currentMinSdk the minimum SDK in the project, or -1 or 0 if unknown (e.g. codename)
+ * @param buildApi the build API, or -1 or 0 if unknown (e.g. codename)
+ *
+ * @return a status object with the error, or null if there is no problem
+ */
+ @SuppressWarnings("cast") // In Eclipse 3.6.2 cast below is needed
+ @Nullable
+ public IStatus validateTemplate(int currentMinSdk, int buildApi) {
+ TemplateMetadata template = getTemplate();
+ if (template == null) {
+ return null;
+ }
+ if (!template.isSupported()) {
+ String versionString = (String) AdtPlugin.getDefault().getBundle().getHeaders().get(
+ Constants.BUNDLE_VERSION);
+ Version version = new Version(versionString);
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format("This template requires a more recent version of the " +
+ "Android Eclipse plugin. Please update from version %1$d.%2$d.%3$d.",
+ version.getMajor(), version.getMinor(), version.getMicro()));
+ }
+ int templateMinSdk = template.getMinSdk();
+ if (templateMinSdk > currentMinSdk && currentMinSdk >= 1) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format("This template requires a minimum SDK version of at " +
+ "least %1$d, and the current min version is %2$d",
+ templateMinSdk, currentMinSdk));
+ }
+ int templateMinBuildApi = template.getMinBuildApi();
+ if (templateMinBuildApi > buildApi && buildApi >= 1) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format("This template requires a build target API version of at " +
+ "least %1$d, and the current version is %2$d",
+ templateMinBuildApi, buildApi));
+ }
+
+ return null;
+ }
+}