| /* |
| * 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 org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrString; |
| import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrStringContent; |
| import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrStringInjection; |
| |
| 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 = "(" + escapeAndQuote(data) + ")"; |
| } else { |
| extraGroovyCode = " " + escapeAndQuote(data); |
| } |
| break; |
| case MODULE: |
| if (data instanceof Map) { |
| extraGroovyCode = " project(" + GradleGroovyFile.convertMapToGroovySource((Map<String, Object>)data) + ")"; |
| } else { |
| extraGroovyCode = " project(" + escapeAndQuote(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(" + escapeAndQuote(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); |
| } |
| |
| /** |
| * Groovy has either plain strings or rich gstrings, i.e. it's possible to write <code>'position number $index'</code> |
| * (single quotes - plain string) or <code>"position number $index"</code> (double quotes, 'index' variable value will be inserted |
| * in runtime). |
| * <p/> |
| * This method escapes given string content and surrounds it by correct quotes (trying to guess if it's a pain string or gstring). |
| * |
| * @param stringContent |
| * @return |
| */ |
| @NotNull |
| private static String escapeAndQuote(@Nullable Object data) { |
| if (data == null) { |
| return "''"; |
| } |
| String stringContent = data.toString(); |
| boolean gstring = false; |
| // We assume that given string is a gstring if it has an unescaped dollar sign. |
| for (int i = stringContent.indexOf('$'); i >= 0 && i < stringContent.length(); i = stringContent.indexOf('$', i + 1)) { |
| if (i <= 0 || stringContent.charAt(i - 1) != '\\') { |
| gstring = true; |
| break; |
| } |
| } |
| char quote = gstring ? '"' : '\''; |
| return quote + escapeLiteralString(stringContent) + quote; |
| } |
| |
| /** |
| * 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 if (element instanceof GrString) { |
| GroovyPsiElement[] contentParts = ((GrString)element).getAllContentParts(); |
| final StringBuilder buffer = new StringBuilder(); |
| for (GroovyPsiElement part : contentParts) { |
| if (part instanceof GrStringContent || part instanceof GrStringInjection) { |
| buffer.append(part.getText()); |
| } |
| } |
| if (buffer.length() > 0) { |
| dependencies.add(new Dependency(scope, Dependency.Type.EXTERNAL, buffer.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; |
| } |
| } |
| } |