1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.apache.rat.config.parameters;
20
21 import java.lang.reflect.InvocationTargetException;
22 import java.lang.reflect.Method;
23 import java.util.Collection;
24 import java.util.Map;
25 import java.util.TreeMap;
26 import java.util.function.Predicate;
27 import java.util.stream.Collectors;
28
29 import org.apache.commons.lang3.StringUtils;
30 import org.apache.rat.BuilderParams;
31 import org.apache.rat.ConfigurationException;
32 import org.apache.rat.analysis.IHeaderMatcher;
33 import org.apache.rat.utils.DefaultLog;
34
35 import static java.lang.String.format;
36
37 /**
38 * A description of a component.
39 */
40 public class Description {
41 /** The type of component this describes */
42 private final ComponentType type;
43 /**
44 * The common name for the component. Set by ConfigComponent.name() or
45 * class/field name.
46 */
47 private final String name;
48 /** The description for the component */
49 private final String desc;
50 /** The class of the getter/setter parameter */
51 private final Class<?> childClass;
52 /** True if the getter/setter expects a collection of childClass objects */
53 private final boolean isCollection;
54 /** True if this component is required. */
55 private final boolean required;
56 /**
57 * A map of name to Description for all the components that are children of the
58 * described component.
59 */
60 private final Map<String, Description> children;
61
62 /**
63 * Constructor.
64 * @param type the type of the component.
65 * @param name the name of the component.
66 * @param desc the description of the component.
67 * @param isCollection true if the getter/setter expects a collection
68 * @param childClass the class for expected for the getter/setter.
69 * @param children the collection of descriptions for all the components that
70 * are children of the described component.
71 * @param required If {@code true} the component is required.
72 */
73 public Description(final ComponentType type, final String name, final String desc, final boolean isCollection,
74 final Class<?> childClass, final Collection<Description> children, final boolean required) {
75 this.type = type;
76 this.name = name;
77 this.desc = desc;
78 this.isCollection = isCollection;
79 this.required = required;
80 if (type == ComponentType.BUILD_PARAMETER) {
81 Method m;
82 try {
83 m = BuilderParams.class.getMethod(name);
84 } catch (NoSuchMethodException | SecurityException e) {
85 throw new ConfigurationException(format("'%s' is not a valid BuildParams method", name));
86 }
87 this.childClass = m.getReturnType();
88 } else {
89 this.childClass = childClass;
90 }
91 this.children = new TreeMap<>();
92 if (children != null) {
93 children.forEach(d -> this.children.put(d.name, d));
94 }
95 }
96
97 /**
98 * Constructor
99 * @param configComponent the configuration component.
100 * @param isCollection the collection flag.
101 * @param childClass the type of object that the method getter/setter expects.
102 * @param children the collection of descriptions for all the components that
103 * are children the described component.
104 */
105 public Description(final ConfigComponent configComponent, final boolean isCollection, final Class<?> childClass,
106 final Collection<Description> children) {
107 this(configComponent.type(), configComponent.name(), configComponent.desc(), isCollection, childClass, children,
108 configComponent.required());
109 }
110
111 /**
112 * Get the canBeChild flag.
113 * @return {@code true} if this item can be a child of the containing item.
114 */
115 public boolean isRequired() {
116 return required;
117 }
118
119 /**
120 * Gets the type of the component.
121 * @return the component type.
122 */
123 public ComponentType getType() {
124 return type;
125 }
126
127 /**
128 * Get the isCollection flag.
129 * @return true if this is a collection.
130 */
131 public boolean isCollection() {
132 return isCollection;
133 }
134
135 /**
136 * Get the class of the objects for the getter/setter methods.
137 * @return the getter/setter param class.
138 */
139 public Class<?> getChildType() {
140 return childClass;
141 }
142
143 /**
144 * Gets the common name for the matcher. (e.g. 'text', 'spdx', etc.) May not be
145 * null.
146 * @return The common name for the item being inspected.
147 */
148 public String getCommonName() {
149 return name;
150 }
151
152 /**
153 * Gets the description of descriptive text for the component. May be an empty
154 * string or null.
155 * @return the descriptive text;
156 */
157 public String getDescription() {
158 return desc;
159 }
160
161 /**
162 * Retrieve the value of the described parameter from the specified object.
163 * If the parameter is a collection return {@code null}.
164 * @param object the object that contains the value.
165 * @return the string value.
166 */
167 public String getParamValue(final Object object) {
168 if (isCollection) {
169 return null;
170 }
171 try {
172 Object val = getter(object.getClass()).invoke(object);
173 return val == null ? null : val.toString();
174 } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException | NoSuchMethodException
175 | SecurityException e) {
176 DefaultLog.getInstance().error(format("Can not retrieve value for '%s' from %s%n", name, object.getClass().getName()),
177 e);
178 return null;
179 }
180 }
181
182 /**
183 * Gets a map of the parameters that the object contains. For example Copyright
184 * has 'start', 'stop', and 'owner' parameters. Some IHeaderMatchers have simple
185 * text values (e.g. 'regex' or 'text' types) these should list an unnamed
186 * parameter (empty string) with the text value.
187 * @return the map of parameters to the objects that represent them.
188 */
189 public Map<String, Description> getChildren() {
190 return children;
191 }
192
193 /**
194 * Get all the children of a specific type
195 * @param type the type to return
196 * @return the collection of children of the specified type.
197 */
198 public Collection<Description> childrenOfType(final ComponentType type) {
199 return filterChildren(d -> d.getType() == type);
200 }
201
202 /**
203 * Gets a filtered collection of the child descriptions.
204 * @param filter the filter to apply to the child descriptions.
205 * @return the collection of children that matches the filter.
206 */
207 public Collection<Description> filterChildren(final Predicate<Description> filter) {
208 return children.values().stream().filter(filter).collect(Collectors.toList());
209 }
210
211 /**
212 * Generate a method name for this description.
213 * @param prefix the start of the method name (e.g. "set", "get" )
214 * @return the method name.
215 */
216 public String methodName(final String prefix) {
217 return prefix + StringUtils.capitalize(name);
218 }
219
220 /**
221 * Returns the getter for the component in the specified class.
222 * @param clazz the Class to get the getter from.
223 * @return the getter Method.
224 * @throws NoSuchMethodException if the class does not have the getter.
225 * @throws SecurityException if the getter can not be accessed.
226 */
227 public Method getter(final Class<?> clazz) throws NoSuchMethodException, SecurityException {
228 return clazz.getMethod(methodName("get"));
229 }
230
231 /**
232 * Returns the setter for the component in the specified class. Notes:
233 * <ul>
234 * <li>License can not be set in components. They are top level components.</li>
235 * <li>Matcher expects an "add" method that accepts an
236 * IHeaderMatcher.Builder.</li>
237 * <li>Parameter expects a {@code set(String)} method.</li>
238 * <li>Unlabeled expects a {@code set(String)} method.</li>
239 * <li>BuilderParam expects a {@code set} method that takes a
240 * {@code childClass} argument.</li>
241 * </ul>
242 * @param clazz the Class to get the getter from, generally a Builder class.
243 * @return the getter Method.
244 * @throws NoSuchMethodException if the class does not have the getter.
245 * @throws SecurityException if the getter can not be accessed.
246 */
247 public Method setter(final Class<?> clazz) throws NoSuchMethodException, SecurityException {
248 String methodName = methodName(isCollection ? "add" : "set");
249 switch (type) {
250 case LICENSE:
251 throw new NoSuchMethodException("Can not set a License as a child");
252 case MATCHER:
253 return clazz.getMethod(methodName, IHeaderMatcher.Builder.class);
254 case PARAMETER:
255 return clazz.getMethod(methodName,
256 IHeaderMatcher.class.isAssignableFrom(childClass) ? IHeaderMatcher.Builder.class : childClass);
257 case BUILD_PARAMETER:
258 return clazz.getMethod(methodName, childClass);
259 }
260 // should not happen
261 throw new IllegalStateException("Type " + type + " not valid.");
262 }
263
264 private void callSetter(final Description description, final IHeaderMatcher.Builder builder, final String value) {
265 try {
266 description.setter(builder.getClass()).invoke(builder, value);
267 } catch (NoSuchMethodException e) {
268 String msg = format("No setter for '%s' on %s", description.getCommonName(),
269 builder.getClass().getCanonicalName());
270 DefaultLog.getInstance().error(msg);
271 throw new ConfigurationException(msg);
272 } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | SecurityException e) {
273 String msg = format("Unable to call setter for '%s' on %s", description.getCommonName(),
274 builder.getClass().getCanonicalName());
275 DefaultLog.getInstance().error(msg, e);
276 throw new ConfigurationException(msg, e);
277 }
278 }
279
280 /**
281 * Sets the children of values in the builder. Sets the parameters to the values
282 * specified in the map. Only children that accept string arguments should be
283 * specified.
284 * @param builder The Matcher builder to set the values in.
285 * @param attributes a Map of parameter names to values.
286 */
287 public void setChildren(final IHeaderMatcher.Builder builder, final Map<String, String> attributes) {
288 attributes.forEach((key, value) -> setChild(builder, key, value));
289 }
290
291 /**
292 * Sets the child value in the builder.
293 * @param builder The Matcher builder to set the values in.
294 * @param name the name of the child to set
295 * @param value the value of the parameter.
296 */
297 public void setChild(final IHeaderMatcher.Builder builder, final String name, final String value) {
298 Description d = getChildren().get(name);
299 if (d == null) {
300 DefaultLog.getInstance().error(format("%s does not define a ConfigComponent for a member %s.",
301 builder.getClass().getCanonicalName(), name));
302 } else {
303 callSetter(d, builder, value);
304 }
305 }
306
307 @Override
308 public String toString() {
309 String childList = children.isEmpty() ? ""
310 : children.values().stream().map(Description::getCommonName).collect(Collectors.joining(", "));
311
312 return format("Description[%s t:%s c:%s %s children: [%s]] ", name, type, isCollection, childClass,
313 childList);
314 }
315 }