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