diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateHandler.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateHandler.java | 1239 |
1 files changed, 1239 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateHandler.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateHandler.java new file mode 100644 index 000000000..8e11841b4 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateHandler.java @@ -0,0 +1,1239 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.templates; + +import static com.android.SdkConstants.ATTR_PACKAGE; +import static com.android.SdkConstants.DOT_AIDL; +import static com.android.SdkConstants.DOT_FTL; +import static com.android.SdkConstants.DOT_JAVA; +import static com.android.SdkConstants.DOT_RS; +import static com.android.SdkConstants.DOT_SVG; +import static com.android.SdkConstants.DOT_TXT; +import static com.android.SdkConstants.DOT_XML; +import static com.android.SdkConstants.EXT_XML; +import static com.android.SdkConstants.FD_NATIVE_LIBS; +import static com.android.SdkConstants.XMLNS_PREFIX; +import static com.android.ide.eclipse.adt.internal.wizards.templates.InstallDependencyPage.SUPPORT_LIBRARY_NAME; +import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateManager.getTemplateRootFolder; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.android.ide.common.xml.XmlFormatStyle; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.actions.AddSupportJarAction; +import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlFormatPreferences; +import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlPrettyPrinter; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.ide.eclipse.adt.internal.sdk.AdtManifestMergeCallback; +import com.android.manifmerger.ManifestMerger; +import com.android.manifmerger.MergerLog; +import com.android.resources.ResourceFolderType; +import com.android.utils.SdkUtils; +import com.google.common.base.Charsets; +import com.google.common.collect.Lists; +import com.google.common.io.Files; + +import freemarker.cache.TemplateLoader; +import freemarker.template.Configuration; +import freemarker.template.DefaultObjectWrapper; +import freemarker.template.Template; +import freemarker.template.TemplateException; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Status; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.ToolFactory; +import org.eclipse.jdt.core.formatter.CodeFormatter; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.operation.IRunnableWithProgress; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.ltk.core.refactoring.Change; +import org.eclipse.ltk.core.refactoring.NullChange; +import org.eclipse.ltk.core.refactoring.TextFileChange; +import org.eclipse.swt.SWT; +import org.eclipse.text.edits.InsertEdit; +import org.eclipse.text.edits.MultiTextEdit; +import org.eclipse.text.edits.ReplaceEdit; +import org.eclipse.text.edits.TextEdit; +import org.osgi.framework.Constants; +import org.osgi.framework.Version; +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.reflect.InvocationTargetException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +/** + * Handler which manages instantiating FreeMarker templates, copying resources + * and merging into existing files + */ +class TemplateHandler { + /** Highest supported format; templates with a higher number will be skipped + * <p> + * <ul> + * <li> 1: Initial format, supported by ADT 20 and up. + * <li> 2: ADT 21 and up. Boolean variables that have a default value and are not + * edited by the user would end up as strings in ADT 20; now they are always + * proper Booleans. Templates which rely on this should specify format >= 2. + * <li> 3: The wizard infrastructure passes the {@code isNewProject} boolean variable + * to indicate whether a wizard is created as part of a new blank project + * <li> 4: The templates now specify dependencies in the recipe file. + * </ul> + */ + static final int CURRENT_FORMAT = 4; + + /** + * Special marker indicating that this path refers to the special shared + * resource directory rather than being somewhere inside the root/ directory + * where all template specific resources are found + */ + private static final String VALUE_TEMPLATE_DIR = "$TEMPLATEDIR"; //$NON-NLS-1$ + + /** + * Directory within the template which contains the resources referenced + * from the template.xml file + */ + private static final String DATA_ROOT = "root"; //$NON-NLS-1$ + + /** + * Shared resource directory containing common resources shared among + * multiple templates + */ + private static final String RESOURCE_ROOT = "resources"; //$NON-NLS-1$ + + /** Reserved filename which describes each template */ + static final String TEMPLATE_XML = "template.xml"; //$NON-NLS-1$ + + // Various tags and attributes used in the template metadata files - template.xml, + // globals.xml.ftl, recipe.xml.ftl, etc. + + static final String TAG_MERGE = "merge"; //$NON-NLS-1$ + static final String TAG_EXECUTE = "execute"; //$NON-NLS-1$ + static final String TAG_GLOBALS = "globals"; //$NON-NLS-1$ + static final String TAG_GLOBAL = "global"; //$NON-NLS-1$ + static final String TAG_PARAMETER = "parameter"; //$NON-NLS-1$ + static final String TAG_COPY = "copy"; //$NON-NLS-1$ + static final String TAG_INSTANTIATE = "instantiate"; //$NON-NLS-1$ + static final String TAG_OPEN = "open"; //$NON-NLS-1$ + static final String TAG_THUMB = "thumb"; //$NON-NLS-1$ + static final String TAG_THUMBS = "thumbs"; //$NON-NLS-1$ + static final String TAG_DEPENDENCY = "dependency"; //$NON-NLS-1$ + static final String TAG_ICONS = "icons"; //$NON-NLS-1$ + static final String TAG_FORMFACTOR = "formfactor"; //$NON-NLS-1$ + static final String TAG_CATEGORY = "category"; //$NON-NLS-1$ + static final String ATTR_FORMAT = "format"; //$NON-NLS-1$ + static final String ATTR_REVISION = "revision"; //$NON-NLS-1$ + static final String ATTR_VALUE = "value"; //$NON-NLS-1$ + static final String ATTR_DEFAULT = "default"; //$NON-NLS-1$ + static final String ATTR_SUGGEST = "suggest"; //$NON-NLS-1$ + static final String ATTR_ID = "id"; //$NON-NLS-1$ + static final String ATTR_NAME = "name"; //$NON-NLS-1$ + static final String ATTR_DESCRIPTION = "description";//$NON-NLS-1$ + static final String ATTR_TYPE = "type"; //$NON-NLS-1$ + static final String ATTR_HELP = "help"; //$NON-NLS-1$ + static final String ATTR_FILE = "file"; //$NON-NLS-1$ + static final String ATTR_TO = "to"; //$NON-NLS-1$ + static final String ATTR_FROM = "from"; //$NON-NLS-1$ + static final String ATTR_CONSTRAINTS = "constraints";//$NON-NLS-1$ + static final String ATTR_BACKGROUND = "background"; //$NON-NLS-1$ + static final String ATTR_FOREGROUND = "foreground"; //$NON-NLS-1$ + static final String ATTR_SHAPE = "shape"; //$NON-NLS-1$ + static final String ATTR_TRIM = "trim"; //$NON-NLS-1$ + static final String ATTR_PADDING = "padding"; //$NON-NLS-1$ + static final String ATTR_SOURCE_TYPE = "source"; //$NON-NLS-1$ + static final String ATTR_CLIPART_NAME = "clipartName";//$NON-NLS-1$ + static final String ATTR_TEXT = "text"; //$NON-NLS-1$ + static final String ATTR_SRC_DIR = "srcDir"; //$NON-NLS-1$ + static final String ATTR_SRC_OUT = "srcOut"; //$NON-NLS-1$ + static final String ATTR_RES_DIR = "resDir"; //$NON-NLS-1$ + static final String ATTR_RES_OUT = "resOut"; //$NON-NLS-1$ + static final String ATTR_MANIFEST_DIR = "manifestDir";//$NON-NLS-1$ + static final String ATTR_MANIFEST_OUT = "manifestOut";//$NON-NLS-1$ + static final String ATTR_PROJECT_DIR = "projectDir"; //$NON-NLS-1$ + static final String ATTR_PROJECT_OUT = "projectOut"; //$NON-NLS-1$ + static final String ATTR_MAVEN_URL = "mavenUrl"; //$NON-NLS-1$ + static final String ATTR_DEBUG_KEYSTORE_SHA1 = + "debugKeystoreSha1"; //$NON-NLS-1$ + + static final String CATEGORY_ACTIVITIES = "activities";//$NON-NLS-1$ + static final String CATEGORY_PROJECTS = "projects"; //$NON-NLS-1$ + static final String CATEGORY_OTHER = "other"; //$NON-NLS-1$ + + static final String MAVEN_SUPPORT_V4 = "support-v4"; //$NON-NLS-1$ + static final String MAVEN_SUPPORT_V13 = "support-v13"; //$NON-NLS-1$ + static final String MAVEN_APPCOMPAT = "appcompat-v7"; //$NON-NLS-1$ + + /** Default padding to apply in wizards around the thumbnail preview images */ + static final int PREVIEW_PADDING = 10; + + /** Default width to scale thumbnail preview images in wizards to */ + static final int PREVIEW_WIDTH = 200; + + /** + * List of files to open after the wizard has been created (these are + * identified by {@link #TAG_OPEN} elements in the recipe file + */ + private final List<String> mOpen = Lists.newArrayList(); + + /** + * List of actions to perform after the wizard has finished. + */ + protected List<Runnable> mFinalizingActions = Lists.newArrayList(); + + /** Path to the directory containing the templates */ + @NonNull + private final File mRootPath; + + /** The changes being processed by the template handler */ + private List<Change> mMergeChanges; + private List<Change> mTextChanges; + private List<Change> mOtherChanges; + + /** The project to write the template into */ + private IProject mProject; + + /** The template loader which is responsible for finding (and sharing) template files */ + private final MyTemplateLoader mLoader; + + /** Agree to all file-overwrites from now on? */ + private boolean mYesToAll = false; + + /** Is writing the template cancelled? */ + private boolean mNoToAll = false; + + /** + * Should files that we merge contents into be backed up? If yes, will + * create emacs-style tilde-file backups (filename.xml~) + */ + private boolean mBackupMergedFiles = true; + + /** + * Template metadata + */ + private TemplateMetadata mTemplate; + + private final TemplateManager mManager; + + /** Creates a new {@link TemplateHandler} for the given root path */ + static TemplateHandler createFromPath(File rootPath) { + return new TemplateHandler(rootPath, new TemplateManager()); + } + + /** Creates a new {@link TemplateHandler} for the template name, which should + * be relative to the templates directory */ + static TemplateHandler createFromName(String category, String name) { + TemplateManager manager = new TemplateManager(); + + // Use the TemplateManager iteration which should merge contents between the + // extras/templates/ and tools/templates folders and pick the most recent version + List<File> templates = manager.getTemplates(category); + for (File file : templates) { + if (file.getName().equals(name) && category.equals(file.getParentFile().getName())) { + return new TemplateHandler(file, manager); + } + } + + return new TemplateHandler(new File(getTemplateRootFolder(), + category + File.separator + name), manager); + } + + private TemplateHandler(File rootPath, TemplateManager manager) { + mRootPath = rootPath; + mManager = manager; + mLoader = new MyTemplateLoader(); + mLoader.setPrefix(mRootPath.getPath()); + } + + public TemplateManager getManager() { + return mManager; + } + + public void setBackupMergedFiles(boolean backupMergedFiles) { + mBackupMergedFiles = backupMergedFiles; + } + + @NonNull + public List<Change> render(IProject project, Map<String, Object> args) { + mOpen.clear(); + + mProject = project; + mMergeChanges = new ArrayList<Change>(); + mTextChanges = new ArrayList<Change>(); + mOtherChanges = new ArrayList<Change>(); + + // Render the instruction list template. + Map<String, Object> paramMap = createParameterMap(args); + Configuration freemarker = new Configuration(); + freemarker.setObjectWrapper(new DefaultObjectWrapper()); + freemarker.setTemplateLoader(mLoader); + + processVariables(freemarker, TEMPLATE_XML, paramMap); + + // Add the changes in the order where merges are shown first, then text files, + // and finally other files (like jars and icons which don't have previews). + List<Change> changes = new ArrayList<Change>(); + changes.addAll(mMergeChanges); + changes.addAll(mTextChanges); + changes.addAll(mOtherChanges); + return changes; + } + + Map<String, Object> createParameterMap(Map<String, Object> args) { + final Map<String, Object> paramMap = createBuiltinMap(); + + // Wizard parameters supplied by user, specific to this template + paramMap.putAll(args); + + return paramMap; + } + + /** Data model for the templates */ + static Map<String, Object> createBuiltinMap() { + // Create the data model. + final Map<String, Object> paramMap = new HashMap<String, Object>(); + + // Builtin conversion methods + paramMap.put("slashedPackageName", new FmSlashedPackageNameMethod()); //$NON-NLS-1$ + paramMap.put("camelCaseToUnderscore", new FmCamelCaseToUnderscoreMethod()); //$NON-NLS-1$ + paramMap.put("underscoreToCamelCase", new FmUnderscoreToCamelCaseMethod()); //$NON-NLS-1$ + paramMap.put("activityToLayout", new FmActivityToLayoutMethod()); //$NON-NLS-1$ + paramMap.put("layoutToActivity", new FmLayoutToActivityMethod()); //$NON-NLS-1$ + paramMap.put("classToResource", new FmClassNameToResourceMethod()); //$NON-NLS-1$ + paramMap.put("escapeXmlAttribute", new FmEscapeXmlStringMethod()); //$NON-NLS-1$ + paramMap.put("escapeXmlText", new FmEscapeXmlStringMethod()); //$NON-NLS-1$ + paramMap.put("escapeXmlString", new FmEscapeXmlStringMethod()); //$NON-NLS-1$ + paramMap.put("extractLetters", new FmExtractLettersMethod()); //$NON-NLS-1$ + + // This should be handled better: perhaps declared "required packages" as part of the + // inputs? (It would be better if we could conditionally disable template based + // on availability) + Map<String, String> builtin = new HashMap<String, String>(); + builtin.put("templatesRes", VALUE_TEMPLATE_DIR); //$NON-NLS-1$ + paramMap.put("android", builtin); //$NON-NLS-1$ + + return paramMap; + } + + static void addDirectoryParameters(Map<String, Object> parameters, IProject project) { + IPath srcDir = project.getFile(SdkConstants.SRC_FOLDER).getProjectRelativePath(); + parameters.put(ATTR_SRC_DIR, srcDir.toString()); + + IPath resDir = project.getFile(SdkConstants.RES_FOLDER).getProjectRelativePath(); + parameters.put(ATTR_RES_DIR, resDir.toString()); + + IPath manifestDir = project.getProjectRelativePath(); + parameters.put(ATTR_MANIFEST_DIR, manifestDir.toString()); + parameters.put(ATTR_MANIFEST_OUT, manifestDir.toString()); + + parameters.put(ATTR_PROJECT_DIR, manifestDir.toString()); + parameters.put(ATTR_PROJECT_OUT, manifestDir.toString()); + + parameters.put(ATTR_DEBUG_KEYSTORE_SHA1, ""); + } + + @Nullable + public TemplateMetadata getTemplate() { + if (mTemplate == null) { + mTemplate = mManager.getTemplate(mRootPath); + } + + return mTemplate; + } + + @NonNull + public String getResourcePath(String templateName) { + return new File(mRootPath.getPath(), templateName).getPath(); + } + + /** + * Load a text resource for the given relative path within the template + * + * @param relativePath relative path within the template + * @return the string contents of the template text file + */ + @Nullable + public String readTemplateTextResource(@NonNull String relativePath) { + try { + return Files.toString(new File(mRootPath, + relativePath.replace('/', File.separatorChar)), Charsets.UTF_8); + } catch (IOException e) { + AdtPlugin.log(e, null); + return null; + } + } + + @Nullable + public String readTemplateTextResource(@NonNull File file) { + assert file.isAbsolute(); + try { + return Files.toString(file, Charsets.UTF_8); + } catch (IOException e) { + AdtPlugin.log(e, null); + return null; + } + } + + /** + * Reads the contents of a resource + * + * @param relativePath the path relative to the template directory + * @return the binary data read from the file + */ + @Nullable + public byte[] readTemplateResource(@NonNull String relativePath) { + try { + return Files.toByteArray(new File(mRootPath, relativePath)); + } catch (IOException e) { + AdtPlugin.log(e, null); + return null; + } + } + + /** + * Most recent thrown exception during template instantiation. This should + * basically always be null. Used by unit tests to see if any template + * instantiation recorded a failure. + */ + @VisibleForTesting + public static Exception sMostRecentException; + + /** Read the given FreeMarker file and process the variable definitions */ + private void processVariables(final Configuration freemarker, + String file, final Map<String, Object> paramMap) { + try { + String xml; + if (file.endsWith(DOT_XML)) { + // Just read the file + xml = readTemplateTextResource(file); + if (xml == null) { + return; + } + } else { + mLoader.setTemplateFile(new File(mRootPath, file)); + Template inputsTemplate = freemarker.getTemplate(file); + StringWriter out = new StringWriter(); + inputsTemplate.process(paramMap, out); + out.flush(); + xml = out.toString(); + } + + SAXParserFactory factory = SAXParserFactory.newInstance(); + SAXParser saxParser = factory.newSAXParser(); + saxParser.parse(new ByteArrayInputStream(xml.getBytes()), new DefaultHandler() { + @Override + public void startElement(String uri, String localName, String name, + Attributes attributes) + throws SAXException { + if (TAG_PARAMETER.equals(name)) { + String id = attributes.getValue(ATTR_ID); + if (!paramMap.containsKey(id)) { + String value = attributes.getValue(ATTR_DEFAULT); + Object mapValue = value; + if (value != null && !value.isEmpty()) { + String type = attributes.getValue(ATTR_TYPE); + if ("boolean".equals(type)) { //$NON-NLS-1$ + mapValue = Boolean.valueOf(value); + } + } + paramMap.put(id, mapValue); + } + } else if (TAG_GLOBAL.equals(name)) { + String id = attributes.getValue(ATTR_ID); + if (!paramMap.containsKey(id)) { + paramMap.put(id, TypedVariable.parseGlobal(attributes)); + } + } else if (TAG_GLOBALS.equals(name)) { + // Handle evaluation of variables + String path = attributes.getValue(ATTR_FILE); + if (path != null) { + processVariables(freemarker, path, paramMap); + } // else: <globals> root element + } else if (TAG_EXECUTE.equals(name)) { + String path = attributes.getValue(ATTR_FILE); + if (path != null) { + execute(freemarker, path, paramMap); + } + } else if (TAG_DEPENDENCY.equals(name)) { + String dependencyName = attributes.getValue(ATTR_NAME); + if (dependencyName.equals(SUPPORT_LIBRARY_NAME)) { + // We assume the revision requirement has been satisfied + // by the wizard + File path = AddSupportJarAction.getSupportJarFile(); + if (path != null) { + IPath to = getTargetPath(FD_NATIVE_LIBS +'/' + path.getName()); + try { + copy(path, to); + } catch (IOException ioe) { + AdtPlugin.log(ioe, null); + } + } + } + } else if (!name.equals("template") && !name.equals(TAG_CATEGORY) && + !name.equals(TAG_FORMFACTOR) && !name.equals("option") && + !name.equals(TAG_THUMBS) && !name.equals(TAG_THUMB) && + !name.equals(TAG_ICONS)) { + System.err.println("WARNING: Unknown template directive " + name); + } + } + }); + } catch (Exception e) { + sMostRecentException = e; + AdtPlugin.log(e, null); + } + } + + @SuppressWarnings("unused") + private boolean canOverwrite(File file) { + if (file.exists()) { + // Warn that the file already exists and ask the user what to do + if (!mYesToAll) { + MessageDialog dialog = new MessageDialog(null, "File Already Exists", null, + String.format( + "%1$s already exists.\nWould you like to replace it?", + file.getPath()), + MessageDialog.QUESTION, new String[] { + // Yes will be moved to the end because it's the default + "Yes", "No", "Cancel", "Yes to All" + }, 0); + int result = dialog.open(); + switch (result) { + case 0: + // Yes + break; + case 3: + // Yes to all + mYesToAll = true; + break; + case 1: + // No + return false; + case SWT.DEFAULT: + case 2: + // Cancel + mNoToAll = true; + return false; + } + } + + if (mBackupMergedFiles) { + return makeBackup(file); + } else { + return file.delete(); + } + } + + return true; + } + + /** Executes the given recipe file: copying, merging, instantiating, opening files etc */ + private void execute( + final Configuration freemarker, + String file, + final Map<String, Object> paramMap) { + try { + mLoader.setTemplateFile(new File(mRootPath, file)); + Template freemarkerTemplate = freemarker.getTemplate(file); + + StringWriter out = new StringWriter(); + freemarkerTemplate.process(paramMap, out); + out.flush(); + String xml = out.toString(); + + // Parse and execute the resulting instruction list. + SAXParserFactory factory = SAXParserFactory.newInstance(); + SAXParser saxParser = factory.newSAXParser(); + + saxParser.parse(new ByteArrayInputStream(xml.getBytes()), + new DefaultHandler() { + @Override + public void startElement(String uri, String localName, String name, + Attributes attributes) + throws SAXException { + if (mNoToAll) { + return; + } + + try { + boolean instantiate = TAG_INSTANTIATE.equals(name); + if (TAG_COPY.equals(name) || instantiate) { + String fromPath = attributes.getValue(ATTR_FROM); + String toPath = attributes.getValue(ATTR_TO); + if (toPath == null || toPath.isEmpty()) { + toPath = attributes.getValue(ATTR_FROM); + toPath = AdtUtils.stripSuffix(toPath, DOT_FTL); + } + IPath to = getTargetPath(toPath); + if (instantiate) { + instantiate(freemarker, paramMap, fromPath, to); + } else { + copyTemplateResource(fromPath, to); + } + } else if (TAG_MERGE.equals(name)) { + String fromPath = attributes.getValue(ATTR_FROM); + String toPath = attributes.getValue(ATTR_TO); + if (toPath == null || toPath.isEmpty()) { + toPath = attributes.getValue(ATTR_FROM); + toPath = AdtUtils.stripSuffix(toPath, DOT_FTL); + } + // Resources in template.xml are located within root/ + IPath to = getTargetPath(toPath); + merge(freemarker, paramMap, fromPath, to); + } else if (name.equals(TAG_OPEN)) { + // The relative path here is within the output directory: + String relativePath = attributes.getValue(ATTR_FILE); + if (relativePath != null && !relativePath.isEmpty()) { + mOpen.add(relativePath); + } + } else if (TAG_DEPENDENCY.equals(name)) { + String dependencyUrl = attributes.getValue(ATTR_MAVEN_URL); + File path; + if (dependencyUrl.contains(MAVEN_SUPPORT_V4)) { + // We assume the revision requirement has been satisfied + // by the wizard + path = AddSupportJarAction.getSupportJarFile(); + } else if (dependencyUrl.contains(MAVEN_SUPPORT_V13)) { + path = AddSupportJarAction.getSupport13JarFile(); + } else if (dependencyUrl.contains(MAVEN_APPCOMPAT)) { + path = null; + mFinalizingActions.add(new Runnable() { + @Override + public void run() { + AddSupportJarAction.installAppCompatLibrary(mProject, true); + } + }); + } else { + path = null; + System.err.println("WARNING: Unknown dependency type"); + } + + if (path != null) { + IPath to = getTargetPath(FD_NATIVE_LIBS +'/' + path.getName()); + try { + copy(path, to); + } catch (IOException ioe) { + AdtPlugin.log(ioe, null); + } + } + } else if (!name.equals("recipe") && !name.equals(TAG_DEPENDENCY)) { //$NON-NLS-1$ + System.err.println("WARNING: Unknown template directive " + name); + } + } catch (Exception e) { + sMostRecentException = e; + AdtPlugin.log(e, null); + } + } + }); + + } catch (Exception e) { + sMostRecentException = e; + AdtPlugin.log(e, null); + } + } + + @NonNull + private File getFullPath(@NonNull String fromPath) { + if (fromPath.startsWith(VALUE_TEMPLATE_DIR)) { + return new File(getTemplateRootFolder(), RESOURCE_ROOT + File.separator + + fromPath.substring(VALUE_TEMPLATE_DIR.length() + 1).replace('/', + File.separatorChar)); + } + return new File(mRootPath, DATA_ROOT + File.separator + fromPath); + } + + @NonNull + private IPath getTargetPath(@NonNull String relative) { + if (relative.indexOf('\\') != -1) { + relative = relative.replace('\\', '/'); + } + return new Path(relative); + } + + @NonNull + private IFile getTargetFile(@NonNull IPath path) { + return mProject.getFile(path); + } + + private void merge( + @NonNull final Configuration freemarker, + @NonNull final Map<String, Object> paramMap, + @NonNull String relativeFrom, + @NonNull IPath toPath) throws IOException, TemplateException { + + String currentXml = null; + + IFile to = getTargetFile(toPath); + if (to.exists()) { + currentXml = AdtPlugin.readFile(to); + } + + if (currentXml == null) { + // The target file doesn't exist: don't merge, just copy + boolean instantiate = relativeFrom.endsWith(DOT_FTL); + if (instantiate) { + instantiate(freemarker, paramMap, relativeFrom, toPath); + } else { + copyTemplateResource(relativeFrom, toPath); + } + return; + } + + if (!to.getFileExtension().equals(EXT_XML)) { + throw new RuntimeException("Only XML files can be merged at this point: " + to); + } + + String xml = null; + File from = getFullPath(relativeFrom); + if (relativeFrom.endsWith(DOT_FTL)) { + // Perform template substitution of the template prior to merging + mLoader.setTemplateFile(from); + Template template = freemarker.getTemplate(from.getName()); + Writer out = new StringWriter(); + template.process(paramMap, out); + out.flush(); + xml = out.toString(); + } else { + xml = readTemplateTextResource(from); + if (xml == null) { + return; + } + } + + Document currentDocument = DomUtilities.parseStructuredDocument(currentXml); + assert currentDocument != null : currentXml; + Document fragment = DomUtilities.parseStructuredDocument(xml); + assert fragment != null : xml; + + XmlFormatStyle formatStyle = XmlFormatStyle.MANIFEST; + boolean modified; + boolean ok; + String fileName = to.getName(); + if (fileName.equals(SdkConstants.FN_ANDROID_MANIFEST_XML)) { + modified = ok = mergeManifest(currentDocument, fragment); + } else { + // Merge plain XML files + String parentFolderName = to.getParent().getName(); + ResourceFolderType folderType = ResourceFolderType.getFolderType(parentFolderName); + if (folderType != null) { + formatStyle = EclipseXmlPrettyPrinter.getForFile(toPath); + } else { + formatStyle = XmlFormatStyle.FILE; + } + + modified = mergeResourceFile(currentDocument, fragment, folderType, paramMap); + ok = true; + } + + // Finally write out the merged file (formatting etc) + String contents = null; + if (ok) { + if (modified) { + contents = EclipseXmlPrettyPrinter.prettyPrint(currentDocument, + EclipseXmlFormatPreferences.create(), formatStyle, null, + currentXml.endsWith("\n")); //$NON-NLS-1$ + } + } else { + // Just insert into file along with comment, using the "standard" conflict + // syntax that many tools and editors recognize. + String sep = SdkUtils.getLineSeparator(); + contents = + "<<<<<<< Original" + sep + + currentXml + sep + + "=======" + sep + + xml + + ">>>>>>> Added" + sep; + } + + if (contents != null) { + TextFileChange change = new TextFileChange("Merge " + fileName, to); + MultiTextEdit rootEdit = new MultiTextEdit(); + rootEdit.addChild(new ReplaceEdit(0, currentXml.length(), contents)); + change.setEdit(rootEdit); + change.setTextType(SdkConstants.EXT_XML); + mMergeChanges.add(change); + } + } + + /** Merges the given resource file contents into the given resource file + * @param paramMap */ + private static boolean mergeResourceFile(Document currentDocument, Document fragment, + ResourceFolderType folderType, Map<String, Object> paramMap) { + boolean modified = false; + + // Copy namespace declarations + NamedNodeMap attributes = fragment.getDocumentElement().getAttributes(); + if (attributes != null) { + for (int i = 0, n = attributes.getLength(); i < n; i++) { + Attr attribute = (Attr) attributes.item(i); + if (attribute.getName().startsWith(XMLNS_PREFIX)) { + currentDocument.getDocumentElement().setAttribute(attribute.getName(), + attribute.getValue()); + } + } + } + + // For layouts for example, I want to *append* inside the root all the + // contents of the new file. + // But for resources for example, I want to combine elements which specify + // the same name or id attribute. + // For elements like manifest files we need to insert stuff at the right + // location in a nested way (activities in the application element etc) + // but that doesn't happen for the other file types. + Element root = fragment.getDocumentElement(); + NodeList children = root.getChildNodes(); + List<Node> nodes = new ArrayList<Node>(children.getLength()); + for (int i = children.getLength() - 1; i >= 0; i--) { + Node child = children.item(i); + nodes.add(child); + root.removeChild(child); + } + Collections.reverse(nodes); + + root = currentDocument.getDocumentElement(); + + if (folderType == ResourceFolderType.VALUES) { + // Try to merge items of the same name + Map<String, Node> old = new HashMap<String, Node>(); + NodeList newSiblings = root.getChildNodes(); + for (int i = newSiblings.getLength() - 1; i >= 0; i--) { + Node child = newSiblings.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE) { + Element element = (Element) child; + String name = getResourceId(element); + if (name != null) { + old.put(name, element); + } + } + } + + for (Node node : nodes) { + if (node.getNodeType() == Node.ELEMENT_NODE) { + Element element = (Element) node; + String name = getResourceId(element); + Node replace = name != null ? old.get(name) : null; + if (replace != null) { + // There is an existing item with the same id: just replace it + // ACTUALLY -- let's NOT change it. + // Let's say you've used the activity wizard once, and it + // emits some configuration parameter as a resource that + // it depends on, say "padding". Then the user goes and + // tweaks the padding to some other number. + // Now running the wizard a *second* time for some new activity, + // we should NOT go and set the value back to the template's + // default! + //root.replaceChild(node, replace); + + // ... ON THE OTHER HAND... What if it's a parameter class + // (where the template rewrites a common attribute). Here it's + // really confusing if the new parameter is not set. This is + // really an error in the template, since we shouldn't have conflicts + // like that, but we need to do something to help track this down. + AdtPlugin.log(null, + "Warning: Ignoring name conflict in resource file for name %1$s", + name); + } else { + root.appendChild(node); + modified = true; + } + } + } + } else { + // In other file types, such as layouts, just append all the new content + // at the end. + for (Node node : nodes) { + root.appendChild(node); + modified = true; + } + } + return modified; + } + + /** Merges the given manifest fragment into the given manifest file */ + private static boolean mergeManifest(Document currentManifest, Document fragment) { + // TODO change MergerLog.wrapSdkLog by a custom IMergerLog that will create + // and maintain error markers. + + // Transfer package element from manifest to merged in root; required by + // manifest merger + Element fragmentRoot = fragment.getDocumentElement(); + Element manifestRoot = currentManifest.getDocumentElement(); + if (fragmentRoot == null || manifestRoot == null) { + return false; + } + String pkg = fragmentRoot.getAttribute(ATTR_PACKAGE); + if (pkg == null || pkg.isEmpty()) { + pkg = manifestRoot.getAttribute(ATTR_PACKAGE); + if (pkg != null && !pkg.isEmpty()) { + fragmentRoot.setAttribute(ATTR_PACKAGE, pkg); + } + } + + ManifestMerger merger = new ManifestMerger( + MergerLog.wrapSdkLog(AdtPlugin.getDefault()), + new AdtManifestMergeCallback()).setExtractPackagePrefix(true); + return currentManifest != null && + fragment != null && + merger.process(currentManifest, fragment); + } + + /** + * Makes a backup of the given file, if it exists, by renaming it to name~ + * (and removing an old name~ file if it exists) + */ + private static boolean makeBackup(File file) { + if (!file.exists()) { + return true; + } + if (file.isDirectory()) { + return false; + } + + File backupFile = new File(file.getParentFile(), file.getName() + '~'); + if (backupFile.exists()) { + backupFile.delete(); + } + return file.renameTo(backupFile); + } + + private static String getResourceId(Element element) { + String name = element.getAttribute(ATTR_NAME); + if (name == null) { + name = element.getAttribute(ATTR_ID); + } + + return name; + } + + /** Instantiates the given template file into the given output file */ + private void instantiate( + @NonNull final Configuration freemarker, + @NonNull final Map<String, Object> paramMap, + @NonNull String relativeFrom, + @NonNull IPath to) throws IOException, TemplateException { + // For now, treat extension-less files as directories... this isn't quite right + // so I should refine this! Maybe with a unique attribute in the template file? + boolean isDirectory = relativeFrom.indexOf('.') == -1; + if (isDirectory) { + // It's a directory + copyTemplateResource(relativeFrom, to); + } else { + File from = getFullPath(relativeFrom); + mLoader.setTemplateFile(from); + Template template = freemarker.getTemplate(from.getName()); + Writer out = new StringWriter(1024); + template.process(paramMap, out); + out.flush(); + String contents = out.toString(); + + contents = format(mProject, contents, to); + IFile targetFile = getTargetFile(to); + TextFileChange change = createNewFileChange(targetFile); + MultiTextEdit rootEdit = new MultiTextEdit(); + rootEdit.addChild(new InsertEdit(0, contents)); + change.setEdit(rootEdit); + mTextChanges.add(change); + } + } + + private static String format(IProject project, String contents, IPath to) { + String name = to.lastSegment(); + if (name.endsWith(DOT_XML)) { + XmlFormatStyle formatStyle = EclipseXmlPrettyPrinter.getForFile(to); + EclipseXmlFormatPreferences prefs = EclipseXmlFormatPreferences.create(); + return EclipseXmlPrettyPrinter.prettyPrint(contents, prefs, formatStyle, null); + } else if (name.endsWith(DOT_JAVA)) { + Map<?, ?> options = null; + if (project != null && project.isAccessible()) { + try { + IJavaProject javaProject = BaseProjectHelper.getJavaProject(project); + if (javaProject != null) { + options = javaProject.getOptions(true); + } + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + } + if (options == null) { + options = JavaCore.getOptions(); + } + + CodeFormatter formatter = ToolFactory.createCodeFormatter(options); + + try { + IDocument doc = new org.eclipse.jface.text.Document(); + // format the file (the meat and potatoes) + doc.set(contents); + TextEdit edit = formatter.format( + CodeFormatter.K_COMPILATION_UNIT | CodeFormatter.F_INCLUDE_COMMENTS, + contents, 0, contents.length(), 0, null); + if (edit != null) { + edit.apply(doc); + } + + return doc.get(); + } catch (Exception e) { + AdtPlugin.log(e, null); + } + } + + return contents; + } + + private static TextFileChange createNewFileChange(IFile targetFile) { + String fileName = targetFile.getName(); + String message; + if (targetFile.exists()) { + message = String.format("Replace %1$s", fileName); + } else { + message = String.format("Create %1$s", fileName); + } + + TextFileChange change = new TextFileChange(message, targetFile) { + @Override + protected IDocument acquireDocument(IProgressMonitor pm) throws CoreException { + IDocument document = super.acquireDocument(pm); + + // In our case, we know we *always* use this TextFileChange + // to *create* files, we're not appending to existing files. + // However, due to the following bug we can end up with cached + // contents of previously deleted files that happened to have the + // same file name: + // https://bugs.eclipse.org/bugs/show_bug.cgi?id=390402 + // Therefore, as a workaround, wipe out the cached contents here + if (document.getLength() > 0) { + try { + document.replace(0, document.getLength(), ""); + } catch (BadLocationException e) { + // pass + } + } + + return document; + } + }; + change.setTextType(fileName.substring(fileName.lastIndexOf('.') + 1)); + return change; + } + + /** + * Returns the list of files to open when the template has been created + * + * @return the list of files to open + */ + @NonNull + public List<String> getFilesToOpen() { + return mOpen; + } + + /** + * Returns the list of actions to perform when the template has been created + * + * @return the list of actions to perform + */ + @NonNull + public List<Runnable> getFinalizingActions() { + return mFinalizingActions; + } + + /** Copy a template resource */ + private final void copyTemplateResource( + @NonNull String relativeFrom, + @NonNull IPath output) throws IOException { + File from = getFullPath(relativeFrom); + copy(from, output); + } + + /** Returns true if the given file contains the given bytes */ + private static boolean isIdentical(@Nullable byte[] data, @NonNull IFile dest) { + assert dest.exists(); + byte[] existing = AdtUtils.readData(dest); + return Arrays.equals(existing, data); + } + + /** + * Copies the given source file into the given destination file (where the + * source is allowed to be a directory, in which case the whole directory is + * copied recursively) + */ + private void copy(File src, IPath path) throws IOException { + if (src.isDirectory()) { + File[] children = src.listFiles(); + if (children != null) { + for (File child : children) { + copy(child, path.append(child.getName())); + } + } + } else { + IResource dest = mProject.getFile(path); + if (dest.exists() && !(dest instanceof IFile)) {// Don't attempt to overwrite a folder + assert false : dest.getClass().getName(); + return; + } + IFile file = (IFile) dest; + String targetName = path.lastSegment(); + if (dest instanceof IFile) { + if (dest.exists() && isIdentical(Files.toByteArray(src), file)) { + String label = String.format( + "Not overwriting %1$s because the files are identical", targetName); + NullChange change = new NullChange(label); + change.setEnabled(false); + mOtherChanges.add(change); + return; + } + } + + if (targetName.endsWith(DOT_XML) + || targetName.endsWith(DOT_JAVA) + || targetName.endsWith(DOT_TXT) + || targetName.endsWith(DOT_RS) + || targetName.endsWith(DOT_AIDL) + || targetName.endsWith(DOT_SVG)) { + + String newFile = Files.toString(src, Charsets.UTF_8); + newFile = format(mProject, newFile, path); + + TextFileChange addFile = createNewFileChange(file); + addFile.setEdit(new InsertEdit(0, newFile)); + mTextChanges.add(addFile); + } else { + // Write binary file: Need custom change for that + IPath workspacePath = mProject.getFullPath().append(path); + mOtherChanges.add(new CreateFileChange(targetName, workspacePath, src)); + } + } + } + + /** + * A custom {@link TemplateLoader} which locates and provides templates + * within the plugin .jar file + */ + private static final class MyTemplateLoader implements TemplateLoader { + private String mPrefix; + + public void setPrefix(String prefix) { + mPrefix = prefix; + } + + public void setTemplateFile(File file) { + setTemplateParent(file.getParentFile()); + } + + public void setTemplateParent(File parent) { + mPrefix = parent.getPath(); + } + + @Override + public Reader getReader(Object templateSource, String encoding) throws IOException { + URL url = (URL) templateSource; + return new InputStreamReader(url.openStream(), encoding); + } + + @Override + public long getLastModified(Object templateSource) { + return 0; + } + + @Override + public Object findTemplateSource(String name) throws IOException { + String path = mPrefix != null ? mPrefix + '/' + name : name; + File file = new File(path); + if (file.exists()) { + return file.toURI().toURL(); + } + return null; + } + + @Override + public void closeTemplateSource(Object templateSource) throws IOException { + } + } + + /** + * Validates this template to make sure it's supported + * @param currentMinSdk the minimum SDK in the project, or -1 or 0 if unknown (e.g. codename) + * @param buildApi the build API, or -1 or 0 if unknown (e.g. codename) + * + * @return a status object with the error, or null if there is no problem + */ + @SuppressWarnings("cast") // In Eclipse 3.6.2 cast below is needed + @Nullable + public IStatus validateTemplate(int currentMinSdk, int buildApi) { + TemplateMetadata template = getTemplate(); + if (template == null) { + return null; + } + if (!template.isSupported()) { + String versionString = (String) AdtPlugin.getDefault().getBundle().getHeaders().get( + Constants.BUNDLE_VERSION); + Version version = new Version(versionString); + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + String.format("This template requires a more recent version of the " + + "Android Eclipse plugin. Please update from version %1$d.%2$d.%3$d.", + version.getMajor(), version.getMinor(), version.getMicro())); + } + int templateMinSdk = template.getMinSdk(); + if (templateMinSdk > currentMinSdk && currentMinSdk >= 1) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + String.format("This template requires a minimum SDK version of at " + + "least %1$d, and the current min version is %2$d", + templateMinSdk, currentMinSdk)); + } + int templateMinBuildApi = template.getMinBuildApi(); + if (templateMinBuildApi > buildApi && buildApi >= 1) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + String.format("This template requires a build target API version of at " + + "least %1$d, and the current version is %2$d", + templateMinBuildApi, buildApi)); + } + + return null; + } +} |