/* * Copyright (C) 2014 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.repository.GradleCoordinate; import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Multimap; import com.intellij.ide.startup.impl.StartupManagerImpl; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.Result; import com.intellij.openapi.command.WriteCommandAction; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectManager; import com.intellij.openapi.project.impl.ProjectManagerImpl; import com.intellij.openapi.startup.StartupManager; import com.intellij.openapi.util.Disposer; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFileFactory; import com.intellij.psi.codeStyle.CodeStyleManager; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.plugins.groovy.GroovyFileType; import org.jetbrains.plugins.groovy.lang.psi.GroovyFile; import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElementFactory; import org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrArgumentList; import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrCall; import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression; import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression; import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrLiteral; import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.path.GrCallExpression; import java.io.File; import java.util.*; import static com.android.ide.common.repository.GradleCoordinate.COMPARE_PLUS_LOWER; /** * Utility class to help with merging Gradle files into one another */ public class GradleFileMerger { private static final String DEPENDENCIES = "dependencies"; private static final String COMPILE = "compile"; public static final String COMPILE_FORMAT = "compile '%s'\n"; public static String mergeGradleFiles(@NotNull String source, @NotNull String dest, @Nullable Project project) { source = source.replace("\r", ""); dest = dest.replace("\r", ""); final Project project2; boolean projectNeedsCleanup = false; if (project != null) { project2 = project; } else { project2 = ((ProjectManagerImpl)ProjectManager.getInstance()).newProject("MergingOnly", "", false, true); assert project2 != null; ((StartupManagerImpl)StartupManager.getInstance(project2)).runStartupActivities(); projectNeedsCleanup = true; } final GroovyFile templateBuildFile = (GroovyFile)PsiFileFactory.getInstance(project2).createFileFromText(SdkConstants.FN_BUILD_GRADLE, GroovyFileType.GROOVY_FILE_TYPE, source); final GroovyFile existingBuildFile = (GroovyFile)PsiFileFactory.getInstance(project2).createFileFromText(SdkConstants.FN_BUILD_GRADLE, GroovyFileType.GROOVY_FILE_TYPE, dest); String result = (new WriteCommandAction<String>(project2, "Merge Gradle Files", existingBuildFile) { @Override protected void run(@NotNull Result<String> result) throws Throwable { mergePsi(templateBuildFile, existingBuildFile, project2); PsiElement formatted = CodeStyleManager.getInstance(project2).reformat(existingBuildFile); result.setResult(formatted.getText()); } }).execute().getResultObject(); if (projectNeedsCleanup) { ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { Disposer.dispose(project2); } }); } return result; } private static void mergePsi(@NotNull PsiElement fromRoot, @NotNull PsiElement toRoot, @NotNull Project project) { Set<PsiElement> destinationChildren = new HashSet<PsiElement>(); destinationChildren.addAll(Arrays.asList(toRoot.getChildren())); // First try and do a string literal replacement. // If both toRoot and fromRoot are call expressions if (toRoot instanceof GrCallExpression && fromRoot instanceof GrCallExpression) { PsiElement[] fromArguments = fromRoot.getLastChild().getChildren(); PsiElement[] toArguments = toRoot.getLastChild().getChildren(); // and both have only one argument and that argument is a literal if (toArguments.length == 1 && fromArguments.length == 1 && toArguments[0] instanceof GrLiteral && fromArguments[0] instanceof GrLiteral) { // End this branch by replacing the old literal with the new toArguments[0].replace(fromArguments[0]); return; } } // Do an element-wise (disregarding order) child comparison for (PsiElement child : fromRoot.getChildren()) { PsiElement destination = findEquivalentElement(destinationChildren, child); if (destination == null) { if (destinationChildren.isEmpty()) { toRoot.add(child); } else { toRoot.addBefore(child, toRoot.getLastChild()); } // And we're done for this branch } else if (child.getFirstChild() != null && child.getFirstChild().getText().equalsIgnoreCase(DEPENDENCIES) && destination.getFirstChild() != null && destination.getFirstChild().getText().equalsIgnoreCase(DEPENDENCIES)) { // Special case dependencies // The last child of the dependencies method call is the closable block mergeDependencies(child.getLastChild(), destination.getLastChild(), project); } else { mergePsi(child, destination, project); } } } private static void mergeDependencies(@NotNull PsiElement fromRoot, @NotNull PsiElement toRoot, @NotNull Project project) { Multimap<String, GradleCoordinate> dependencies = LinkedListMultimap.create(); List<String> unparseableDependencies = new ArrayList<String>(); // Load existing dependencies into the map for the existing build.gradle pullDependenciesIntoMap(toRoot, dependencies, null); // Load dependencies into the map for the new build.gradle pullDependenciesIntoMap(fromRoot, dependencies, unparseableDependencies); GroovyPsiElementFactory factory = GroovyPsiElementFactory.getInstance(project); RepositoryUrlManager urlManager = RepositoryUrlManager.get(); for (String key : dependencies.keySet()) { GradleCoordinate highest = Collections.max(dependencies.get(key), COMPARE_PLUS_LOWER); // For test consistency, don't depend on installed SDK state while testing if (!ApplicationManager.getApplication().isUnitTestMode() || Boolean.getBoolean("force.gradlemerger.repository.check")) { // If this coordinate points to an artifact in one of our repositories, check to see if there is a static version // that we can add instead of a plus revision. if (RepositoryUrlManager.supports(highest.getArtifactId())) { String libraryCoordinate = urlManager.getLibraryCoordinate(highest.getArtifactId(), null, false /* No previews */); GradleCoordinate available = GradleCoordinate.parseCoordinateString(libraryCoordinate); if (available != null) { File archiveFile = urlManager.getArchiveForCoordinate(available); if (archiveFile != null && archiveFile.exists() && COMPARE_PLUS_LOWER.compare(available, highest) >= 0) { highest = available; } } } } PsiElement dependencyElement = factory.createStatementFromText(String.format(COMPILE_FORMAT, highest.toString())); toRoot.addBefore(dependencyElement, toRoot.getLastChild()); } for (String unparseableDependency : unparseableDependencies) { PsiElement dependencyElement = factory.createStatementFromText(unparseableDependency); toRoot.addBefore(dependencyElement, toRoot.getLastChild()); } } /** * Looks for 'compile "*"' statements and tries to parse them into Gradle coordinates. If successful, * adds the new coordinate to the map and removes the corresponding PsiElement from the tree. * @return true if new items were added to the map */ private static boolean pullDependenciesIntoMap(@NotNull PsiElement root, Multimap<String, GradleCoordinate> map, @Nullable List<String> unparseableDependencies) { boolean wasMapUpdated = false; for (PsiElement existingElem : root.getChildren()) { if (existingElem instanceof GrCall) { PsiElement reference = existingElem.getFirstChild(); if (reference instanceof GrReferenceExpression && reference.getText().equalsIgnoreCase(COMPILE)) { boolean parsed = false; GrCall call = (GrCall)existingElem; GrArgumentList arguments = call.getArgumentList(); // Don't try merging dependencies if one of them has a closure block attached. if (arguments != null && call.getClosureArguments().length == 0) { GrExpression[] expressionArguments = arguments.getExpressionArguments(); if (expressionArguments.length == 1 && expressionArguments[0] instanceof GrLiteral) { Object value = ((GrLiteral)expressionArguments[0]).getValue(); if (value instanceof String) { String coordinateText = (String)value; GradleCoordinate coordinate = GradleCoordinate.parseCoordinateString(coordinateText); if (coordinate != null) { parsed = true; if (!map.get(coordinate.getId()).contains(coordinate)) { map.put(coordinate.getId(), coordinate); existingElem.delete(); wasMapUpdated = true; } } } } } if (!parsed && unparseableDependencies != null) { unparseableDependencies.add(existingElem.getText()); } } } } return wasMapUpdated; } /** * Finds an exact match if possible (and returns it) otherwise, looks for a unique "close" match (Defined as a matching * reference expression). If only one "close" match is found, then that match gets returned. Otherwise returns null. */ @Nullable private static PsiElement findEquivalentElement(@NotNull Collection<PsiElement> collection, @NotNull PsiElement element) { List<PsiElement> matchingItems = Lists.newArrayListWithExpectedSize(1); for (PsiElement item : collection) { if (item.getText() != null && item.getText().equals(element.getText())) { return item; } else if (item.getFirstChild() != null && element.getFirstChild() != null) { if (item.getFirstChild().getText().equals(element.getFirstChild().getText())) { matchingItems.add(item); } } } if (matchingItems.size() == 1) { return matchingItems.get(0); } else { return null; } } }