PageRenderTime 125ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 1ms

/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateHandler.java

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