blob: ce1072e8aaafdc0e3243068d21e3c3026ca127f8 [file] [log] [blame]
/*
* 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.DefaultProject;
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 && !(project instanceof DefaultProject)) {
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;
}
}
}