• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 */
createFromPath(File rootPath)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 */
createFromName(String category, String name)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 
TemplateHandler(File rootPath, TemplateManager manager)289     private TemplateHandler(File rootPath, TemplateManager manager) {
290         mRootPath = rootPath;
291         mManager = manager;
292         mLoader = new MyTemplateLoader();
293         mLoader.setPrefix(mRootPath.getPath());
294     }
295 
getManager()296     public TemplateManager getManager() {
297         return mManager;
298     }
299 
setBackupMergedFiles(boolean backupMergedFiles)300     public void setBackupMergedFiles(boolean backupMergedFiles) {
301         mBackupMergedFiles = backupMergedFiles;
302     }
303 
304     @NonNull
render(IProject project, Map<String, Object> args)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 
createParameterMap(Map<String, Object> args)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 */
createBuiltinMap()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 
addDirectoryParameters(Map<String, Object> parameters, IProject project)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
getTemplate()384     public TemplateMetadata getTemplate() {
385         if (mTemplate == null) {
386             mTemplate = mManager.getTemplate(mRootPath);
387         }
388 
389         return mTemplate;
390     }
391 
392     @NonNull
getResourcePath(String templateName)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
readTemplateTextResource(@onNull String relativePath)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
readTemplateTextResource(@onNull File file)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
readTemplateResource(@onNull String relativePath)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 */
processVariables(final Configuration freemarker, String file, final Map<String, Object> paramMap)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")
canOverwrite(File file)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 */
execute( final Configuration freemarker, String file, final Map<String, Object> paramMap)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
getFullPath(@onNull String fromPath)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
getTargetPath(@onNull String relative)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
getTargetFile(@onNull IPath path)701     private IFile getTargetFile(@NonNull IPath path) {
702         return mProject.getFile(path);
703     }
704 
merge( @onNull final Configuration freemarker, @NonNull final Map<String, Object> paramMap, @NonNull String relativeFrom, @NonNull IPath toPath)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 */
mergeResourceFile(Document currentDocument, Document fragment, ResourceFolderType folderType, Map<String, Object> 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 */
mergeManifest(Document currentManifest, Document fragment)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      */
makeBackup(File file)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 
getResourceId(Element element)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 */
instantiate( @onNull final Configuration freemarker, @NonNull final Map<String, Object> paramMap, @NonNull String relativeFrom, @NonNull IPath to)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 
format(IProject project, String contents, IPath to)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 
createNewFileChange(IFile targetFile)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
getFilesToOpen()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
getFinalizingActions()1082     public List<Runnable> getFinalizingActions() {
1083         return mFinalizingActions;
1084     }
1085 
1086     /** Copy a template resource */
copyTemplateResource( @onNull String relativeFrom, @NonNull IPath output)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 */
isIdentical(@ullable byte[] data, @NonNull IFile dest)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      */
copy(File src, IPath path)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 
setPrefix(String prefix)1161         public void setPrefix(String prefix) {
1162             mPrefix = prefix;
1163         }
1164 
setTemplateFile(File file)1165         public void setTemplateFile(File file) {
1166             setTemplateParent(file.getParentFile());
1167         }
1168 
setTemplateParent(File parent)1169         public void setTemplateParent(File parent) {
1170             mPrefix = parent.getPath();
1171         }
1172 
1173         @Override
getReader(Object templateSource, String encoding)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
getLastModified(Object templateSource)1180         public long getLastModified(Object templateSource) {
1181             return 0;
1182         }
1183 
1184         @Override
findTemplateSource(String name)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
closeTemplateSource(Object templateSource)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
validateTemplate(int currentMinSdk, int buildApi)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