blob: 734183faa51a328632d552d872a783d27225c9da [file] [log] [blame]
/*
* 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.android.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
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.api.statements.blocks.GrClosableBlock;
import java.util.List;
import static com.android.tools.idea.gradle.parser.BuildFileKeyType.*;
import static com.android.tools.idea.gradle.parser.ValueFactory.KeyFilter;
/**
* Enumerates the values we know how to parse out of the build file. This includes values that only occur in one place
* and are always rooted at the file root (e.g. android/buildToolsVersion) and values that can occur at different places (e.g.
* signingConfig, which can occur in defaultConfig, a build type, or a flavor. When retrieving keys that are of the former type, you
* can call {@link GradleBuildFile#getValue(BuildFileKey)}, which uses the build file itself as
* the root; in the case of the latter, call
* {@link GradleBuildFile#getValue(org.jetbrains.plugins.groovy.lang.psi.api.util.GrStatementOwner, BuildFileKey)}
* and pass in the block that is the root of the key's path.
* <p>
* Values have types. Scalar types like string, integer, etc. are self-explanatory, but complex closure types are supported as well. In
* that case, you can supply a factory class that knows how to instantiate container instances for subobjects that are found within a
* given closure in the build file.
*/
public enum BuildFileKey {
// Buildscript block
PLUGIN_CLASSPATH("buildscript/dependencies/classpath", STRING),
PLUGIN_REPOSITORY("buildscript/repositories", "Android Plugin Repository", CLOSURE, Repository.getFactory()),
PLUGIN_VERSION("buildscript/dependencies/classpath", "Android Plugin Version", STRING, null) {
@Override
public Object getValue(@NotNull GroovyPsiElement arg) {
// PLUGIN_CLASSPATH is STRING type; we're guaranteed the getValue result can be cast to String.
String s = (String)PLUGIN_CLASSPATH.getValue(arg);
if (s != null && s.startsWith(SdkConstants.GRADLE_PLUGIN_NAME)) {
return s.substring(SdkConstants.GRADLE_PLUGIN_NAME.length());
} else {
return null;
}
}
@Override
public void setValue(@NotNull GroovyPsiElement arg, @NotNull Object value, @Nullable KeyFilter filter) {
PLUGIN_CLASSPATH.setValue(arg, SdkConstants.GRADLE_PLUGIN_NAME + value);
}
},
ALLPROJECTS_LIBRARY_REPOSITORY("allprojects/repositories", "Default Library Repository", CLOSURE, Repository.getFactory()),
LIBRARY_REPOSITORY("repositories", "Library Repository", CLOSURE, Repository.getFactory()),
// Dependencies block
DEPENDENCIES("dependencies", null, CLOSURE, Dependency.getFactory()),
// defaultConfig or build flavor
MIN_SDK_VERSION("minSdkVersion", INTEGER_OR_STRING),
APPLICATION_ID("applicationId", STRING),
PROGUARD_FILE("proguardFile", FILE_AS_STRING),
SIGNING_CONFIG("signingConfig", REFERENCE),
TARGET_SDK_VERSION("targetSdkVersion", INTEGER_OR_STRING),
TEST_INSTRUMENTATION_RUNNER("testInstrumentationRunner", STRING),
TEST_APPLICATION_ID("testApplicationId", STRING),
VERSION_CODE("versionCode", INTEGER),
VERSION_NAME("versionName", STRING),
// Build type
DEBUGGABLE("debuggable", BOOLEAN),
JNI_DEBUG_BUILD("jniDebuggable", BOOLEAN),
RENDERSCRIPT_DEBUG_BUILD("renderscriptDebuggable", BOOLEAN),
RENDERSCRIPT_OPTIM_LEVEL("renderscriptOptimLevel", INTEGER),
MINIFY_ENABLED("minifyEnabled", BOOLEAN),
PSEUDOLOCALES_ENABLED("pseudoLocalesEnabled", BOOLEAN),
APPLICATION_ID_SUFFIX("applicationIdSuffix", STRING),
VERSION_NAME_SUFFIX("versionNameSuffix", STRING),
ZIP_ALIGN("zipAlignEnabled", BOOLEAN),
// Signing config
KEY_ALIAS("keyAlias", STRING),
KEY_PASSWORD("keyPassword", STRING),
STORE_FILE("storeFile", FILE),
STORE_PASSWORD("storePassword", STRING),
// Android block
DEFAULT_CONFIG("android/defaultConfig", CLOSURE),
BUILD_TOOLS_VERSION("android/buildToolsVersion", STRING),
COMPILE_SDK_VERSION("android/compileSdkVersion", INTEGER_OR_STRING),
IGNORE_ASSETS_PATTERN("android/aaptOptions/ignoreAssetsPattern", STRING),
INCREMENTAL_DEX("android/dexOptions/incremental", "Incremental Dex", BOOLEAN, null),
NO_COMPRESS("android/aaptOptions/noCompress", STRING), // TODO: Implement properly. This is not a simple literal.
SOURCE_COMPATIBILITY("android/compileOptions/sourceCompatibility", REFERENCE),
TARGET_COMPATIBILITY("android/compileOptions/targetCompatibility", REFERENCE),
// Non-Gradle build file keys
// These keys set values in places other than Gradle build files (e.g. the Gradle wrapper properties, which is a Java properties file)
// They are included here so that other code can reuse the infrastructure built up around BuildFileKeys.
GRADLE_WRAPPER_VERSION("", "Gradle version", STRING, NonGroovyValueFactory.getFactory()),
// These complex types are named entities that within them have key/value pairs where the keys are BuildFileKey instances themselves.
// We can use a generic container class to deal with them.
// It would be nice if these lists of sub-parameters were static constants, but that results in unresolvable circular references.
SIGNING_CONFIGS("android/signingConfigs", null, CLOSURE,
NamedObject.getFactory(ImmutableList.of(KEY_ALIAS, KEY_PASSWORD, STORE_FILE, STORE_PASSWORD))),
FLAVORS("android/productFlavors", null, CLOSURE,
NamedObject.getFactory(ImmutableList.of(MIN_SDK_VERSION, APPLICATION_ID, PROGUARD_FILE, SIGNING_CONFIG, TARGET_SDK_VERSION,
TEST_INSTRUMENTATION_RUNNER, TEST_APPLICATION_ID, VERSION_CODE, VERSION_NAME))),
BUILD_TYPES("android/buildTypes", null, CLOSURE,
NamedObject.getFactory(ImmutableList
.of(DEBUGGABLE, JNI_DEBUG_BUILD, SIGNING_CONFIG, RENDERSCRIPT_DEBUG_BUILD, RENDERSCRIPT_OPTIM_LEVEL,
MINIFY_ENABLED, PSEUDOLOCALES_ENABLED, PROGUARD_FILE, APPLICATION_ID_SUFFIX, VERSION_NAME_SUFFIX,
ZIP_ALIGN)));
static {
SIGNING_CONFIG.myContainerType = SIGNING_CONFIGS;
SIGNING_CONFIGS.myItemType = SIGNING_CONFIG;
SIGNING_CONFIGS.myShouldInsertAtBeginning = true;
}
private final String myPath;
private final BuildFileKeyType myType;
private final ValueFactory myValueFactory;
private final String myDisplayName;
private BuildFileKey myContainerType;
private BuildFileKey myItemType;
private boolean myShouldInsertAtBeginning;
BuildFileKey(@NotNull String path, @NotNull BuildFileKeyType type) {
this(path, null, type, null);
}
/**
* @param path an XPath-like identifier to a method call which may be inside nested closures. For example, "a/b/c" will identify
* <code>a { b { c("value") } }</code>
*/
BuildFileKey(@NotNull String path, @Nullable String displayName, @NotNull BuildFileKeyType type, @Nullable ValueFactory factory) {
myPath = path;
myType = type;
myValueFactory = factory;
if (displayName != null) {
myDisplayName = displayName;
} else {
int lastSlash = myPath.lastIndexOf('/');
myDisplayName = splitCamelCase(lastSlash >= 0 ? myPath.substring(lastSlash + 1) : myPath);
}
}
@NotNull
@VisibleForTesting
static String splitCamelCase(@NotNull String string) {
StringBuilder sb = new StringBuilder(2 * string.length());
int n = string.length();
boolean lastWasUpperCase = Character.isUpperCase(string.charAt(0));
boolean capitalizeNext = true;
for (int i = 0; i < n; i++) {
char c = string.charAt(i);
boolean isUpperCase = Character.isUpperCase(c);
if (isUpperCase && !lastWasUpperCase) {
sb.append(' ');
capitalizeNext = true;
}
lastWasUpperCase = isUpperCase;
if (capitalizeNext) {
c = Character.toUpperCase(c);
capitalizeNext = false;
} else {
c = Character.toLowerCase(c);
}
sb.append(c);
}
return sb.toString();
}
@NotNull
static String escapeLiteralString(@Nullable Object o) {
if (o == null) {
return "";
}
return o.toString().replace("'", "\\'");
}
@NotNull
public String getDisplayName() {
return myDisplayName;
}
@NotNull
public BuildFileKeyType getType() {
return myType;
}
@Nullable
public ValueFactory getValueFactory() {
return myValueFactory;
}
@NotNull
public String getPath() {
return myPath;
}
/**
* For keys whose values are Groovy expressions that reference another object in the same build file, this returns the type of the
* referenced object. For example, the {@link SIGNING_CONFIG} key returns a reference to the container {@link SIGNING_CONFIGS} key.
*/
@Nullable
public BuildFileKey getContainerType() {
return myContainerType;
}
/**
* For keys whose values are containers of named objects that can be referred to elsewhere in the build file via
* {@link com.android.tools.idea.gradle.parser.BuildFileKeyType#REFERENCE} links, this returns the key for those named object items.
* For example, the {@link SIGNING_CONFIGS} key returns a reference to the item type {@link SIGNING_CONFIG} key.
*/
@Nullable
public BuildFileKey getItemType() {
return myItemType;
}
/**
* True if this block should be inserted at the beginning of its parent's closure instead of at the end. For types that can be referenced
* from other keys, such as {@link SIGNING_CONFIGS}, the objects must appear in the file before references to them. Groovy executes the
* block sequentially at build file evaluation time, and any references must resolve to objects it's already seen.
* @return
*/
public boolean shouldInsertAtBeginning() {
return myShouldInsertAtBeginning;
}
@Nullable
protected Object getValue(@NotNull GroovyPsiElement arg) {
if (myValueFactory != null && arg instanceof GrClosableBlock) {
return myValueFactory.getValues((GrClosableBlock) arg);
}
return myType.getValue(arg);
}
protected void setValue(@NotNull GroovyPsiElement arg, @NotNull Object value) {
setValue(arg, value, null);
}
protected void setValue(@NotNull GroovyPsiElement arg, @NotNull Object value, @Nullable KeyFilter filter) {
if (myValueFactory != null && arg instanceof GrClosableBlock && value instanceof List) {
myValueFactory.setValues((GrClosableBlock) arg, (List)value, filter);
} else {
myType.setValue(arg, value);
}
}
@Nullable
public static BuildFileKey findByPath(@NotNull String path) {
for (BuildFileKey key : values()) {
if (path.equals(key.myPath)) {
return key;
}
}
return null;
}
}