| /* |
| * Copyright 2013-present Facebook, Inc. |
| * |
| * 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.facebook.buck.rules; |
| |
| import static com.facebook.buck.model.BuildTarget.BUILD_TARGET_PREFIX; |
| |
| import com.facebook.buck.model.BuildTarget; |
| import com.google.common.base.Optional; |
| import com.google.common.base.Preconditions; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.ImmutableSortedSet; |
| import com.google.common.collect.Lists; |
| import com.google.common.primitives.Primitives; |
| |
| import java.lang.reflect.Field; |
| import java.lang.reflect.ParameterizedType; |
| import java.lang.reflect.Type; |
| import java.lang.reflect.WildcardType; |
| import java.nio.file.Path; |
| import java.util.Collection; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.SortedSet; |
| |
| import javax.annotation.Nullable; |
| |
| /** |
| * Parameter information derived from the object returned by |
| * {@link Description#getConstructorArg()}. |
| */ |
| // DO NOT EXPOSE OUT OF PACKAGE. |
| class ParamInfo implements Comparable<ParamInfo> { |
| private final String name; |
| |
| /** |
| * Is the property optional (indicated by being Optional or @Hinted with a default value. |
| */ |
| private final boolean isOptional; |
| |
| /** |
| * If the value is a List or Set, which of the two is it. |
| */ |
| @Nullable |
| private final Class<?> containerType; |
| |
| /** |
| * The type of the field, or the contained type of an Optional or a Collection. |
| */ |
| private final Class<?> type; |
| private final Path pathRelativeToProjectRoot; |
| |
| private Field field; |
| |
| /** |
| * @param pathRelativeToProjectRoot The path to the directory containing the build file this param |
| * is for. |
| * @param field The field in the constructor arg that this represents. |
| */ |
| public ParamInfo(Path pathRelativeToProjectRoot, Field field) { |
| this.pathRelativeToProjectRoot = Preconditions.checkNotNull(pathRelativeToProjectRoot); |
| |
| this.field = Preconditions.checkNotNull(field); |
| Hint hint = field.getAnnotation(Hint.class); |
| this.name = determineName(field.getName(), hint); |
| |
| Class<?> rawType = field.getType(); |
| Type genericType = field.getGenericType(); |
| |
| this.isOptional = Optional.class.isAssignableFrom(rawType); |
| |
| // TODO(simons): Check for a wildcard here. |
| if (List.class.isAssignableFrom(rawType) || Set.class.isAssignableFrom(rawType)) { |
| this.containerType = rawType; |
| this.type = determineGenericType(genericType); |
| } else if (Optional.class.isAssignableFrom(rawType)) { |
| this.type = unwrapGenericType(genericType); |
| |
| Class<?> container = null; |
| |
| if (genericType instanceof ParameterizedType) { |
| Type containerType = ((ParameterizedType) genericType).getActualTypeArguments()[0]; |
| if (containerType instanceof ParameterizedType) { |
| Type rawContainerType = ((ParameterizedType) containerType).getRawType(); |
| if (!(rawContainerType instanceof Class)) { |
| throw new RuntimeException("Container type isn't a class: " + rawContainerType); |
| } |
| container = (Class<?>) rawContainerType; |
| if (!Collection.class.isAssignableFrom(container)) { |
| throw new RuntimeException("Cannot determine container type: " + container); |
| } |
| } |
| } |
| containerType = container; |
| } else { |
| this.containerType = null; |
| this.type = Primitives.wrap(rawType); |
| } |
| } |
| |
| String getName() { |
| return name; |
| } |
| |
| boolean isOptional() { |
| return isOptional; |
| } |
| |
| @Nullable |
| Class<?> getContainerType() { |
| return containerType; |
| } |
| |
| Class<?> getType() { |
| return type; |
| } |
| |
| private Class<?> unwrapGenericType(Type type) { |
| if (!(type instanceof ParameterizedType)) { |
| return (Class<?>) type; |
| } |
| |
| Type[] types = ((ParameterizedType) type).getActualTypeArguments(); |
| if (types.length != 1) { |
| throw new IllegalArgumentException("Unable to determine generic type"); |
| } |
| |
| if (types[0] instanceof WildcardType) { |
| throw new IllegalStateException("Generic types must be specific: " + type); |
| } |
| |
| if (types[0] instanceof ParameterizedType) { |
| return unwrapGenericType(types[0]); |
| } |
| return (Class<?>) types[0]; |
| } |
| |
| private String determineName(String name, @Nullable Hint hint) { |
| return hint == null ? name : hint.name(); |
| } |
| |
| private Class<?> determineGenericType(Type genericType) { |
| if (!(genericType instanceof ParameterizedType)) { |
| throw new IllegalArgumentException("Collection type was not generic"); |
| } |
| |
| Type[] types = ((ParameterizedType) genericType).getActualTypeArguments(); |
| if (types.length != 1) { |
| throw new IllegalArgumentException("Unable to determine generic type"); |
| } |
| |
| if (types[0] instanceof WildcardType) { |
| WildcardType wild = (WildcardType) types[0]; |
| |
| if (Object.class.equals(wild.getUpperBounds()[0])) { |
| throw new IllegalArgumentException("Generic types must be specific: " + genericType); |
| } |
| return Primitives.wrap((Class<?>) wild.getUpperBounds()[0]); |
| } |
| |
| return Primitives.wrap((Class<?>) types[0]); |
| } |
| |
| public void setFromParams( |
| BuildRuleResolver ruleResolver, |
| Object dto, |
| BuildRuleFactoryParams params) { |
| if (containerType != null) { |
| set(ruleResolver, dto, params.getOptionalListAttribute(name)); |
| } else if (Path.class.isAssignableFrom(type) || |
| SourcePath.class.isAssignableFrom(type) || |
| String.class.equals(type)) { |
| set(ruleResolver, dto, params.getOptionalStringAttribute(name).orNull()); |
| } else if (Number.class.isAssignableFrom(type)) { |
| set(ruleResolver, dto, params.getRequiredLongAttribute(name)); |
| } else if (BuildTarget.class.isAssignableFrom(type)) { |
| Optional<BuildTarget> optionalTarget = params.getOptionalBuildTarget(name); |
| set(ruleResolver, dto, optionalTarget.orNull()); |
| } else if (BuildRule.class.isAssignableFrom(type)) { |
| Optional<BuildTarget> optionalTarget = params.getOptionalBuildTarget(name); |
| set(ruleResolver, dto, optionalTarget.orNull()); |
| } else if (Boolean.class.equals(type)) { |
| set(ruleResolver, dto, params.getBooleanAttribute(name)); |
| } else { |
| throw new RuntimeException("Unknown type: " + type); |
| } |
| } |
| |
| /** |
| * Sets a single property of the {@code dto}, coercing types as necessary. |
| * |
| * @param resolver {@link com.facebook.buck.rules.BuildRuleResolver} used for {@link com.facebook.buck.rules.BuildRule} instances. |
| * @param dto The constructor DTO on which the value should be set. |
| * @param value The value, which may be coerced depending on the type on {@code dto}. |
| */ |
| public void set(BuildRuleResolver resolver, Object dto, @Nullable Object value) { |
| if (value == null) { |
| if (!isOptional) { |
| throw new IllegalArgumentException(String.format( |
| "%s cannot be null. Build file can be found in %s.", |
| dto, pathRelativeToProjectRoot)); |
| } |
| |
| value = Optional.absent(); |
| } else if (isOptional && matchesDefaultValue(value)) { |
| if (value instanceof Collection) { |
| value = asCollection(resolver, Lists.newArrayList()); |
| value = Optional.of(value); |
| } else { |
| value = Optional.absent(); |
| } |
| } else if (containerType == null) { |
| value = coerceToExpectedType(resolver, value); |
| if (isOptional) { |
| value = Optional.of(value); |
| } |
| } else { |
| value = asCollection(resolver, value); |
| if (isOptional) { |
| value = Optional.of(value); |
| } |
| } |
| |
| try { |
| field.set(dto, value); |
| } catch (ReflectiveOperationException e) { |
| throw new RuntimeException(e); |
| } catch (ClassCastException | IllegalArgumentException | NullPointerException e) { |
| throw new IllegalArgumentException(String.format( |
| "Unable to convert '%s' to %s in build file in %s", |
| value, type, pathRelativeToProjectRoot)); |
| } |
| } |
| |
| private boolean matchesDefaultValue(Object value) { |
| if (value instanceof String && "".equals(value)) { |
| return true; |
| } |
| |
| if (value instanceof Number && ((Number)value).intValue() == 0) { |
| return true; |
| } |
| |
| if (Boolean.FALSE.equals(value)) { |
| return true; |
| } |
| |
| if (value instanceof List && ((List<?>) value).isEmpty()) { |
| return true; |
| } |
| |
| return false; |
| } |
| |
| private Object coerceToExpectedType(BuildRuleResolver ruleResolver, Object value) { |
| if (BuildRule.class.isAssignableFrom(type)) { |
| BuildTarget target = asBuildTarget(value); |
| return ruleResolver.get(target); |
| } |
| |
| if (BuildTarget.class.isAssignableFrom(type)) { |
| return asBuildTarget(value); |
| } |
| |
| // All paths should be relative to the base path. |
| if (Path.class.isAssignableFrom(type)) { |
| return asNormalizedPath(value); |
| } |
| |
| if (SourcePath.class.isAssignableFrom(type)) { |
| BuildTarget target = asBuildTarget(value); |
| if (target != null) { |
| return new BuildTargetSourcePath(target); |
| } |
| Path path = asNormalizedPath(value); |
| return new FileSourcePath(pathRelativeToProjectRoot.relativize(path).toString()); |
| } |
| |
| if (value instanceof Number) { |
| Number num = (Number) value; |
| if (Double.class.equals(type)) { |
| return num.doubleValue(); |
| } else if (Integer.class.equals(type)) { |
| return num.intValue(); |
| } else if (Float.class.equals(type)) { |
| return num.floatValue(); |
| } else if (Long.class.equals(type)) { |
| return num.longValue(); // not strictly necessary, but included for completeness. |
| } else if (Short.class.equals(type)) { |
| return num.shortValue(); |
| } |
| } |
| |
| // We're going to cheat and let the JVM take the strain of converting between primitive and |
| // object wrapper types, but it has a habit of coercing to a String that should be avoided. |
| if (String.class.equals(type) && !String.class.equals(value.getClass())) { |
| throw new IllegalArgumentException( |
| String.format("Unable to convert '%s' to %s", value, type)); |
| } |
| |
| return value; |
| } |
| |
| @Nullable |
| private BuildTarget asBuildTarget(Object value) { |
| if (value instanceof BuildTarget) { |
| return (BuildTarget) value; |
| } |
| |
| Preconditions.checkArgument(value instanceof String, |
| "Expected argument '%s' to be a build target", value); |
| |
| String param = (String) value; |
| int colon = param.indexOf(':'); |
| if (colon == 0 && param.length() > 1) { |
| return new BuildTarget( |
| BUILD_TARGET_PREFIX + pathRelativeToProjectRoot.toString(), |
| param.substring(1)); |
| } else if (colon > 0 && param.length() > 2) { |
| return new BuildTarget(param.substring(0, colon), param.substring(colon + 1)); |
| } |
| return null; |
| } |
| |
| private Path asNormalizedPath(Object value) { |
| Preconditions.checkArgument(value instanceof String, |
| "Expected argument '%s' to be a string in build file in %s", |
| value, pathRelativeToProjectRoot); |
| |
| return pathRelativeToProjectRoot.resolve((String) value).normalize(); |
| } |
| |
| @SuppressWarnings("unchecked") |
| private Object asCollection(BuildRuleResolver ruleResolver, Object value) { |
| if (!(value instanceof Collection)) { |
| throw new IllegalArgumentException(String.format( |
| "May not set '%s' on a collection type in build file in %s", |
| value, pathRelativeToProjectRoot)); |
| } |
| |
| List<Object> collection = Lists.newArrayList(); |
| |
| for (Object obj : (Iterable<Object>) value) { |
| collection.add(coerceToExpectedType(ruleResolver, obj)); |
| } |
| |
| if (SortedSet.class.isAssignableFrom(containerType)) { |
| return ImmutableSortedSet.copyOf(collection); |
| } else if (Set.class.isAssignableFrom(containerType)) { |
| return ImmutableSet.copyOf(collection); |
| } else { |
| return ImmutableList.copyOf(collection); |
| } |
| } |
| |
| /** |
| * Only valid when comparing {@link ParamInfo} instances from the same description. |
| */ |
| @Override |
| public int compareTo(ParamInfo that) { |
| return this.name.compareTo(that.name); |
| } |
| |
| @Override |
| public int hashCode() { |
| return name.hashCode(); |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (!(obj instanceof ParamInfo)) { |
| return false; |
| } |
| |
| ParamInfo that = (ParamInfo) obj; |
| return name.equals(that.getName()); |
| } |
| } |