| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 |
| * |
| * 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.tools.idea.templates; |
| |
| import com.android.SdkConstants; |
| import com.android.ide.common.xml.XmlFormatPreferences; |
| import com.android.ide.common.xml.XmlFormatStyle; |
| import com.android.ide.common.xml.XmlPrettyPrinter; |
| import com.android.manifmerger.ManifestMerger2; |
| import com.android.manifmerger.MergingReport; |
| import com.android.resources.ResourceFolderType; |
| import com.android.utils.StdLogger; |
| import com.android.utils.XmlUtils; |
| import com.google.common.base.Splitter; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Maps; |
| import com.intellij.lang.xml.XMLLanguage; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.io.FileUtil; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.psi.PsiElement; |
| import com.intellij.psi.PsiFileFactory; |
| import com.intellij.psi.codeStyle.CodeStyleManager; |
| import com.intellij.psi.xml.*; |
| import com.intellij.util.SystemProperties; |
| import freemarker.template.TemplateException; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.w3c.dom.Document; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import static com.android.SdkConstants.*; |
| |
| /** |
| * Utility class to support the recipe.xml merge instruction. |
| */ |
| public class RecipeMergeUtils { |
| private static final Logger LOG = Logger.getInstance(RecipeMergeUtils.class); |
| |
| private static final String MERGE_ATTR_STRATEGY = "templateMergeStrategy"; |
| private static final String MERGE_ATTR_STRATEGY_REPLACE = "replace"; |
| private static final String MERGE_ATTR_STRATEGY_PRESERVE = "preserve"; |
| |
| /** |
| * Finds include ':module_name_1', ':module_name_2',... statements in settings.gradle files |
| */ |
| private static final Pattern INCLUDE_PATTERN = Pattern.compile("(^|\\n)\\s*include +(':[^']+', *)*':[^']+'"); |
| |
| public static String mergeGradleSettingsFile(@NotNull String source, @NotNull String dest) throws IOException, TemplateException { |
| // TODO: Right now this is implemented as a dumb text merge. It would be much better to read it into PSI using IJ's Groovy support. |
| // If Gradle build files get first-class PSI support in the future, we will pick that up cheaply. At the moment, Our Gradle-Groovy |
| // support requires a project, which we don't necessarily have when instantiating a template. |
| |
| StringBuilder contents = new StringBuilder(dest); |
| |
| for (String line : Splitter.on('\n').omitEmptyStrings().trimResults().split(source)) { |
| if (!line.startsWith("include")) { |
| throw new RuntimeException("When merging settings.gradle files, only include directives can be merged."); |
| } |
| line = line.substring("include".length()).trim(); |
| |
| Matcher matcher = INCLUDE_PATTERN.matcher(contents); |
| if (matcher.find()) { |
| contents.insert(matcher.end(), ", " + line); |
| } |
| else { |
| contents.insert(0, "include " + line + SystemProperties.getLineSeparator()); |
| } |
| } |
| return contents.toString(); |
| } |
| |
| /** |
| * Merges sourceXml into targetXml/targetFile (targetXml is the contents of targetFile). |
| * Returns the resulting xml if it still needs to be written to targetFile, |
| * or null if the file has already been/doesn't need to be updated. |
| */ |
| @Nullable |
| public static String mergeXml(@NotNull Project project, String sourceXml, String targetXml, File targetFile) { |
| boolean ok; |
| String fileName = targetFile.getName(); |
| String contents; |
| if (fileName.equals(SdkConstants.FN_ANDROID_MANIFEST_XML)) { |
| Document currentDocument = XmlUtils.parseDocumentSilently(targetXml, true); |
| assert currentDocument != null : targetXml + " failed to parse"; |
| Document fragment = XmlUtils.parseDocumentSilently(sourceXml, true); |
| assert fragment != null : sourceXml + " failed to parse"; |
| contents = mergeManifest(targetFile, sourceXml); |
| ok = contents != null; |
| } |
| else { |
| // Merge plain XML files |
| String parentFolderName = targetFile.getParentFile().getName(); |
| ResourceFolderType folderType = ResourceFolderType.getFolderType(parentFolderName); |
| // mergeResourceFile handles the file updates itself, so no content is returned in this case. |
| contents = mergeResourceFile(project, targetXml, sourceXml, folderType); |
| ok = contents != null; |
| } |
| |
| // Finally write out the merged file |
| if (!ok) { |
| // Just insert into file along with comment, using the "standard" conflict |
| // syntax that many tools and editors recognize. |
| |
| contents = wrapWithMergeConflict(targetXml, sourceXml); |
| } |
| return contents; |
| } |
| |
| /** |
| * Merges the given resource file contents into the given resource file |
| */ |
| public static String mergeResourceFile(@NotNull Project project, |
| @NotNull String targetXml, |
| @NotNull String sourceXml, |
| @Nullable ResourceFolderType folderType) { |
| XmlFile targetPsiFile = (XmlFile)PsiFileFactory.getInstance(project) |
| .createFileFromText("targetFile", XMLLanguage.INSTANCE, StringUtil.convertLineSeparators(targetXml)); |
| XmlFile sourcePsiFile = (XmlFile)PsiFileFactory.getInstance(project) |
| .createFileFromText("sourceFile", XMLLanguage.INSTANCE, StringUtil.convertLineSeparators(sourceXml)); |
| XmlTag root = targetPsiFile.getDocument().getRootTag(); |
| assert root != null : "Cannot find XML root in target: " + targetXml; |
| |
| XmlAttribute[] attributes = sourcePsiFile.getRootTag().getAttributes(); |
| for (XmlAttribute attr : attributes) { |
| if (attr.getNamespacePrefix().equals(XMLNS_PREFIX)) { |
| root.setAttribute(attr.getName(), attr.getValue()); |
| } |
| } |
| |
| List<XmlTagChild> prependElements = Lists.newArrayList(); |
| XmlText indent = null; |
| if (folderType == ResourceFolderType.VALUES) { |
| // Try to merge items of the same name |
| Map<String, XmlTag> old = Maps.newHashMap(); |
| for (XmlTag newSibling : root.getSubTags()) { |
| old.put(getResourceId(newSibling), newSibling); |
| } |
| for (PsiElement child : sourcePsiFile.getRootTag().getChildren()) { |
| if (child instanceof XmlComment) { |
| if (indent != null) { |
| prependElements.add(indent); |
| } |
| prependElements.add((XmlTagChild)child); |
| } |
| else if (child instanceof XmlText) { |
| indent = (XmlText)child; |
| } |
| else if (child instanceof XmlTag) { |
| XmlTag subTag = (XmlTag)child; |
| String mergeStrategy = subTag.getAttributeValue(MERGE_ATTR_STRATEGY); |
| subTag.setAttribute(MERGE_ATTR_STRATEGY, null); |
| // remove the space left by the deleted attribute |
| CodeStyleManager.getInstance(project).reformat(subTag); |
| String name = getResourceId(subTag); |
| XmlTag replace = name != null ? old.get(name) : null; |
| if (replace != null) { |
| // There is an existing item with the same id. Either replace it |
| // or preserve it depending on the "templateMergeStrategy" attribute. |
| // If that attribute does not exist, default to preserving 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! |
| if (MERGE_ATTR_STRATEGY_REPLACE.equals(mergeStrategy)) { |
| child = replace.replace(child); |
| // When we're replacing, the line is probably already indented. Skip the initial indent |
| if (child.getPrevSibling() instanceof XmlText && prependElements.get(0) instanceof XmlText) { |
| prependElements.remove(0); |
| // If we're adding something we'll need a newline/indent after it |
| if (!prependElements.isEmpty()) { |
| prependElements.add(indent); |
| } |
| } |
| for (XmlTagChild element : prependElements) { |
| root.addBefore(element, child); |
| } |
| } |
| else if (MERGE_ATTR_STRATEGY_PRESERVE.equals(mergeStrategy)) { |
| // Preserve the existing value. |
| } |
| else { |
| // No explicit directive given, preserve the original value by default. |
| LOG.warn("Warning: Ignoring name conflict in resource file for name " + name); |
| } |
| } |
| else { |
| if (indent != null) { |
| prependElements.add(indent); |
| } |
| subTag = root.addSubTag(subTag, false); |
| for (XmlTagChild element : prependElements) { |
| root.addBefore(element, subTag); |
| } |
| } |
| prependElements.clear(); |
| } |
| } |
| } |
| else { |
| // In other file types, such as layouts, just append all the new content |
| // at the end. |
| for (PsiElement child : sourcePsiFile.getRootTag().getChildren()) { |
| if (child instanceof XmlTag) { |
| root.addSubTag((XmlTag)child, false); |
| } |
| } |
| } |
| return targetPsiFile.getText(); |
| } |
| |
| /** |
| * Merges the given manifest fragment into the given manifest file |
| */ |
| @Nullable |
| private static String mergeManifest(@NotNull File targetManifest, @NotNull String mergeText) { |
| File tempFile = null; |
| try { |
| //noinspection SpellCheckingInspection |
| tempFile = FileUtil.createTempFile("manifmerge", DOT_XML); |
| FileUtil.writeToFile(tempFile, mergeText); |
| StdLogger logger = new StdLogger(StdLogger.Level.INFO); |
| ManifestMerger2.Invoker merger = ManifestMerger2.newMerger(targetManifest, logger, ManifestMerger2.MergeType.APPLICATION) |
| .withFeatures(ManifestMerger2.Invoker.Feature.EXTRACT_FQCNS, ManifestMerger2.Invoker.Feature.NO_PLACEHOLDER_REPLACEMENT) |
| .addLibraryManifest(tempFile); |
| MergingReport mergeReport = merger.merge(); |
| if (mergeReport.getMergedDocument().isPresent()) { |
| return XmlPrettyPrinter |
| .prettyPrint(mergeReport.getMergedDocument().get().getXml(), createXmlFormatPreferences(), XmlFormatStyle.MANIFEST, "\n", |
| mergeText.endsWith("\n")); |
| } |
| return null; |
| } |
| catch (IOException e) { |
| LOG.error(e); |
| } |
| catch (ManifestMerger2.MergeFailureException e) { |
| LOG.error(e); |
| try { |
| FileUtil.appendToFile(tempFile, String.format("<!--%s-->", e.getMessage())); |
| } |
| catch (IOException e1) { |
| LOG.error(e1); |
| } |
| } |
| finally { |
| if (tempFile != null) { |
| tempFile.delete(); |
| } |
| } |
| return null; |
| } |
| |
| |
| private static String getResourceId(@NotNull XmlTag tag) { |
| String name = tag.getAttributeValue(ATTR_NAME); |
| if (name == null) { |
| name = tag.getAttributeValue(ATTR_ID); |
| } |
| |
| return name; |
| } |
| |
| @NotNull |
| private static XmlFormatPreferences createXmlFormatPreferences() { |
| // TODO: implement |
| return XmlFormatPreferences.defaults(); |
| } |
| |
| /** |
| * Wraps the given strings in the standard conflict syntax |
| */ |
| private static String wrapWithMergeConflict(String original, String added) { |
| String sep = "\n"; |
| return "<<<<<<< Original" + sep + original + sep + "=======" + sep + added + ">>>>>>> Added" + sep; |
| } |
| } |