/* * 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.coercer; import com.facebook.buck.io.ProjectFilesystem; import com.facebook.buck.model.BuildTarget; import com.facebook.buck.rules.CellPathResolver; import com.facebook.buck.rules.Hint; import com.facebook.buck.util.Types; import com.google.common.base.CaseFormat; import com.google.common.base.Preconditions; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.nio.file.Path; import java.util.Collection; import java.util.Map; import javax.annotation.Nullable; /** Represents a single field that can be represented in buck build files. */ public class ParamInfo implements Comparable<ParamInfo> { private final TypeCoercer<?> typeCoercer; private final String name; private final Method setter; /** * Holds the closest getter for this property defined on the abstract class or interface. * * <p>Note that this may not be abstract, for instance if a @Value.Default is specified. */ private final Supplier<Method> closestGetterOnAbstractClassOrInterface; /** Holds the getter for the concrete Immutable class. */ private final Supplier<Method> concreteGetter; private final Supplier<Boolean> isOptional; @SuppressWarnings("PMD.EmptyCatchBlock") public ParamInfo(TypeCoercerFactory typeCoercerFactory, Method setter) { Preconditions.checkArgument( setter.getParameterCount() == 1, "Setter is expected to have exactly one parameter but had %s", setter.getParameterCount()); Preconditions.checkArgument( setter.getName().startsWith("set"), "Setter is expected to have name starting with 'set' but was %s", setter.getName()); Preconditions.checkArgument( setter.getName().length() > 3, "Setter must have name longer than just 'set' but was %s", setter.getName()); this.setter = setter; this.closestGetterOnAbstractClassOrInterface = Suppliers.memoize(this::findClosestGetterOnAbstractClassOrInterface); this.concreteGetter = Suppliers.memoize( () -> { // This needs to get (and invoke) the concrete Immutable class's getter, not the abstract // getter from a superclass. // Accordingly, we manually find the getter there, rather than using // closestGetterOnAbstractClassOrInterface. Class<?> enclosingClass = setter.getDeclaringClass().getEnclosingClass(); if (enclosingClass == null) { throw new IllegalStateException( String.format( "Couldn't find enclosing class of Builder %s", setter.getDeclaringClass())); } Iterable<String> getterNames = getGetterNames(); for (String possibleGetterName : getterNames) { try { return enclosingClass.getMethod(possibleGetterName); } catch (NoSuchMethodException e) { // Handled below } } throw new IllegalStateException( String.format( "Couldn't find declared getter for %s#%s. Tried enclosing class %s methods: %s", setter.getDeclaringClass(), setter.getName(), enclosingClass, getterNames)); }); this.isOptional = Suppliers.memoize( () -> { Method getter = closestGetterOnAbstractClassOrInterface.get(); Class<?> type = getter.getReturnType(); if (CoercedTypeCache.OPTIONAL_TYPES.contains(type)) { return true; } if (Collection.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type)) { return true; } // Unfortunately @Value.Default isn't retained at runtime, so we use abstract-ness // as a proxy for whether something has a default value. if (!Modifier.isAbstract(getter.getModifiers())) { return true; } return false; }); StringBuilder builder = new StringBuilder(); builder.append(setter.getName().substring(3, 4).toLowerCase()); if (setter.getName().length() > 4) { builder.append(setter.getName().substring(4)); } this.name = builder.toString(); this.typeCoercer = typeCoercerFactory.typeCoercerForType(setter.getGenericParameterTypes()[0]); } public String getName() { return name; } public boolean isOptional() { return this.isOptional.get(); } public String getPythonName() { return CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, getName()); } public boolean isDep() { Hint hint = getHint(); if (hint != null) { return hint.isDep(); } return Hint.DEFAULT_IS_DEP; } public boolean isInput() { Hint hint = getHint(); if (hint != null) { return hint.isInput(); } return Hint.DEFAULT_IS_INPUT; } private Hint getHint() { return this.closestGetterOnAbstractClassOrInterface.get().getAnnotation(Hint.class); } /** * Returns the type that input values will be coerced to. Return the type parameter of Optional if * wrapped in Optional. */ public Class<?> getResultClass() { return typeCoercer.getOutputClass(); } /** * Traverse the value of the field on {@code dto} that is represented by this instance. * * <p>If this field has a top level Optional type, traversal begins at the Optional value, or not * at all if the field is empty. * * @param traversal traversal to apply on the values. * @param dto the object whose field will be traversed. * @see TypeCoercer#traverse(Object, TypeCoercer.Traversal) */ public void traverse(Traversal traversal, Object dto) { traverseHelper(typeCoercer, traversal, dto); } @SuppressWarnings("unchecked") private <U> void traverseHelper(TypeCoercer<U> typeCoercer, Traversal traversal, Object dto) { U object = (U) get(dto); if (object != null) { typeCoercer.traverse(object, traversal); } } /** Get the value of this param as set on dto. */ public Object get(Object dto) { Method getter = this.concreteGetter.get(); try { return getter.invoke(dto); } catch (InvocationTargetException | IllegalAccessException e) { throw new IllegalStateException( String.format( "Error invoking getter %s on class %s", getter.getName(), getter.getDeclaringClass()), e); } } public boolean hasElementTypes(final Class<?>... types) { return typeCoercer.hasElementClass(types); } public void setFromParams( CellPathResolver cellRoots, ProjectFilesystem filesystem, BuildTarget buildTarget, Object arg, Map<String, ?> instance) throws ParamInfoException { set(cellRoots, filesystem, buildTarget.getBasePath(), arg, instance.get(name)); } /** * Sets a single property of the {@code dto}, coercing types as necessary. * * @param cellRoots * @param filesystem {@link ProjectFilesystem} used to ensure {@link Path}s exist. * @param pathRelativeToProjectRoot The path relative to the project root that this DTO is for. * @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( CellPathResolver cellRoots, ProjectFilesystem filesystem, Path pathRelativeToProjectRoot, Object dto, @Nullable Object value) throws ParamInfoException { if (value == null) { return; } try { setCoercedValue( dto, typeCoercer.coerce(cellRoots, filesystem, pathRelativeToProjectRoot, value)); } catch (CoerceFailedException e) { throw new ParamInfoException(name, e.getMessage(), e); } } /** * Set the param on dto to value, assuming value has already been coerced. * * <p>This is useful for things like making copies of dtos. */ public void setCoercedValue(Object dto, Object value) { try { setter.invoke(dto, value); } catch (IllegalAccessException | InvocationTargetException e) { throw new RuntimeException(e); } } /** Returns the most-overridden getter on the abstract Immutable. */ @SuppressWarnings("PMD.EmptyCatchBlock") private Method findClosestGetterOnAbstractClassOrInterface() { Iterable<Class<?>> superClasses = Iterables.skip(Types.getSupertypes(setter.getDeclaringClass().getEnclosingClass()), 1); ImmutableList<String> getterNames = getGetterNames(); for (Class<?> clazz : superClasses) { for (String getterName : getterNames) { try { return clazz.getDeclaredMethod(getterName); } catch (NoSuchMethodException e) { // Handled below } } } throw new IllegalStateException( String.format( "Couldn't find declared getter for %s#%s. Tried parent classes %s methods: %s", setter.getDeclaringClass(), setter.getName(), superClasses, getterNames)); } private ImmutableList<String> getGetterNames() { String suffix = setter.getName().substring(3); return ImmutableList.of("get" + suffix, "is" + suffix); } /** Only valid when comparing {@link ParamInfo} instances from the same description. */ @Override public int compareTo(ParamInfo that) { if (this == that) { return 0; } 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()); } public interface Traversal extends TypeCoercer.Traversal {} }