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