Home | History | Annotate | Download | only in templates
      1 /*
      2  * Copyright (C) 2012 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.ide.eclipse.adt.internal.wizards.templates;
     17 
     18 import static com.android.SdkConstants.ATTR_PACKAGE;
     19 import static com.android.SdkConstants.DOT_AIDL;
     20 import static com.android.SdkConstants.DOT_FTL;
     21 import static com.android.SdkConstants.DOT_JAVA;
     22 import static com.android.SdkConstants.DOT_RS;
     23 import static com.android.SdkConstants.DOT_SVG;
     24 import static com.android.SdkConstants.DOT_TXT;
     25 import static com.android.SdkConstants.DOT_XML;
     26 import static com.android.SdkConstants.EXT_XML;
     27 import static com.android.SdkConstants.FD_NATIVE_LIBS;
     28 import static com.android.SdkConstants.XMLNS_PREFIX;
     29 import static com.android.ide.eclipse.adt.internal.wizards.templates.InstallDependencyPage.SUPPORT_LIBRARY_NAME;
     30 import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateManager.getTemplateRootFolder;
     31 
     32 import com.android.SdkConstants;
     33 import com.android.annotations.NonNull;
     34 import com.android.annotations.Nullable;
     35 import com.android.annotations.VisibleForTesting;
     36 import com.android.ide.common.xml.XmlFormatStyle;
     37 import com.android.ide.eclipse.adt.AdtPlugin;
     38 import com.android.ide.eclipse.adt.AdtUtils;
     39 import com.android.ide.eclipse.adt.internal.actions.AddSupportJarAction;
     40 import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlFormatPreferences;
     41 import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlPrettyPrinter;
     42 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
     43 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
     44 import com.android.ide.eclipse.adt.internal.sdk.AdtManifestMergeCallback;
     45 import com.android.manifmerger.ManifestMerger;
     46 import com.android.manifmerger.MergerLog;
     47 import com.android.resources.ResourceFolderType;
     48 import com.android.utils.SdkUtils;
     49 import com.google.common.base.Charsets;
     50 import com.google.common.collect.Lists;
     51 import com.google.common.io.Files;
     52 
     53 import freemarker.cache.TemplateLoader;
     54 import freemarker.template.Configuration;
     55 import freemarker.template.DefaultObjectWrapper;
     56 import freemarker.template.Template;
     57 import freemarker.template.TemplateException;
     58 
     59 import org.eclipse.core.resources.IFile;
     60 import org.eclipse.core.resources.IProject;
     61 import org.eclipse.core.resources.IResource;
     62 import org.eclipse.core.runtime.CoreException;
     63 import org.eclipse.core.runtime.IPath;
     64 import org.eclipse.core.runtime.IProgressMonitor;
     65 import org.eclipse.core.runtime.IStatus;
     66 import org.eclipse.core.runtime.Path;
     67 import org.eclipse.core.runtime.Status;
     68 import org.eclipse.jdt.core.IJavaProject;
     69 import org.eclipse.jdt.core.JavaCore;
     70 import org.eclipse.jdt.core.ToolFactory;
     71 import org.eclipse.jdt.core.formatter.CodeFormatter;
     72 import org.eclipse.jface.dialogs.MessageDialog;
     73 import org.eclipse.jface.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 */
    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 */
    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 
    265     private TemplateHandler(File rootPath, TemplateManager manager) {
    266         mRootPath = rootPath;
    267         mManager = manager;
    268         mLoader = new MyTemplateLoader();
    269         mLoader.setPrefix(mRootPath.getPath());
    270     }
    271 
    272     public TemplateManager getManager() {
    273         return mManager;
    274     }
    275 
    276     public void setBackupMergedFiles(boolean backupMergedFiles) {
    277         mBackupMergedFiles = backupMergedFiles;
    278     }
    279 
    280     @NonNull
    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 
    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 */
    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
    343     public TemplateMetadata getTemplate() {
    344         if (mTemplate == null) {
    345             mTemplate = mManager.getTemplate(mRootPath);
    346         }
    347 
    348         return mTemplate;
    349     }
    350 
    351     @NonNull
    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
    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
    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
    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 */
    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")
    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 */
    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
    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
    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
    630     private IFile getTargetFile(@NonNull IPath path) {
    631         return mProject.getFile(path);
    632     }
    633 
    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 */
    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 */
    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      */
    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 
    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 */
    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 
    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 
    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
   1001     public List<String> getFilesToOpen() {
   1002         return mOpen;
   1003     }
   1004 
   1005     /** Copy a template resource */
   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 */
   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      */
   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 
   1080         public void setPrefix(String prefix) {
   1081             mPrefix = prefix;
   1082         }
   1083 
   1084         public void setTemplateFile(File file) {
   1085             setTemplateParent(file.getParentFile());
   1086         }
   1087 
   1088         public void setTemplateParent(File parent) {
   1089             mPrefix = parent.getPath();
   1090         }
   1091 
   1092         @Override
   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
   1099         public long getLastModified(Object templateSource) {
   1100             return 0;
   1101         }
   1102 
   1103         @Override
   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
   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
   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