blob: 8e11841b41b31d67ce8f063adcb0ee031591550f [file] [log] [blame]
/*
* 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;
}
}