/* * 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), @Deprecated // Deprecated in 0.11, to be removed by 1.0. Here for migration purposes. PACKAGE_NAME("packageName", 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), @Deprecated // Deprecated in 0.11, to be removed by 1.0. Here for migration purposes. TEST_PACKAGE_NAME("testPackageName", STRING), VERSION_CODE("versionCode", INTEGER), VERSION_NAME("versionName", STRING), // Build type DEBUGGABLE("debuggable", BOOLEAN), JNI_DEBUG_BUILD("jniDebugBuild", BOOLEAN), RENDERSCRIPT_DEBUG_BUILD("renderscriptDebugBuild", BOOLEAN), RENDERSCRIPT_OPTIM_LEVEL("renderscriptOptimLevel", INTEGER), RUN_PROGUARD("runProguard", BOOLEAN), APPLICATION_ID_SUFFIX("applicationIdSuffix", STRING), @Deprecated // Deprecated in 0.11, to be removed by 1.0. Here for migration purposes. PACKAGE_NAME_SUFFIX("packageNameSuffix", STRING), VERSION_NAME_SUFFIX("versionNameSuffix", STRING), ZIP_ALIGN("zipAlign", 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, RUN_PROGUARD, 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); } } }