Home | History | Annotate | Download | only in templates
      1 /*
      2  * Copyright (C) 2012 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.ide.eclipse.adt.internal.wizards.templates;
     17 
     18 import static com.android.SdkConstants.ATTR_PACKAGE;
     19 import static com.android.SdkConstants.DOT_AIDL;
     20 import static com.android.SdkConstants.DOT_FTL;
     21 import static com.android.SdkConstants.DOT_JAVA;
     22 import static com.android.SdkConstants.DOT_RS;
     23 import static com.android.SdkConstants.DOT_SVG;
     24 import static com.android.SdkConstants.DOT_TXT;
     25 import static com.android.SdkConstants.DOT_XML;
     26 import static com.android.SdkConstants.EXT_XML;
     27 import static com.android.SdkConstants.FD_NATIVE_LIBS;
     28 import static com.android.SdkConstants.XMLNS_PREFIX;
     29 import static com.android.ide.eclipse.adt.internal.wizards.templates.InstallDependencyPage.SUPPORT_LIBRARY_NAME;
     30 import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateManager.getTemplateRootFolder;
     31 
     32 import com.android.SdkConstants;
     33 import com.android.annotations.NonNull;
     34 import com.android.annotations.Nullable;
     35 import com.android.annotations.VisibleForTesting;
     36 import com.android.ide.common.xml.XmlFormatStyle;
     37 import com.android.ide.eclipse.adt.AdtPlugin;
     38 import com.android.ide.eclipse.adt.AdtUtils;
     39 import com.android.ide.eclipse.adt.internal.actions.AddSupportJarAction;
     40 import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlFormatPreferences;
     41 import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlPrettyPrinter;
     42 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
     43 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
     44 import com.android.ide.eclipse.adt.internal.sdk.AdtManifestMergeCallback;
     45 import com.android.manifmerger.ManifestMerger;
     46 import com.android.manifmerger.MergerLog;
     47 import com.android.resources.ResourceFolderType;
     48 import com.android.utils.SdkUtils;
     49 import com.google.common.base.Charsets;
     50 import com.google.common.collect.Lists;
     51 import com.google.common.io.Files;
     52 
     53 import freemarker.cache.TemplateLoader;
     54 import freemarker.template.Configuration;
     55 import freemarker.template.DefaultObjectWrapper;
     56 import freemarker.template.Template;
     57 import freemarker.template.TemplateException;
     58 
     59 import org.eclipse.core.resources.IFile;
     60 import org.eclipse.core.resources.IProject;
     61 import org.eclipse.core.resources.IResource;
     62 import org.eclipse.core.runtime.CoreException;
     63 import org.eclipse.core.runtime.IPath;
     64 import org.eclipse.core.runtime.IProgressMonitor;
     65 import org.eclipse.core.runtime.IStatus;
     66 import org.eclipse.core.runtime.Path;
     67 import org.eclipse.core.runtime.Status;
     68 import org.eclipse.jdt.core.IJavaProject;
     69 import org.eclipse.jdt.core.JavaCore;
     70 import org.eclipse.jdt.core.ToolFactory;
     71 import org.eclipse.jdt.core.formatter.CodeFormatter;
     72 import org.eclipse.jface.dialogs.MessageDialog;
     73 import org.eclipse.jface.operation.IRunnableWithProgress;
     74 import org.eclipse.jface.text.BadLocationException;
     75 import org.eclipse.jface.text.IDocument;
     76 import org.eclipse.ltk.core.refactoring.Change;
     77 import org.eclipse.ltk.core.refactoring.NullChange;
     78 import org.eclipse.ltk.core.refactoring.TextFileChange;
     79 import org.eclipse.swt.SWT;
     80 import org.eclipse.text.edits.InsertEdit;
     81 import org.eclipse.text.edits.MultiTextEdit;
     82 import org.eclipse.text.edits.ReplaceEdit;
     83 import org.eclipse.text.edits.TextEdit;
     84 import org.osgi.framework.Constants;
     85 import org.osgi.framework.Version;
     86 import org.w3c.dom.Attr;
     87 import org.w3c.dom.Document;
     88 import org.w3c.dom.Element;
     89 import org.w3c.dom.NamedNodeMap;
     90 import org.w3c.dom.Node;
     91 import org.w3c.dom.NodeList;
     92 import org.xml.sax.Attributes;
     93 import org.xml.sax.SAXException;
     94 import org.xml.sax.helpers.DefaultHandler;
     95 
     96 import java.io.ByteArrayInputStream;
     97 import java.io.File;
     98 import java.io.IOException;
     99 import java.io.InputStreamReader;
    100 import java.io.Reader;
    101 import java.io.StringWriter;
    102 import java.io.Writer;
    103 import java.lang.reflect.InvocationTargetException;
    104 import java.net.URL;
    105 import java.util.ArrayList;
    106 import java.util.Arrays;
    107 import java.util.Collections;
    108 import java.util.HashMap;
    109 import java.util.List;
    110 import java.util.Map;
    111 
    112 import javax.xml.parsers.SAXParser;
    113 import javax.xml.parsers.SAXParserFactory;
    114 
    115 /**
    116  * Handler which manages instantiating FreeMarker templates, copying resources
    117  * and merging into existing files
    118  */
    119 class TemplateHandler {
    120     /** Highest supported format; templates with a higher number will be skipped
    121      * <p>
    122      * <ul>
    123      * <li> 1: Initial format, supported by ADT 20 and up.
    124      * <li> 2: ADT 21 and up. Boolean variables that have a default value and are not
    125      *    edited by the user would end up as strings in ADT 20; now they are always
    126      *    proper Booleans. Templates which rely on this should specify format >= 2.
    127      * <li> 3: The wizard infrastructure passes the {@code isNewProject} boolean variable
    128      *    to indicate whether a wizard is created as part of a new blank project
    129      * <li> 4: The templates now specify dependencies in the recipe file.
    130      * </ul>
    131      */
    132     static final int CURRENT_FORMAT = 4;
    133 
    134     /**
    135      * Special marker indicating that this path refers to the special shared
    136      * resource directory rather than being somewhere inside the root/ directory
    137      * where all template specific resources are found
    138      */
    139     private static final String VALUE_TEMPLATE_DIR = "$TEMPLATEDIR"; //$NON-NLS-1$
    140 
    141     /**
    142      * Directory within the template which contains the resources referenced
    143      * from the template.xml file
    144      */
    145     private static final String DATA_ROOT = "root";      //$NON-NLS-1$
    146 
    147     /**
    148      * Shared resource directory containing common resources shared among
    149      * multiple templates
    150      */
    151     private static final String RESOURCE_ROOT = "resources";   //$NON-NLS-1$
    152 
    153     /** Reserved filename which describes each template */
    154     static final String TEMPLATE_XML = "template.xml";   //$NON-NLS-1$
    155 
    156     // Various tags and attributes used in the template metadata files - template.xml,
    157     // globals.xml.ftl, recipe.xml.ftl, etc.
    158 
    159     static final String TAG_MERGE = "merge";             //$NON-NLS-1$
    160     static final String TAG_EXECUTE = "execute";         //$NON-NLS-1$
    161     static final String TAG_GLOBALS = "globals";         //$NON-NLS-1$
    162     static final String TAG_GLOBAL = "global";           //$NON-NLS-1$
    163     static final String TAG_PARAMETER = "parameter";     //$NON-NLS-1$
    164     static final String TAG_COPY = "copy";               //$NON-NLS-1$
    165     static final String TAG_INSTANTIATE = "instantiate"; //$NON-NLS-1$
    166     static final String TAG_OPEN = "open";               //$NON-NLS-1$
    167     static final String TAG_THUMB = "thumb";             //$NON-NLS-1$
    168     static final String TAG_THUMBS = "thumbs";           //$NON-NLS-1$
    169     static final String TAG_DEPENDENCY = "dependency";   //$NON-NLS-1$
    170     static final String TAG_ICONS = "icons";             //$NON-NLS-1$
    171     static final String TAG_FORMFACTOR = "formfactor";   //$NON-NLS-1$
    172     static final String TAG_CATEGORY = "category";       //$NON-NLS-1$
    173     static final String ATTR_FORMAT = "format";          //$NON-NLS-1$
    174     static final String ATTR_REVISION = "revision";      //$NON-NLS-1$
    175     static final String ATTR_VALUE = "value";            //$NON-NLS-1$
    176     static final String ATTR_DEFAULT = "default";        //$NON-NLS-1$
    177     static final String ATTR_SUGGEST = "suggest";        //$NON-NLS-1$
    178     static final String ATTR_ID = "id";                  //$NON-NLS-1$
    179     static final String ATTR_NAME = "name";              //$NON-NLS-1$
    180     static final String ATTR_DESCRIPTION = "description";//$NON-NLS-1$
    181     static final String ATTR_TYPE = "type";              //$NON-NLS-1$
    182     static final String ATTR_HELP = "help";              //$NON-NLS-1$
    183     static final String ATTR_FILE = "file";              //$NON-NLS-1$
    184     static final String ATTR_TO = "to";                  //$NON-NLS-1$
    185     static final String ATTR_FROM = "from";              //$NON-NLS-1$
    186     static final String ATTR_CONSTRAINTS = "constraints";//$NON-NLS-1$
    187     static final String ATTR_BACKGROUND = "background";  //$NON-NLS-1$
    188     static final String ATTR_FOREGROUND = "foreground";  //$NON-NLS-1$
    189     static final String ATTR_SHAPE = "shape";            //$NON-NLS-1$
    190     static final String ATTR_TRIM = "trim";              //$NON-NLS-1$
    191     static final String ATTR_PADDING = "padding";        //$NON-NLS-1$
    192     static final String ATTR_SOURCE_TYPE = "source";     //$NON-NLS-1$
    193     static final String ATTR_CLIPART_NAME = "clipartName";//$NON-NLS-1$
    194     static final String ATTR_TEXT = "text";              //$NON-NLS-1$
    195     static final String ATTR_SRC_DIR = "srcDir";         //$NON-NLS-1$
    196     static final String ATTR_SRC_OUT = "srcOut";         //$NON-NLS-1$
    197     static final String ATTR_RES_DIR = "resDir";         //$NON-NLS-1$
    198     static final String ATTR_RES_OUT = "resOut";         //$NON-NLS-1$
    199     static final String ATTR_MANIFEST_DIR = "manifestDir";//$NON-NLS-1$
    200     static final String ATTR_MANIFEST_OUT = "manifestOut";//$NON-NLS-1$
    201     static final String ATTR_PROJECT_DIR = "projectDir"; //$NON-NLS-1$
    202     static final String ATTR_PROJECT_OUT = "projectOut"; //$NON-NLS-1$
    203     static final String ATTR_MAVEN_URL = "mavenUrl";     //$NON-NLS-1$
    204     static final String ATTR_DEBUG_KEYSTORE_SHA1 =
    205     		"debugKeystoreSha1";                         //$NON-NLS-1$
    206 
    207     static final String CATEGORY_ACTIVITIES = "activities";//$NON-NLS-1$
    208     static final String CATEGORY_PROJECTS = "projects";    //$NON-NLS-1$
    209     static final String CATEGORY_OTHER = "other";          //$NON-NLS-1$
    210 
    211     static final String MAVEN_SUPPORT_V4 = "support-v4";   //$NON-NLS-1$
    212     static final String MAVEN_SUPPORT_V13 = "support-v13"; //$NON-NLS-1$
    213     static final String MAVEN_APPCOMPAT = "appcompat-v7";  //$NON-NLS-1$
    214 
    215     /** Default padding to apply in wizards around the thumbnail preview images */
    216     static final int PREVIEW_PADDING = 10;
    217 
    218     /** Default width to scale thumbnail preview images in wizards to */
    219     static final int PREVIEW_WIDTH = 200;
    220 
    221     /**
    222      * List of files to open after the wizard has been created (these are
    223      * identified by {@link #TAG_OPEN} elements in the recipe file
    224      */
    225     private final List<String> mOpen = Lists.newArrayList();
    226 
    227     /**
    228      * List of actions to perform after the wizard has finished.
    229      */
    230     protected List<Runnable> mFinalizingActions = Lists.newArrayList();
    231 
    232     /** Path to the directory containing the templates */
    233     @NonNull
    234     private final File mRootPath;
    235 
    236     /** The changes being processed by the template handler */
    237     private List<Change> mMergeChanges;
    238     private List<Change> mTextChanges;
    239     private List<Change> mOtherChanges;
    240 
    241     /** The project to write the template into */
    242     private IProject mProject;
    243 
    244     /** The template loader which is responsible for finding (and sharing) template files */
    245     private final MyTemplateLoader mLoader;
    246 
    247     /** Agree to all file-overwrites from now on? */
    248     private boolean mYesToAll = false;
    249 
    250     /** Is writing the template cancelled? */
    251     private boolean mNoToAll = false;
    252 
    253     /**
    254      * Should files that we merge contents into be backed up? If yes, will
    255      * create emacs-style tilde-file backups (filename.xml~)
    256      */
    257     private boolean mBackupMergedFiles = true;
    258 
    259     /**
    260      * Template metadata
    261      */
    262     private TemplateMetadata mTemplate;
    263 
    264     private final TemplateManager mManager;
    265 
    266     /** Creates a new {@link TemplateHandler} for the given root path */
    267     static TemplateHandler createFromPath(File rootPath) {
    268         return new TemplateHandler(rootPath, new TemplateManager());
    269     }
    270 
    271     /** Creates a new {@link TemplateHandler} for the template name, which should
    272      * be relative to the templates directory */
    273     static TemplateHandler createFromName(String category, String name) {
    274         TemplateManager manager = new TemplateManager();
    275 
    276         // Use the TemplateManager iteration which should merge contents between the
    277         // extras/templates/ and tools/templates folders and pick the most recent version
    278         List<File> templates = manager.getTemplates(category);
    279         for (File file : templates) {
    280             if (file.getName().equals(name) && category.equals(file.getParentFile().getName())) {
    281                 return new TemplateHandler(file, manager);
    282             }
    283         }
    284 
    285         return new TemplateHandler(new File(getTemplateRootFolder(),
    286                 category + File.separator + name), manager);
    287     }
    288 
    289     private TemplateHandler(File rootPath, TemplateManager manager) {
    290         mRootPath = rootPath;
    291         mManager = manager;
    292         mLoader = new MyTemplateLoader();
    293         mLoader.setPrefix(mRootPath.getPath());
    294     }
    295 
    296     public TemplateManager getManager() {
    297         return mManager;
    298     }
    299 
    300     public void setBackupMergedFiles(boolean backupMergedFiles) {
    301         mBackupMergedFiles = backupMergedFiles;
    302     }
    303 
    304     @NonNull
    305     public List<Change> render(IProject project, Map<String, Object> args) {
    306         mOpen.clear();
    307 
    308         mProject = project;
    309         mMergeChanges = new ArrayList<Change>();
    310         mTextChanges = new ArrayList<Change>();
    311         mOtherChanges = new ArrayList<Change>();
    312 
    313         // Render the instruction list template.
    314         Map<String, Object> paramMap = createParameterMap(args);
    315         Configuration freemarker = new Configuration();
    316         freemarker.setObjectWrapper(new DefaultObjectWrapper());
    317         freemarker.setTemplateLoader(mLoader);
    318 
    319         processVariables(freemarker, TEMPLATE_XML, paramMap);
    320 
    321         // Add the changes in the order where merges are shown first, then text files,
    322         // and finally other files (like jars and icons which don't have previews).
    323         List<Change> changes = new ArrayList<Change>();
    324         changes.addAll(mMergeChanges);
    325         changes.addAll(mTextChanges);
    326         changes.addAll(mOtherChanges);
    327         return changes;
    328     }
    329 
    330     Map<String, Object> createParameterMap(Map<String, Object> args) {
    331         final Map<String, Object> paramMap = createBuiltinMap();
    332 
    333         // Wizard parameters supplied by user, specific to this template
    334         paramMap.putAll(args);
    335 
    336         return paramMap;
    337     }
    338 
    339     /** Data model for the templates */
    340     static Map<String, Object> createBuiltinMap() {
    341         // Create the data model.
    342         final Map<String, Object> paramMap = new HashMap<String, Object>();
    343 
    344         // Builtin conversion methods
    345         paramMap.put("slashedPackageName", new FmSlashedPackageNameMethod());       //$NON-NLS-1$
    346         paramMap.put("camelCaseToUnderscore", new FmCamelCaseToUnderscoreMethod()); //$NON-NLS-1$
    347         paramMap.put("underscoreToCamelCase", new FmUnderscoreToCamelCaseMethod()); //$NON-NLS-1$
    348         paramMap.put("activityToLayout", new FmActivityToLayoutMethod());           //$NON-NLS-1$
    349         paramMap.put("layoutToActivity", new FmLayoutToActivityMethod());           //$NON-NLS-1$
    350         paramMap.put("classToResource", new FmClassNameToResourceMethod());         //$NON-NLS-1$
    351         paramMap.put("escapeXmlAttribute", new FmEscapeXmlStringMethod());          //$NON-NLS-1$
    352         paramMap.put("escapeXmlText", new FmEscapeXmlStringMethod());               //$NON-NLS-1$
    353         paramMap.put("escapeXmlString", new FmEscapeXmlStringMethod());             //$NON-NLS-1$
    354         paramMap.put("extractLetters", new FmExtractLettersMethod());               //$NON-NLS-1$
    355 
    356         // This should be handled better: perhaps declared "required packages" as part of the
    357         // inputs? (It would be better if we could conditionally disable template based
    358         // on availability)
    359         Map<String, String> builtin = new HashMap<String, String>();
    360         builtin.put("templatesRes", VALUE_TEMPLATE_DIR); //$NON-NLS-1$
    361         paramMap.put("android", builtin);                //$NON-NLS-1$
    362 
    363         return paramMap;
    364     }
    365 
    366     static void addDirectoryParameters(Map<String, Object> parameters, IProject project) {
    367         IPath srcDir = project.getFile(SdkConstants.SRC_FOLDER).getProjectRelativePath();
    368         parameters.put(ATTR_SRC_DIR, srcDir.toString());
    369 
    370         IPath resDir = project.getFile(SdkConstants.RES_FOLDER).getProjectRelativePath();
    371         parameters.put(ATTR_RES_DIR, resDir.toString());
    372 
    373         IPath manifestDir = project.getProjectRelativePath();
    374         parameters.put(ATTR_MANIFEST_DIR, manifestDir.toString());
    375         parameters.put(ATTR_MANIFEST_OUT, manifestDir.toString());
    376 
    377         parameters.put(ATTR_PROJECT_DIR, manifestDir.toString());
    378         parameters.put(ATTR_PROJECT_OUT, manifestDir.toString());
    379 
    380         parameters.put(ATTR_DEBUG_KEYSTORE_SHA1, "");
    381     }
    382 
    383     @Nullable
    384     public TemplateMetadata getTemplate() {
    385         if (mTemplate == null) {
    386             mTemplate = mManager.getTemplate(mRootPath);
    387         }
    388 
    389         return mTemplate;
    390     }
    391 
    392     @NonNull
    393     public String getResourcePath(String templateName) {
    394         return new File(mRootPath.getPath(), templateName).getPath();
    395     }
    396 
    397     /**
    398      * Load a text resource for the given relative path within the template
    399      *
    400      * @param relativePath relative path within the template
    401      * @return the string contents of the template text file
    402      */
    403     @Nullable
    404     public String readTemplateTextResource(@NonNull String relativePath) {
    405         try {
    406             return Files.toString(new File(mRootPath,
    407                     relativePath.replace('/', File.separatorChar)), Charsets.UTF_8);
    408         } catch (IOException e) {
    409             AdtPlugin.log(e, null);
    410             return null;
    411         }
    412     }
    413 
    414     @Nullable
    415     public String readTemplateTextResource(@NonNull File file) {
    416         assert file.isAbsolute();
    417         try {
    418             return Files.toString(file, Charsets.UTF_8);
    419         } catch (IOException e) {
    420             AdtPlugin.log(e, null);
    421             return null;
    422         }
    423     }
    424 
    425     /**
    426      * Reads the contents of a resource
    427      *
    428      * @param relativePath the path relative to the template directory
    429      * @return the binary data read from the file
    430      */
    431     @Nullable
    432     public byte[] readTemplateResource(@NonNull String relativePath) {
    433         try {
    434             return Files.toByteArray(new File(mRootPath, relativePath));
    435         } catch (IOException e) {
    436             AdtPlugin.log(e, null);
    437             return null;
    438         }
    439     }
    440 
    441     /**
    442      * Most recent thrown exception during template instantiation. This should
    443      * basically always be null. Used by unit tests to see if any template
    444      * instantiation recorded a failure.
    445      */
    446     @VisibleForTesting
    447     public static Exception sMostRecentException;
    448 
    449     /** Read the given FreeMarker file and process the variable definitions */
    450     private void processVariables(final Configuration freemarker,
    451             String file, final Map<String, Object> paramMap) {
    452         try {
    453             String xml;
    454             if (file.endsWith(DOT_XML)) {
    455                 // Just read the file
    456                 xml = readTemplateTextResource(file);
    457                 if (xml == null) {
    458                     return;
    459                 }
    460             } else {
    461                 mLoader.setTemplateFile(new File(mRootPath, file));
    462                 Template inputsTemplate = freemarker.getTemplate(file);
    463                 StringWriter out = new StringWriter();
    464                 inputsTemplate.process(paramMap, out);
    465                 out.flush();
    466                 xml = out.toString();
    467             }
    468 
    469             SAXParserFactory factory = SAXParserFactory.newInstance();
    470             SAXParser saxParser = factory.newSAXParser();
    471             saxParser.parse(new ByteArrayInputStream(xml.getBytes()), new DefaultHandler() {
    472                 @Override
    473                 public void startElement(String uri, String localName, String name,
    474                         Attributes attributes)
    475                                 throws SAXException {
    476                     if (TAG_PARAMETER.equals(name)) {
    477                         String id = attributes.getValue(ATTR_ID);
    478                         if (!paramMap.containsKey(id)) {
    479                             String value = attributes.getValue(ATTR_DEFAULT);
    480                             Object mapValue = value;
    481                             if (value != null && !value.isEmpty()) {
    482                                 String type = attributes.getValue(ATTR_TYPE);
    483                                 if ("boolean".equals(type)) { //$NON-NLS-1$
    484                                     mapValue = Boolean.valueOf(value);
    485                                 }
    486                             }
    487                             paramMap.put(id, mapValue);
    488                         }
    489                     } else if (TAG_GLOBAL.equals(name)) {
    490                         String id = attributes.getValue(ATTR_ID);
    491                         if (!paramMap.containsKey(id)) {
    492                         	paramMap.put(id, TypedVariable.parseGlobal(attributes));
    493                         }
    494                     } else if (TAG_GLOBALS.equals(name)) {
    495                         // Handle evaluation of variables
    496                         String path = attributes.getValue(ATTR_FILE);
    497                         if (path != null) {
    498                             processVariables(freemarker, path, paramMap);
    499                         } // else: <globals> root element
    500                     } else if (TAG_EXECUTE.equals(name)) {
    501                         String path = attributes.getValue(ATTR_FILE);
    502                         if (path != null) {
    503                             execute(freemarker, path, paramMap);
    504                         }
    505                     } else if (TAG_DEPENDENCY.equals(name)) {
    506                         String dependencyName = attributes.getValue(ATTR_NAME);
    507                         if (dependencyName.equals(SUPPORT_LIBRARY_NAME)) {
    508                             // We assume the revision requirement has been satisfied
    509                             // by the wizard
    510                             File path = AddSupportJarAction.getSupportJarFile();
    511                             if (path != null) {
    512                                 IPath to = getTargetPath(FD_NATIVE_LIBS +'/' + path.getName());
    513                                 try {
    514                                     copy(path, to);
    515                                 } catch (IOException ioe) {
    516                                     AdtPlugin.log(ioe, null);
    517                                 }
    518                             }
    519                         }
    520                     } else if (!name.equals("template") && !name.equals(TAG_CATEGORY) &&
    521                     		!name.equals(TAG_FORMFACTOR) && !name.equals("option") &&
    522                     		!name.equals(TAG_THUMBS) && !name.equals(TAG_THUMB) &&
    523                     		!name.equals(TAG_ICONS)) {
    524                         System.err.println("WARNING: Unknown template directive " + name);
    525                     }
    526                 }
    527             });
    528         } catch (Exception e) {
    529             sMostRecentException = e;
    530             AdtPlugin.log(e, null);
    531         }
    532     }
    533 
    534     @SuppressWarnings("unused")
    535     private boolean canOverwrite(File file) {
    536         if (file.exists()) {
    537             // Warn that the file already exists and ask the user what to do
    538             if (!mYesToAll) {
    539                 MessageDialog dialog = new MessageDialog(null, "File Already Exists", null,
    540                         String.format(
    541                                 "%1$s already exists.\nWould you like to replace it?",
    542                                 file.getPath()),
    543                                 MessageDialog.QUESTION, new String[] {
    544                     // Yes will be moved to the end because it's the default
    545                     "Yes", "No", "Cancel", "Yes to All"
    546                 }, 0);
    547                 int result = dialog.open();
    548                 switch (result) {
    549                 case 0:
    550                     // Yes
    551                     break;
    552                 case 3:
    553                     // Yes to all
    554                     mYesToAll = true;
    555                     break;
    556                 case 1:
    557                     // No
    558                     return false;
    559                 case SWT.DEFAULT:
    560                 case 2:
    561                     // Cancel
    562                     mNoToAll = true;
    563                     return false;
    564                 }
    565             }
    566 
    567             if (mBackupMergedFiles) {
    568                 return makeBackup(file);
    569             } else {
    570                 return file.delete();
    571             }
    572         }
    573 
    574         return true;
    575     }
    576 
    577     /** Executes the given recipe file: copying, merging, instantiating, opening files etc */
    578     private void execute(
    579             final Configuration freemarker,
    580             String file,
    581             final Map<String, Object> paramMap) {
    582         try {
    583             mLoader.setTemplateFile(new File(mRootPath, file));
    584             Template freemarkerTemplate = freemarker.getTemplate(file);
    585 
    586             StringWriter out = new StringWriter();
    587             freemarkerTemplate.process(paramMap, out);
    588             out.flush();
    589             String xml = out.toString();
    590 
    591             // Parse and execute the resulting instruction list.
    592             SAXParserFactory factory = SAXParserFactory.newInstance();
    593             SAXParser saxParser = factory.newSAXParser();
    594 
    595             saxParser.parse(new ByteArrayInputStream(xml.getBytes()),
    596                     new DefaultHandler() {
    597                 @Override
    598                 public void startElement(String uri, String localName, String name,
    599                         Attributes attributes)
    600                                 throws SAXException {
    601                     if (mNoToAll) {
    602                         return;
    603                     }
    604 
    605                     try {
    606                         boolean instantiate = TAG_INSTANTIATE.equals(name);
    607                         if (TAG_COPY.equals(name) || instantiate) {
    608                             String fromPath = attributes.getValue(ATTR_FROM);
    609                             String toPath = attributes.getValue(ATTR_TO);
    610                             if (toPath == null || toPath.isEmpty()) {
    611                                 toPath = attributes.getValue(ATTR_FROM);
    612                                 toPath = AdtUtils.stripSuffix(toPath, DOT_FTL);
    613                             }
    614                             IPath to = getTargetPath(toPath);
    615                             if (instantiate) {
    616                                 instantiate(freemarker, paramMap, fromPath, to);
    617                             } else {
    618                                 copyTemplateResource(fromPath, to);
    619                             }
    620                         } else if (TAG_MERGE.equals(name)) {
    621                             String fromPath = attributes.getValue(ATTR_FROM);
    622                             String toPath = attributes.getValue(ATTR_TO);
    623                             if (toPath == null || toPath.isEmpty()) {
    624                                 toPath = attributes.getValue(ATTR_FROM);
    625                                 toPath = AdtUtils.stripSuffix(toPath, DOT_FTL);
    626                             }
    627                             // Resources in template.xml are located within root/
    628                             IPath to = getTargetPath(toPath);
    629                             merge(freemarker, paramMap, fromPath, to);
    630                         } else if (name.equals(TAG_OPEN)) {
    631                             // The relative path here is within the output directory:
    632                             String relativePath = attributes.getValue(ATTR_FILE);
    633                             if (relativePath != null && !relativePath.isEmpty()) {
    634                                 mOpen.add(relativePath);
    635                             }
    636                         } else if (TAG_DEPENDENCY.equals(name)) {
    637                             String dependencyUrl = attributes.getValue(ATTR_MAVEN_URL);
    638                             File path;
    639                             if (dependencyUrl.contains(MAVEN_SUPPORT_V4)) {
    640                                 // We assume the revision requirement has been satisfied
    641                                 // by the wizard
    642                                 path = AddSupportJarAction.getSupportJarFile();
    643                             } else if (dependencyUrl.contains(MAVEN_SUPPORT_V13)) {
    644                                 path = AddSupportJarAction.getSupport13JarFile();
    645                             } else if (dependencyUrl.contains(MAVEN_APPCOMPAT)) {
    646                                 path = null;
    647                                 mFinalizingActions.add(new Runnable() {
    648                                     @Override
    649                                     public void run() {
    650                                         AddSupportJarAction.installAppCompatLibrary(mProject, true);
    651                                     }
    652                                 });
    653                             } else {
    654                                 path = null;
    655                                 System.err.println("WARNING: Unknown dependency type");
    656                             }
    657 
    658                             if (path != null) {
    659                                 IPath to = getTargetPath(FD_NATIVE_LIBS +'/' + path.getName());
    660                                 try {
    661                                     copy(path, to);
    662                                 } catch (IOException ioe) {
    663                                     AdtPlugin.log(ioe, null);
    664                                 }
    665                             }
    666                         } else if (!name.equals("recipe") && !name.equals(TAG_DEPENDENCY)) { //$NON-NLS-1$
    667                             System.err.println("WARNING: Unknown template directive " + name);
    668                         }
    669                     } catch (Exception e) {
    670                         sMostRecentException = e;
    671                         AdtPlugin.log(e, null);
    672                     }
    673                 }
    674             });
    675 
    676         } catch (Exception e) {
    677             sMostRecentException = e;
    678             AdtPlugin.log(e, null);
    679         }
    680     }
    681 
    682     @NonNull
    683     private File getFullPath(@NonNull String fromPath) {
    684         if (fromPath.startsWith(VALUE_TEMPLATE_DIR)) {
    685             return new File(getTemplateRootFolder(), RESOURCE_ROOT + File.separator
    686                     + fromPath.substring(VALUE_TEMPLATE_DIR.length() + 1).replace('/',
    687                             File.separatorChar));
    688         }
    689         return new File(mRootPath, DATA_ROOT + File.separator + fromPath);
    690     }
    691 
    692     @NonNull
    693     private IPath getTargetPath(@NonNull String relative) {
    694         if (relative.indexOf('\\') != -1) {
    695             relative = relative.replace('\\', '/');
    696         }
    697         return new Path(relative);
    698     }
    699 
    700     @NonNull
    701     private IFile getTargetFile(@NonNull IPath path) {
    702         return mProject.getFile(path);
    703     }
    704 
    705     private void merge(
    706             @NonNull final Configuration freemarker,
    707             @NonNull final Map<String, Object> paramMap,
    708             @NonNull String relativeFrom,
    709             @NonNull IPath toPath) throws IOException, TemplateException {
    710 
    711         String currentXml = null;
    712 
    713         IFile to = getTargetFile(toPath);
    714         if (to.exists()) {
    715             currentXml = AdtPlugin.readFile(to);
    716         }
    717 
    718         if (currentXml == null) {
    719             // The target file doesn't exist: don't merge, just copy
    720             boolean instantiate = relativeFrom.endsWith(DOT_FTL);
    721             if (instantiate) {
    722                 instantiate(freemarker, paramMap, relativeFrom, toPath);
    723             } else {
    724                 copyTemplateResource(relativeFrom, toPath);
    725             }
    726             return;
    727         }
    728 
    729         if (!to.getFileExtension().equals(EXT_XML)) {
    730             throw new RuntimeException("Only XML files can be merged at this point: " + to);
    731         }
    732 
    733         String xml = null;
    734         File from = getFullPath(relativeFrom);
    735         if (relativeFrom.endsWith(DOT_FTL)) {
    736             // Perform template substitution of the template prior to merging
    737             mLoader.setTemplateFile(from);
    738             Template template = freemarker.getTemplate(from.getName());
    739             Writer out = new StringWriter();
    740             template.process(paramMap, out);
    741             out.flush();
    742             xml = out.toString();
    743         } else {
    744             xml = readTemplateTextResource(from);
    745             if (xml == null) {
    746                 return;
    747             }
    748         }
    749 
    750         Document currentDocument = DomUtilities.parseStructuredDocument(currentXml);
    751         assert currentDocument != null : currentXml;
    752         Document fragment = DomUtilities.parseStructuredDocument(xml);
    753         assert fragment != null : xml;
    754 
    755         XmlFormatStyle formatStyle = XmlFormatStyle.MANIFEST;
    756         boolean modified;
    757         boolean ok;
    758         String fileName = to.getName();
    759         if (fileName.equals(SdkConstants.FN_ANDROID_MANIFEST_XML)) {
    760             modified = ok = mergeManifest(currentDocument, fragment);
    761         } else {
    762             // Merge plain XML files
    763             String parentFolderName = to.getParent().getName();
    764             ResourceFolderType folderType = ResourceFolderType.getFolderType(parentFolderName);
    765             if (folderType != null) {
    766                 formatStyle = EclipseXmlPrettyPrinter.getForFile(toPath);
    767             } else {
    768                 formatStyle = XmlFormatStyle.FILE;
    769             }
    770 
    771             modified = mergeResourceFile(currentDocument, fragment, folderType, paramMap);
    772             ok = true;
    773         }
    774 
    775         // Finally write out the merged file (formatting etc)
    776         String contents = null;
    777         if (ok) {
    778             if (modified) {
    779                 contents = EclipseXmlPrettyPrinter.prettyPrint(currentDocument,
    780                         EclipseXmlFormatPreferences.create(), formatStyle, null,
    781                         currentXml.endsWith("\n")); //$NON-NLS-1$
    782             }
    783         } else {
    784             // Just insert into file along with comment, using the "standard" conflict
    785             // syntax that many tools and editors recognize.
    786             String sep = SdkUtils.getLineSeparator();
    787             contents =
    788                     "<<<<<<< Original" + sep
    789                     + currentXml + sep
    790                     + "=======" + sep
    791                     + xml
    792                     + ">>>>>>> Added" + sep;
    793         }
    794 
    795         if (contents != null) {
    796             TextFileChange change = new TextFileChange("Merge " + fileName, to);
    797             MultiTextEdit rootEdit = new MultiTextEdit();
    798             rootEdit.addChild(new ReplaceEdit(0, currentXml.length(), contents));
    799             change.setEdit(rootEdit);
    800             change.setTextType(SdkConstants.EXT_XML);
    801             mMergeChanges.add(change);
    802         }
    803     }
    804 
    805     /** Merges the given resource file contents into the given resource file
    806      * @param paramMap */
    807     private static boolean mergeResourceFile(Document currentDocument, Document fragment,
    808             ResourceFolderType folderType, Map<String, Object> paramMap) {
    809         boolean modified = false;
    810 
    811         // Copy namespace declarations
    812         NamedNodeMap attributes = fragment.getDocumentElement().getAttributes();
    813         if (attributes != null) {
    814             for (int i = 0, n = attributes.getLength(); i < n; i++) {
    815                 Attr attribute = (Attr) attributes.item(i);
    816                 if (attribute.getName().startsWith(XMLNS_PREFIX)) {
    817                     currentDocument.getDocumentElement().setAttribute(attribute.getName(),
    818                             attribute.getValue());
    819                 }
    820             }
    821         }
    822 
    823         // For layouts for example, I want to *append* inside the root all the
    824         // contents of the new file.
    825         // But for resources for example, I want to combine elements which specify
    826         // the same name or id attribute.
    827         // For elements like manifest files we need to insert stuff at the right
    828         // location in a nested way (activities in the application element etc)
    829         // but that doesn't happen for the other file types.
    830         Element root = fragment.getDocumentElement();
    831         NodeList children = root.getChildNodes();
    832         List<Node> nodes = new ArrayList<Node>(children.getLength());
    833         for (int i = children.getLength() - 1; i >= 0; i--) {
    834             Node child = children.item(i);
    835             nodes.add(child);
    836             root.removeChild(child);
    837         }
    838         Collections.reverse(nodes);
    839 
    840         root = currentDocument.getDocumentElement();
    841 
    842         if (folderType == ResourceFolderType.VALUES) {
    843             // Try to merge items of the same name
    844             Map<String, Node> old = new HashMap<String, Node>();
    845             NodeList newSiblings = root.getChildNodes();
    846             for (int i = newSiblings.getLength() - 1; i >= 0; i--) {
    847                 Node child = newSiblings.item(i);
    848                 if (child.getNodeType() == Node.ELEMENT_NODE) {
    849                     Element element = (Element) child;
    850                     String name = getResourceId(element);
    851                     if (name != null) {
    852                         old.put(name, element);
    853                     }
    854                 }
    855             }
    856 
    857             for (Node node : nodes) {
    858                 if (node.getNodeType() == Node.ELEMENT_NODE) {
    859                     Element element = (Element) node;
    860                     String name = getResourceId(element);
    861                     Node replace = name != null ? old.get(name) : null;
    862                     if (replace != null) {
    863                         // There is an existing item with the same id: just replace it
    864                         // ACTUALLY -- let's NOT change it.
    865                         // Let's say you've used the activity wizard once, and it
    866                         // emits some configuration parameter as a resource that
    867                         // it depends on, say "padding". Then the user goes and
    868                         // tweaks the padding to some other number.
    869                         // Now running the wizard a *second* time for some new activity,
    870                         // we should NOT go and set the value back to the template's
    871                         // default!
    872                         //root.replaceChild(node, replace);
    873 
    874                         // ... ON THE OTHER HAND... What if it's a parameter class
    875                         // (where the template rewrites a common attribute). Here it's
    876                         // really confusing if the new parameter is not set. This is
    877                         // really an error in the template, since we shouldn't have conflicts
    878                         // like that, but we need to do something to help track this down.
    879                         AdtPlugin.log(null,
    880                                 "Warning: Ignoring name conflict in resource file for name %1$s",
    881                                 name);
    882                     } else {
    883                         root.appendChild(node);
    884                         modified = true;
    885                     }
    886                 }
    887             }
    888         } else {
    889             // In other file types, such as layouts, just append all the new content
    890             // at the end.
    891             for (Node node : nodes) {
    892                 root.appendChild(node);
    893                 modified = true;
    894             }
    895         }
    896         return modified;
    897     }
    898 
    899     /** Merges the given manifest fragment into the given manifest file */
    900     private static boolean mergeManifest(Document currentManifest, Document fragment) {
    901         // TODO change MergerLog.wrapSdkLog by a custom IMergerLog that will create
    902         // and maintain error markers.
    903 
    904         // Transfer package element from manifest to merged in root; required by
    905         // manifest merger
    906         Element fragmentRoot = fragment.getDocumentElement();
    907         Element manifestRoot = currentManifest.getDocumentElement();
    908         if (fragmentRoot == null || manifestRoot == null) {
    909             return false;
    910         }
    911         String pkg = fragmentRoot.getAttribute(ATTR_PACKAGE);
    912         if (pkg == null || pkg.isEmpty()) {
    913             pkg = manifestRoot.getAttribute(ATTR_PACKAGE);
    914             if (pkg != null && !pkg.isEmpty()) {
    915                 fragmentRoot.setAttribute(ATTR_PACKAGE, pkg);
    916             }
    917         }
    918 
    919         ManifestMerger merger = new ManifestMerger(
    920                 MergerLog.wrapSdkLog(AdtPlugin.getDefault()),
    921                 new AdtManifestMergeCallback()).setExtractPackagePrefix(true);
    922         return currentManifest != null &&
    923                 fragment != null &&
    924                 merger.process(currentManifest, fragment);
    925     }
    926 
    927     /**
    928      * Makes a backup of the given file, if it exists, by renaming it to name~
    929      * (and removing an old name~ file if it exists)
    930      */
    931     private static boolean makeBackup(File file) {
    932         if (!file.exists()) {
    933             return true;
    934         }
    935         if (file.isDirectory()) {
    936             return false;
    937         }
    938 
    939         File backupFile = new File(file.getParentFile(), file.getName() + '~');
    940         if (backupFile.exists()) {
    941             backupFile.delete();
    942         }
    943         return file.renameTo(backupFile);
    944     }
    945 
    946     private static String getResourceId(Element element) {
    947         String name = element.getAttribute(ATTR_NAME);
    948         if (name == null) {
    949             name = element.getAttribute(ATTR_ID);
    950         }
    951 
    952         return name;
    953     }
    954 
    955     /** Instantiates the given template file into the given output file */
    956     private void instantiate(
    957             @NonNull final Configuration freemarker,
    958             @NonNull final Map<String, Object> paramMap,
    959             @NonNull String relativeFrom,
    960             @NonNull IPath to) throws IOException, TemplateException {
    961         // For now, treat extension-less files as directories... this isn't quite right
    962         // so I should refine this! Maybe with a unique attribute in the template file?
    963         boolean isDirectory = relativeFrom.indexOf('.') == -1;
    964         if (isDirectory) {
    965             // It's a directory
    966             copyTemplateResource(relativeFrom, to);
    967         } else {
    968             File from = getFullPath(relativeFrom);
    969             mLoader.setTemplateFile(from);
    970             Template template = freemarker.getTemplate(from.getName());
    971             Writer out = new StringWriter(1024);
    972             template.process(paramMap, out);
    973             out.flush();
    974             String contents = out.toString();
    975 
    976             contents = format(mProject, contents, to);
    977             IFile targetFile = getTargetFile(to);
    978             TextFileChange change = createNewFileChange(targetFile);
    979             MultiTextEdit rootEdit = new MultiTextEdit();
    980             rootEdit.addChild(new InsertEdit(0, contents));
    981             change.setEdit(rootEdit);
    982             mTextChanges.add(change);
    983         }
    984     }
    985 
    986     private static String format(IProject project, String contents, IPath to) {
    987         String name = to.lastSegment();
    988         if (name.endsWith(DOT_XML)) {
    989             XmlFormatStyle formatStyle = EclipseXmlPrettyPrinter.getForFile(to);
    990             EclipseXmlFormatPreferences prefs = EclipseXmlFormatPreferences.create();
    991             return EclipseXmlPrettyPrinter.prettyPrint(contents, prefs, formatStyle, null);
    992         } else if (name.endsWith(DOT_JAVA)) {
    993             Map<?, ?> options = null;
    994             if (project != null && project.isAccessible()) {
    995                 try {
    996                     IJavaProject javaProject = BaseProjectHelper.getJavaProject(project);
    997                     if (javaProject != null) {
    998                         options = javaProject.getOptions(true);
    999                     }
   1000                 } catch (CoreException e) {
   1001                     AdtPlugin.log(e, null);
   1002                 }
   1003             }
   1004             if (options == null) {
   1005                 options = JavaCore.getOptions();
   1006             }
   1007 
   1008             CodeFormatter formatter = ToolFactory.createCodeFormatter(options);
   1009 
   1010             try {
   1011                 IDocument doc = new org.eclipse.jface.text.Document();
   1012                 // format the file (the meat and potatoes)
   1013                 doc.set(contents);
   1014                 TextEdit edit = formatter.format(
   1015                         CodeFormatter.K_COMPILATION_UNIT | CodeFormatter.F_INCLUDE_COMMENTS,
   1016                         contents, 0, contents.length(), 0, null);
   1017                 if (edit != null) {
   1018                     edit.apply(doc);
   1019                 }
   1020 
   1021                 return doc.get();
   1022             } catch (Exception e) {
   1023                 AdtPlugin.log(e, null);
   1024             }
   1025         }
   1026 
   1027         return contents;
   1028     }
   1029 
   1030     private static TextFileChange createNewFileChange(IFile targetFile) {
   1031         String fileName = targetFile.getName();
   1032         String message;
   1033         if (targetFile.exists()) {
   1034             message = String.format("Replace %1$s", fileName);
   1035         } else {
   1036             message = String.format("Create %1$s", fileName);
   1037         }
   1038 
   1039         TextFileChange change = new TextFileChange(message, targetFile) {
   1040             @Override
   1041             protected IDocument acquireDocument(IProgressMonitor pm) throws CoreException {
   1042                 IDocument document = super.acquireDocument(pm);
   1043 
   1044                 // In our case, we know we *always* use this TextFileChange
   1045                 // to *create* files, we're not appending to existing files.
   1046                 // However, due to the following bug we can end up with cached
   1047                 // contents of previously deleted files that happened to have the
   1048                 // same file name:
   1049                 //   https://bugs.eclipse.org/bugs/show_bug.cgi?id=390402
   1050                 // Therefore, as a workaround, wipe out the cached contents here
   1051                 if (document.getLength() > 0) {
   1052                     try {
   1053                         document.replace(0, document.getLength(), "");
   1054                     } catch (BadLocationException e) {
   1055                         // pass
   1056                     }
   1057                 }
   1058 
   1059                 return document;
   1060             }
   1061         };
   1062         change.setTextType(fileName.substring(fileName.lastIndexOf('.') + 1));
   1063         return change;
   1064     }
   1065 
   1066     /**
   1067      * Returns the list of files to open when the template has been created
   1068      *
   1069      * @return the list of files to open
   1070      */
   1071     @NonNull
   1072     public List<String> getFilesToOpen() {
   1073         return mOpen;
   1074     }
   1075 
   1076     /**
   1077      * Returns the list of actions to perform when the template has been created
   1078      *
   1079      * @return the list of actions to perform
   1080      */
   1081     @NonNull
   1082     public List<Runnable> getFinalizingActions() {
   1083         return mFinalizingActions;
   1084     }
   1085 
   1086     /** Copy a template resource */
   1087     private final void copyTemplateResource(
   1088             @NonNull String relativeFrom,
   1089             @NonNull IPath output) throws IOException {
   1090         File from = getFullPath(relativeFrom);
   1091         copy(from, output);
   1092     }
   1093 
   1094     /** Returns true if the given file contains the given bytes */
   1095     private static boolean isIdentical(@Nullable byte[] data, @NonNull IFile dest) {
   1096         assert dest.exists();
   1097         byte[] existing = AdtUtils.readData(dest);
   1098         return Arrays.equals(existing, data);
   1099     }
   1100 
   1101     /**
   1102      * Copies the given source file into the given destination file (where the
   1103      * source is allowed to be a directory, in which case the whole directory is
   1104      * copied recursively)
   1105      */
   1106     private void copy(File src, IPath path) throws IOException {
   1107         if (src.isDirectory()) {
   1108             File[] children = src.listFiles();
   1109             if (children != null) {
   1110                 for (File child : children) {
   1111                     copy(child, path.append(child.getName()));
   1112                 }
   1113             }
   1114         } else {
   1115             IResource dest = mProject.getFile(path);
   1116             if (dest.exists() && !(dest instanceof IFile)) {// Don't attempt to overwrite a folder
   1117                 assert false : dest.getClass().getName();
   1118             return;
   1119             }
   1120             IFile file = (IFile) dest;
   1121             String targetName = path.lastSegment();
   1122             if (dest instanceof IFile) {
   1123                 if (dest.exists() && isIdentical(Files.toByteArray(src), file)) {
   1124                     String label = String.format(
   1125                             "Not overwriting %1$s because the files are identical", targetName);
   1126                     NullChange change = new NullChange(label);
   1127                     change.setEnabled(false);
   1128                     mOtherChanges.add(change);
   1129                     return;
   1130                 }
   1131             }
   1132 
   1133             if (targetName.endsWith(DOT_XML)
   1134                     || targetName.endsWith(DOT_JAVA)
   1135                     || targetName.endsWith(DOT_TXT)
   1136                     || targetName.endsWith(DOT_RS)
   1137                     || targetName.endsWith(DOT_AIDL)
   1138                     || targetName.endsWith(DOT_SVG)) {
   1139 
   1140                 String newFile = Files.toString(src, Charsets.UTF_8);
   1141                 newFile = format(mProject, newFile, path);
   1142 
   1143                 TextFileChange addFile = createNewFileChange(file);
   1144                 addFile.setEdit(new InsertEdit(0, newFile));
   1145                 mTextChanges.add(addFile);
   1146             } else {
   1147                 // Write binary file: Need custom change for that
   1148                 IPath workspacePath = mProject.getFullPath().append(path);
   1149                 mOtherChanges.add(new CreateFileChange(targetName, workspacePath, src));
   1150             }
   1151         }
   1152     }
   1153 
   1154     /**
   1155      * A custom {@link TemplateLoader} which locates and provides templates
   1156      * within the plugin .jar file
   1157      */
   1158     private static final class MyTemplateLoader implements TemplateLoader {
   1159         private String mPrefix;
   1160 
   1161         public void setPrefix(String prefix) {
   1162             mPrefix = prefix;
   1163         }
   1164 
   1165         public void setTemplateFile(File file) {
   1166             setTemplateParent(file.getParentFile());
   1167         }
   1168 
   1169         public void setTemplateParent(File parent) {
   1170             mPrefix = parent.getPath();
   1171         }
   1172 
   1173         @Override
   1174         public Reader getReader(Object templateSource, String encoding) throws IOException {
   1175             URL url = (URL) templateSource;
   1176             return new InputStreamReader(url.openStream(), encoding);
   1177         }
   1178 
   1179         @Override
   1180         public long getLastModified(Object templateSource) {
   1181             return 0;
   1182         }
   1183 
   1184         @Override
   1185         public Object findTemplateSource(String name) throws IOException {
   1186             String path = mPrefix != null ? mPrefix + '/' + name : name;
   1187             File file = new File(path);
   1188             if (file.exists()) {
   1189                 return file.toURI().toURL();
   1190             }
   1191             return null;
   1192         }
   1193 
   1194         @Override
   1195         public void closeTemplateSource(Object templateSource) throws IOException {
   1196         }
   1197     }
   1198 
   1199     /**
   1200      * Validates this template to make sure it's supported
   1201      * @param currentMinSdk the minimum SDK in the project, or -1 or 0 if unknown (e.g. codename)
   1202      * @param buildApi the build API, or -1 or 0 if unknown (e.g. codename)
   1203      *
   1204      * @return a status object with the error, or null if there is no problem
   1205      */
   1206     @SuppressWarnings("cast") // In Eclipse 3.6.2 cast below is needed
   1207     @Nullable
   1208     public IStatus validateTemplate(int currentMinSdk, int buildApi) {
   1209         TemplateMetadata template = getTemplate();
   1210         if (template == null) {
   1211             return null;
   1212         }
   1213         if (!template.isSupported()) {
   1214             String versionString = (String) AdtPlugin.getDefault().getBundle().getHeaders().get(
   1215                     Constants.BUNDLE_VERSION);
   1216             Version version = new Version(versionString);
   1217             return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
   1218                     String.format("This template requires a more recent version of the " +
   1219                             "Android Eclipse plugin. Please update from version %1$d.%2$d.%3$d.",
   1220                             version.getMajor(), version.getMinor(), version.getMicro()));
   1221         }
   1222         int templateMinSdk = template.getMinSdk();
   1223         if (templateMinSdk > currentMinSdk && currentMinSdk >= 1) {
   1224             return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
   1225                     String.format("This template requires a minimum SDK version of at " +
   1226                             "least %1$d, and the current min version is %2$d",
   1227                             templateMinSdk, currentMinSdk));
   1228         }
   1229         int templateMinBuildApi = template.getMinBuildApi();
   1230         if (templateMinBuildApi >  buildApi && buildApi >= 1) {
   1231             return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
   1232                     String.format("This template requires a build target API version of at " +
   1233                             "least %1$d, and the current version is %2$d",
   1234                             templateMinBuildApi, buildApi));
   1235         }
   1236 
   1237         return null;
   1238     }
   1239 }
   1240