Description.java
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.rat.config.parameters;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Map;
import java.util.TreeMap;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.rat.BuilderParams;
import org.apache.rat.ConfigurationException;
import org.apache.rat.analysis.IHeaderMatcher;
import org.apache.rat.utils.DefaultLog;
import static java.lang.String.format;
/**
* A description of a component.
*/
public class Description {
/** The type of component this describes */
private final ComponentType type;
/**
* The common name for the component. Set by ConfigComponent.name() or
* class/field name.
*/
private final String name;
/** The description for the component */
private final String desc;
/** The class of the getter/setter parameter */
private final Class<?> childClass;
/** True if the getter/setter expects a collection of childClass objects */
private final boolean isCollection;
/** True if this component is required. */
private final boolean required;
/**
* A map of name to Description for all the components that are children of the
* described component.
*/
private final Map<String, Description> children;
/**
* Constructor.
* @param type the type of the component.
* @param name the name of the component.
* @param desc the description of the component.
* @param isCollection true if the getter/setter expects a collection
* @param childClass the class for expected for the getter/setter.
* @param children the collection of descriptions for all the components that
* are children of the described component.
* @param required If {@code true} the component is required.
*/
public Description(final ComponentType type, final String name, final String desc, final boolean isCollection,
final Class<?> childClass, final Collection<Description> children, final boolean required) {
this.type = type;
this.name = name;
this.desc = desc;
this.isCollection = isCollection;
this.required = required;
if (type == ComponentType.BUILD_PARAMETER) {
Method m;
try {
m = BuilderParams.class.getMethod(name);
} catch (NoSuchMethodException | SecurityException e) {
throw new ConfigurationException(format("'%s' is not a valid BuildParams method", name));
}
this.childClass = m.getReturnType();
} else {
this.childClass = childClass;
}
this.children = new TreeMap<>();
if (children != null) {
children.forEach(d -> this.children.put(d.name, d));
}
}
/**
* Constructor
* @param configComponent the configuration component.
* @param isCollection the collection flag.
* @param childClass the type of object that the method getter/setter expects.
* @param children the collection of descriptions for all the components that
* are children the described component.
*/
public Description(final ConfigComponent configComponent, final boolean isCollection, final Class<?> childClass,
final Collection<Description> children) {
this(configComponent.type(), configComponent.name(), configComponent.desc(), isCollection, childClass, children,
configComponent.required());
}
/**
* Get the canBeChild flag.
* @return {@code true} if this item can be a child of the containing item.
*/
public boolean isRequired() {
return required;
}
/**
* Gets the type of the component.
* @return the component type.
*/
public ComponentType getType() {
return type;
}
/**
* Get the isCollection flag.
* @return true if this is a collection.
*/
public boolean isCollection() {
return isCollection;
}
/**
* Get the class of the objects for the getter/setter methods.
* @return the getter/setter param class.
*/
public Class<?> getChildType() {
return childClass;
}
/**
* Gets the common name for the matcher. (e.g. 'text', 'spdx', etc.) May not be
* null.
* @return The common name for the item being inspected.
*/
public String getCommonName() {
return name;
}
/**
* Gets the description of descriptive text for the component. May be an empty
* string or null.
* @return the descriptive text;
*/
public String getDescription() {
return desc;
}
/**
* Retrieve the value of the described parameter from the specified object.
* If the parameter is a collection return {@code null}.
* @param object the object that contains the value.
* @return the string value.
*/
public String getParamValue(final Object object) {
if (isCollection) {
return null;
}
try {
Object val = getter(object.getClass()).invoke(object);
return val == null ? null : val.toString();
} catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException | NoSuchMethodException
| SecurityException e) {
DefaultLog.getInstance().error(format("Can not retrieve value for '%s' from %s%n", name, object.getClass().getName()),
e);
return null;
}
}
/**
* Gets a map of the parameters that the object contains. For example Copyright
* has 'start', 'stop', and 'owner' parameters. Some IHeaderMatchers have simple
* text values (e.g. 'regex' or 'text' types) these should list an unnamed
* parameter (empty string) with the text value.
* @return the map of parameters to the objects that represent them.
*/
public Map<String, Description> getChildren() {
return children;
}
/**
* Get all the children of a specific type
* @param type the type to return
* @return the collection of children of the specified type.
*/
public Collection<Description> childrenOfType(final ComponentType type) {
return filterChildren(d -> d.getType() == type);
}
/**
* Gets a filtered collection of the child descriptions.
* @param filter the filter to apply to the child descriptions.
* @return the collection of children that matches the filter.
*/
public Collection<Description> filterChildren(final Predicate<Description> filter) {
return children.values().stream().filter(filter).collect(Collectors.toList());
}
/**
* Generate a method name for this description.
* @param prefix the start of the method name (e.g. "set", "get" )
* @return the method name.
*/
public String methodName(final String prefix) {
return prefix + StringUtils.capitalize(name);
}
/**
* Returns the getter for the component in the specified class.
* @param clazz the Class to get the getter from.
* @return the getter Method.
* @throws NoSuchMethodException if the class does not have the getter.
* @throws SecurityException if the getter can not be accessed.
*/
public Method getter(final Class<?> clazz) throws NoSuchMethodException, SecurityException {
return clazz.getMethod(methodName("get"));
}
/**
* Returns the setter for the component in the specified class. Notes:
* <ul>
* <li>License can not be set in components. They are top level components.</li>
* <li>Matcher expects an "add" method that accepts an
* IHeaderMatcher.Builder.</li>
* <li>Parameter expects a {@code set(String)} method.</li>
* <li>Unlabeled expects a {@code set(String)} method.</li>
* <li>BuilderParam expects a {@code set} method that takes a
* {@code childClass} argument.</li>
* </ul>
* @param clazz the Class to get the getter from, generally a Builder class.
* @return the getter Method.
* @throws NoSuchMethodException if the class does not have the getter.
* @throws SecurityException if the getter can not be accessed.
*/
public Method setter(final Class<?> clazz) throws NoSuchMethodException, SecurityException {
String methodName = methodName(isCollection ? "add" : "set");
return switch (type) {
case LICENSE -> throw new NoSuchMethodException("Can not set a License as a child");
case MATCHER -> clazz.getMethod(methodName, IHeaderMatcher.Builder.class);
case PARAMETER -> clazz.getMethod(methodName,
IHeaderMatcher.class.isAssignableFrom(childClass) ? IHeaderMatcher.Builder.class : childClass);
case BUILD_PARAMETER -> clazz.getMethod(methodName, childClass);
};
// should not happen
}
private void callSetter(final Description description, final IHeaderMatcher.Builder builder, final String value) {
try {
description.setter(builder.getClass()).invoke(builder, value);
} catch (NoSuchMethodException e) {
String msg = format("No setter for '%s' on %s", description.getCommonName(),
builder.getClass().getCanonicalName());
DefaultLog.getInstance().error(msg);
throw new ConfigurationException(msg);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | SecurityException e) {
String msg = format("Unable to call setter for '%s' on %s", description.getCommonName(),
builder.getClass().getCanonicalName());
DefaultLog.getInstance().error(msg, e);
throw new ConfigurationException(msg, e);
}
}
/**
* Sets the children of values in the builder. Sets the parameters to the values
* specified in the map. Only children that accept string arguments should be
* specified.
* @param builder The Matcher builder to set the values in.
* @param attributes a Map of parameter names to values.
*/
public void setChildren(final IHeaderMatcher.Builder builder, final Map<String, String> attributes) {
attributes.forEach((key, value) -> setChild(builder, key, value));
}
/**
* Sets the child value in the builder.
* @param builder The Matcher builder to set the values in.
* @param name the name of the child to set
* @param value the value of the parameter.
*/
public void setChild(final IHeaderMatcher.Builder builder, final String name, final String value) {
Description d = getChildren().get(name);
if (d == null) {
DefaultLog.getInstance().error(format("%s does not define a ConfigComponent for a member %s.",
builder.getClass().getCanonicalName(), name));
} else {
callSetter(d, builder, value);
}
}
@Override
public String toString() {
String childList = children.isEmpty() ? ""
: children.values().stream().map(Description::getCommonName).collect(Collectors.joining(", "));
return format("Description[%s t:%s c:%s %s children: [%s]] ", name, type, isCollection, childClass,
childList);
}
}