blob: 31e6899688f90cb9de4cb889c211e2d683ba8b57 [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.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.intellij.psi.PsiElement;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElementFactory;
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.util.GrStatementOwner;
import java.text.Collator;
import java.util.List;
import java.util.Map;
/**
* This is a container class for configurable objects in build.gradle files like signing configs, flavors, build types, and the like.
* These are named entities that live inside a parent closure, like buildType1 and buildType2 in this example:
* <p>
* <code>
* android {
* buildTypes {
* buildType1 {
* someProperty "someValue"
* anotherProperty "anotherValue"
* }
* buildType2 {
* someProperty "differentValue"
* anotherProperty "anotherDifferentValue"
* }
* }
* }
* </code>
*
* <p>
* These objects are syntactically method calls in Groovy; the method name is the object's name, and the argument is a closure that
* takes property/value statements (which are themselves method calls).
*/
public class NamedObject implements Comparable<NamedObject> {
private String myName;
private final boolean myAlwaysOnTop;
private final Map<BuildFileKey, Object> myValues = Maps.newHashMap();
public NamedObject(@NotNull String name) {
this(name, false);
}
protected NamedObject(@NotNull String name, boolean alwaysOnTop) {
myName = name;
myAlwaysOnTop = alwaysOnTop;
}
public NamedObject(@NotNull NamedObject obj) {
myName = obj.myName;
myAlwaysOnTop = obj.myAlwaysOnTop;
myValues.putAll(obj.myValues);
}
@NotNull
public String getName() {
return myName;
}
@NotNull
public Map<BuildFileKey, Object> getValues() {
return myValues;
}
@Nullable
public Object getValue(@NotNull BuildFileKey buildFileKey) {
return myValues.get(buildFileKey);
}
public void setName(@NotNull String name) {
myName = name;
}
public void setValue(@NotNull BuildFileKey property, @Nullable Object value) {
if (value == null) {
myValues.remove(property);
} else {
myValues.put(property, value);
}
}
public static ValueFactory getFactory(@NotNull List<BuildFileKey> properties) {
return new Factory(properties);
}
@Override
public int compareTo(NamedObject o) {
if (myAlwaysOnTop != o.myAlwaysOnTop) {
return myAlwaysOnTop ? -1 : 1;
}
return Collator.getInstance().compare(myName, o.myName);
}
public static class Factory extends ValueFactory<NamedObject> {
private final List<BuildFileKey> myProperties;
private Factory(@NotNull List<BuildFileKey> properties) {
myProperties = properties;
}
/**
* When looking for values to delete, do a less fragile comparison based only on object names and not identity.
*/
@Override
protected Iterable<NamedObject> findValuesToDelete(@NotNull GrStatementOwner closure,
@NotNull final List<NamedObject> replacementValues) {
return Iterables.filter(getValues(closure), new Predicate<NamedObject>() {
@Override
public boolean apply(NamedObject input) {
for (NamedObject value : replacementValues) {
if (value.getName().equals(input.getName())) {
return false;
}
}
return true;
}
});
}
@Override
protected void setValue(@NotNull GrStatementOwner closure, @NotNull NamedObject object, @Nullable KeyFilter filter) {
GrClosableBlock subclosure = GradleGroovyFile.getMethodClosureArgument(closure, object.myName);
GroovyPsiElementFactory factory = GroovyPsiElementFactory.getInstance(closure.getProject());
if (subclosure == null) {
closure.addBefore(factory.createStatementFromText(object.getName() + " {\n}\n"), closure.getLastChild());
subclosure = GradleGroovyFile.getMethodClosureArgument(closure, object.myName);
if (subclosure == null) {
return;
}
}
for (BuildFileKey property : myProperties) {
if (filter != null && !filter.shouldWriteKey(property, object)) {
continue;
}
Object value = object.getValue(property);
if (value != null) {
// Don't need to pass the filter along. It's already done its job on the parent key.
GradleGroovyFile.setValueStatic(subclosure, property, value, false, null);
} else if (GradleGroovyFile.getValueStatic(subclosure, property) != GradleBuildFile.UNRECOGNIZED_VALUE) {
GradleGroovyFile.removeValueStatic(subclosure, property);
}
}
GradleGroovyFile.reformatClosure(subclosure);
}
@Nullable
@Override
public List<NamedObject> getValues(@NotNull PsiElement statement) {
if (!(statement instanceof GrMethodCall)) {
return null;
}
GrMethodCall method = (GrMethodCall)statement;
NamedObject item = new NamedObject(GradleGroovyFile.getMethodCallName((GrMethodCall)statement));
GrClosableBlock subclosure = GradleGroovyFile.getMethodClosureArgument(method);
if (subclosure == null) {
return null;
}
for (BuildFileKey property : myProperties) {
Object value = GradleGroovyFile.getValueStatic(subclosure, property);
if (value != null) {
item.setValue(property, value);
}
}
return ImmutableList.of(item);
}
@Override
protected void removeValue(@NotNull GrStatementOwner closure, @NotNull NamedObject value) {
GrMethodCall call = GradleGroovyFile.getMethodCall(closure, value.getName());
if (call != null) {
call.removeStatement();
}
}
@NotNull
public List<BuildFileKey> getProperties() {
return myProperties;
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof NamedObject)) return false;
NamedObject that = (NamedObject)o;
if (!myName.equals(that.myName)) return false;
if (!myValues.equals(that.myValues)) return false;
return true;
}
@Override
public int hashCode() {
int result = myName.hashCode();
result = 31 * result + myValues.hashCode();
return result;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("NamedObject ");
sb.append(myName);
sb.append(' ');
sb.append(myValues.toString());
return sb.toString();
}
}