/* * Copyright (C) 2013 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.gradle.parser; import com.android.SdkConstants; import com.google.common.base.Joiner; import com.google.common.base.Objects; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.io.FileUtil; import com.intellij.psi.PsiElement; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElement; import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElementFactory; import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrStatement; import org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrArgumentList; import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock; import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrMethodCall; import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrLiteral; import java.io.File; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import static com.android.tools.idea.gradle.parser.BuildFileKey.escapeLiteralString; /** * Represents a dependency statement in a Gradle build file. Dependencies have a scope (which defines what types of compiles the * dependency is relevant for), a type (e.g. Maven, local jar file, etc.), and dependency-specific data. */ public class Dependency extends BuildFileStatement { private static final Logger LOG = Logger.getInstance(Dependency.class); @NonNls private static final String FILE_TREE_BASE_DIR_PROPERTY = "dir"; @NonNls private static final String FILE_TREE_INCLUDE_PATTERN_PROPERTY = "include"; public enum Scope { COMPILE("Compile", "compile", true, true), PROVIDED("Provided", "provided", true, false), APK("APK", "apk", true, false), ANDROID_TEST_COMPILE("Test compile", "androidTestCompile", true, false), DEBUG_COMPILE("Debug compile", "debugCompile", true, false), RELEASE_COMPILE("Release compile", "releaseCompile", true, false), RUNTIME("Runtime", "runtime", false, true), TEST_COMPILE("Test compile", "testCompile", false, true), TEST_RUNTIME("Test runtime", "testRuntime", false, true); private final String myGroovyMethodCall; private final String myDisplayName; private final boolean myAndroidScope; // True if this is used in Android modules private final boolean myJavaScope; // True if this is used in plain Java modules Scope(@NotNull String displayName, @NotNull String groovyMethodCall, boolean androidScope, boolean javaScope) { myDisplayName = displayName; myGroovyMethodCall = groovyMethodCall; myAndroidScope = androidScope; myJavaScope = javaScope; } public String getGroovyMethodCall() { return myGroovyMethodCall; } @Nullable public static Scope fromMethodCall(@NotNull String methodCall) { for (Scope scope : values()) { if (scope.myGroovyMethodCall.equals(methodCall)) { return scope; } } return null; } @NotNull public String getDisplayName() { return myDisplayName; } public boolean isAndroidScope() { return myAndroidScope; } public boolean isJavaScope() { return myJavaScope; } @Override @NotNull public String toString() { return myDisplayName; } } public enum Type { FILES, FILETREE, EXTERNAL, MODULE } public Scope scope; public Type type; public Object data; public String extraClosure; public Dependency(@NotNull Scope scope, @NotNull Type type, @NotNull Object data, @Nullable String extraClosure) { this.scope = scope; this.type = type; this.data = data; this.extraClosure = extraClosure; } public Dependency(@NotNull Scope scope, @NotNull Type type, @NotNull Object data) { this(scope, type, data, null); } @Override @NotNull public List<PsiElement> getGroovyElements(@NotNull GroovyPsiElementFactory factory) { String extraGroovyCode; switch (type) { case EXTERNAL: if (extraClosure != null) { extraGroovyCode = "('" + escapeLiteralString(data) + "')"; } else { extraGroovyCode = " '" + escapeLiteralString(data) + "'"; } break; case MODULE: if (data instanceof Map) { extraGroovyCode = " project(" + GradleGroovyFile.convertMapToGroovySource((Map<String, Object>)data) + ")"; } else { extraGroovyCode = " project('" + escapeLiteralString(data) + "')"; } if (extraClosure != null) { // If there's a closure with exclusion rules, then we need extra parentheses: // compile project(':foo') { ... } is not valid Groovy syntax // compile(project(':foo')) { ... } is correct extraGroovyCode = "(" + extraGroovyCode.substring(1) + ")"; } break; case FILES: extraGroovyCode = " files('" + escapeLiteralString(data) + "')"; break; case FILETREE: extraGroovyCode = " fileTree(" + GradleGroovyFile.convertMapToGroovySource((Map<String, Object>)data) + ")"; break; default: extraGroovyCode = ""; break; } GrStatement statement = factory.createStatementFromText(scope.getGroovyMethodCall() + extraGroovyCode); if (statement instanceof GrMethodCall && extraClosure != null) { statement.add(factory.createClosureFromText(extraClosure)); } return ImmutableList.of((PsiElement)statement); } /** * Returns true if the given dependency "matches" or "is covered" by this dependency. It will return true if they are equal * in the {@link #equals(Object)}} sense, but it does some broader matching as well, in order to aid the use case of merging new * dependencies into existing build files. * * <ul> * <li>For Maven-style dependencies, it only checks the group and name parts of the coordinate; it ignores version number * and packaging. This allows us to gloss over differences between minor version numbers, or whether a version number uses * + syntax, by giving up on it altogether. It also glosses over whether a dependency has explicit packaging (e.g. @aar) * specified or not by also giving up on it.</li> * <li>For module dependencies, it ignores the leading colon in the Gradle module specification when comparing.</li> * <li>For files('...') dependencies, it will match a filetree dependency that includes the same file; e.g. * files('libs/foo.jar') is matched by fileTree(dir: 'lib', include: ['*.jar', '*.aar'])</li> * <li>It has hardcoded knowledge that the appcompat-v7 library includes support-v4.</li> * </ul> */ public boolean matches(@NotNull Dependency dependency) { if (equals(dependency)) { return true; } if (scope != dependency.scope) { return false; } String s1 = data.toString(); String s2 = dependency.data.toString(); switch(type) { default: case MODULE: if (dependency.type != Type.MODULE) { return false; } if (data instanceof Map) { s1 = GradleGroovyFile.convertMapToGroovySource((Map<String, Object>)data).replaceAll("path: ':", "path: '"); } if (dependency.data instanceof Map) { s2 = GradleGroovyFile.convertMapToGroovySource((Map<String, Object>)dependency.data).replaceAll("path: ':", "path: '"); } if (s1.startsWith(":")) { s1 = s1.substring(1); } if (s2.startsWith(":")) { s2 = s2.substring(1); } return (s1.equals(s2)); case EXTERNAL: if (dependency.type != Type.EXTERNAL) { return false; } // Special hardcoded case: com.android.support:appcompat-v7 includes com.android.support:support-v4 if (s1.startsWith(SdkConstants.APPCOMPAT_LIB_ARTIFACT) && s2.startsWith(SdkConstants.SUPPORT_LIB_ARTIFACT)) { return true; } // Maven dependencies match if they share the same group and artifact. We ignore version and packaging. String[] tokens1 = s1.split(":"); String[] tokens2 = s2.split(":"); if (tokens1.length < 2 || tokens2.length < 2) { return false; } return tokens1[0].equals(tokens2[0]) && tokens1[1].equals(tokens2[1]); case FILES: if (dependency.type != Type.FILES) { return false; } return FileUtil.pathsEqual(s1, s2); case FILETREE: if (dependency.type != Type.FILES) { return false; } Map<String, Object> values = (Map<String, Object>)data; String dir = (String)values.get(FILE_TREE_BASE_DIR_PROPERTY); Object value = values.get(FILE_TREE_INCLUDE_PATTERN_PROPERTY); if (value == null) { return false; } List<String> includes = (value instanceof List) ? (List<String>)value : ImmutableList.of(value.toString()); if (dir == null || includes == null) { return false; } File baseDir = new File(dir); File depFile = new File(s2); File depDir = depFile.getParentFile(); if (depDir == null) { return false; } if (FileUtil.filesEqual(baseDir, depDir)) { for (String glob : includes) { Pattern pattern = Pattern.compile(FileUtil.convertAntToRegexp(glob)); if (pattern.matcher(depFile.getName()).matches()) { return true; } } } return false; } } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Dependency that = (Dependency)o; if (data != null ? !data.equals(that.data) : that.data != null) { return false; } if (scope != that.scope) { return false; } if (type != that.type) { return false; } return true; } @Override public int hashCode() { return Objects.hashCode(scope, type, data); } @Override public String toString() { return "Dependency {" + "myScope=" + scope + ", myType=" + type + ", myData='" + data + '\'' + '}'; } public @NotNull String getValueAsString() { return data.toString(); } public static ValueFactory getFactory() { return new DependencyFactory(); } private static class DependencyFactory extends BuildFileStatementFactory { @NotNull @Override public List<BuildFileStatement> getValues(@NotNull PsiElement statement) { if (!(statement instanceof GrMethodCall)) { return getUnparseableStatements(statement); } GrMethodCall call = (GrMethodCall)statement; Dependency.Scope scope = Dependency.Scope.fromMethodCall(GradleGroovyFile.getMethodCallName(call)); if (scope == null) { return getUnparseableStatements(statement); } String extraClosure = null; GrClosableBlock[] closureArguments = ((GrMethodCall)statement).getClosureArguments(); if (closureArguments.length > 0) { extraClosure = closureArguments[0].getText(); } GrArgumentList argumentList = call.getArgumentList(); List<BuildFileStatement> dependencies = Lists.newArrayList(); GroovyPsiElement[] allArguments = argumentList.getAllArguments(); if (allArguments.length == 1) { GroovyPsiElement element = allArguments[0]; if (element instanceof GrMethodCall) { GrMethodCall method = (GrMethodCall)element; String methodName = GradleGroovyFile.getMethodCallName(method); if ("project".equals(methodName)) { Object value = GradleGroovyFile.getFirstLiteralArgumentValue(method); if (value != null) { dependencies.add(new Dependency(scope, Dependency.Type.MODULE, value.toString(), extraClosure)); } else { Map<String, Object> values = GradleGroovyFile.getNamedArgumentValues(method); if (!values.isEmpty()) { dependencies.add(new Dependency(scope, Type.MODULE, values, extraClosure)); } } } else if ("files".equals(methodName)) { for (Object o : GradleGroovyFile.getLiteralArgumentValues(method)) { dependencies.add(new Dependency(scope, Dependency.Type.FILES, o.toString(), extraClosure)); } } else if ("fileTree".equals(methodName)) { Map<String, Object> values = GradleGroovyFile.getNamedArgumentValues(method); dependencies.add(new Dependency(scope, Type.FILETREE, values, extraClosure)); } else { // Oops, we didn't know how to parse this. LOG.warn("Didn't know how to parse dependency method call " + methodName); } } else if (element instanceof GrLiteral) { Object value = ((GrLiteral)element).getValue(); if (value != null) { dependencies.add(new Dependency(scope, Dependency.Type.EXTERNAL, value.toString(), extraClosure)); } } else { return getUnparseableStatements(statement); } } else if (allArguments.length > 1) { Map<String, Object> attributes = GradleGroovyFile.getNamedArgumentValues(call); if (attributes.isEmpty()) { return getUnparseableStatements(statement); } Object groupId = attributes.get("group"); Object artifactId = attributes.get("name"); Object version = attributes.get("version"); Object ext = attributes.get("ext"); if (groupId == null || artifactId == null || version == null) { return getUnparseableStatements(statement); } String coordinate = Joiner.on(":").join(groupId, artifactId, version); if (ext != null) { coordinate = coordinate + "@" + ext; } dependencies.add(new Dependency(scope, Dependency.Type.EXTERNAL, coordinate, extraClosure)); } return dependencies; } } }