View Javadoc
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.Arrays;
24  import java.util.Collection;
25  import java.util.Map;
26  import java.util.TreeMap;
27  import java.util.function.Predicate;
28  import java.util.stream.Collectors;
29  
30  import org.apache.commons.lang3.StringUtils;
31  import org.apache.rat.BuilderParams;
32  import org.apache.rat.ConfigurationException;
33  import org.apache.rat.analysis.IHeaderMatcher;
34  import org.apache.rat.utils.Log;
35  
36  /**
37   * A description of a component.
38   */
39  public class Description {
40      /** The type of component this describes */
41      private final ComponentType type;
42      /**
43       * The common name for the component. Set by ConfigComponent.name() or
44       * class/field name.
45       */
46      private final String name;
47      /** The description for the component */
48      private final String desc;
49      /** The class of the getter/setter parameter */
50      private final Class<?> childClass;
51      /** True if the getter/setter expects a collection of childClass objects */
52      private final boolean isCollection;
53      /** True if this component is required. */
54      private final boolean required;
55      /**
56       * A map of name to Description for all the components that are children of the
57       * described component.
58       */
59      private final Map<String, Description> children;
60  
61      public static Predicate<Description> typePredicate(ComponentType type) {
62          return (d) -> d.getType() == type;
63      }
64  
65      /**
66       * Constructor.
67       * 
68       * @param type the type of the component.
69       * @param name the name of the component.
70       * @param desc the description of the component.
71       * @param isCollection true if the getter/setter expects a collection
72       * @param childClass the class for expected for the getter/setter.
73       * @param children the collection of descriptions for all the components that
74       * are children of the described component.
75       */
76      public Description(ComponentType type, String name, String desc, boolean isCollection, Class<?> childClass,
77              Collection<Description> children, boolean required) {
78          this.type = type;
79          this.name = name;
80          this.desc = desc;
81          this.isCollection = isCollection;
82          this.required = required;
83          if (type == ComponentType.BUILD_PARAMETER) {
84              Method m;
85              try {
86                  m = BuilderParams.class.getMethod(name);
87              } catch (NoSuchMethodException | SecurityException e) {
88                  throw new ConfigurationException(String.format("'%s' is not a valid BuildParams method", name));
89              }
90              this.childClass = m.getReturnType();
91          } else {
92              this.childClass = childClass;
93          }
94          this.children = new TreeMap<>();
95          if (children != null) {
96              children.forEach(d -> {
97                  this.children.put(d.name, d);
98              });
99          }
100     }
101 
102     /**
103      * Constructor
104      * 
105      * @param configComponent the configuration component
106      * @param isCollection the collection flag.
107      * @param childClass the type of object that the method getter/setter expects.
108      * @param children the collection of descriptions for all the components that
109      * are children the described component.
110      */
111     public Description(ConfigComponent configComponent, boolean isCollection, Class<?> childClass,
112             Collection<Description> children) {
113         this(configComponent.type(), configComponent.name(), configComponent.desc(), isCollection, childClass, children,
114                 configComponent.required());
115     }
116 
117     /**
118      * Get the canBeChild flag.
119      * 
120      * @return {@code true} if this item can be a child of the containing item.
121      */
122     public boolean isRequired() {
123         return required;
124     }
125 
126     /**
127      * Gets the type of the component.
128      * 
129      * @return the component type.
130      */
131     public ComponentType getType() {
132         return type;
133     }
134 
135     /**
136      * Get the isCollection flag.
137      * 
138      * @return true if this is a collection.
139      */
140     public boolean isCollection() {
141         return isCollection;
142     }
143 
144     /**
145      * Get the class of the objects for the getter/setter methods.
146      * 
147      * @return the getter/setter param class.
148      */
149     public Class<?> getChildType() {
150         return childClass;
151     }
152 
153     /**
154      * Gets the common name for the matcher. (e.g. 'text', 'spdx', etc.) May not be
155      * null.
156      * 
157      * @return The common name for the item being inspected.
158      */
159     public String getCommonName() {
160         return name;
161     }
162 
163     /**
164      * Gets the description of descriptive text for the component. May be an empty
165      * string or null.
166      * 
167      * @return the descriptive text;
168      */
169     public String getDescription() {
170         return desc;
171     }
172 
173     /**
174      * Retrieve the value of a the described parameter from the specified object.
175      * 
176      * If the parameter is a collection return {@code null}.
177      * 
178      * @param log the Log to log issues to.
179      * @param object the object that contains the value.
180      * @return the string value.
181      */
182     public String getParamValue(Log log, Object object) {
183         if (isCollection) {
184             return null;
185         }
186         try {
187             Object val = getter(object.getClass()).invoke(object);
188             return val == null ? null : val.toString();
189         } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException | NoSuchMethodException
190                 | SecurityException e) {
191             log.error(System.err.format("Can not retrieve value for '%s' from %s%n", name, object.getClass().getName()),
192                     e);
193             return null;
194         }
195     }
196 
197     /**
198      * Gets a map of the parameters that the object contains. For example Copyright
199      * has 'start', 'stop', and 'owner' parameters. Some IHeaderMatchers have simple
200      * text values (e.g. 'regex' or 'text' types) these should list an unnamed
201      * parameter (empty string) with the text value.
202      * 
203      * @return the map of parameters to the objects that represent them.
204      */
205     public Map<String, Description> getChildren() {
206         return children;
207     }
208 
209     /**
210      * Get all the children of a specific type
211      * 
212      * @param type the type to return
213      * @return the collection of children of the specified type.
214      */
215     public Collection<Description> childrenOfType(ComponentType type) {
216         return filterChildren(d -> d.getType() == type);
217     }
218 
219     /**
220      * Get a filtered collection of the child descriptions.
221      * 
222      * @param filter the filter to apply to the child descriptions.
223      * @return the collection of children that matche the filter..
224      */
225     public Collection<Description> filterChildren(Predicate<Description> filter) {
226         return children.values().stream().filter(filter).collect(Collectors.toList());
227     }
228 
229     /**
230      * Generate a method name for this description.
231      * 
232      * @param prefix the start of the method name (e.g. "set", "get" )
233      * @return the method name.
234      */
235     public String methodName(String prefix) {
236         return prefix + StringUtils.capitalize(name);
237     }
238 
239     /**
240      * Returns the getter for the component in the specified class.
241      * 
242      * @param clazz the Class to get the getter from.
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 getter(Class<?> clazz) throws NoSuchMethodException, SecurityException {
248         return clazz.getMethod(methodName("get"));
249     }
250 
251     /**
252      * Returns the setter for the component in the specified class. Notes:
253      * <ul>
254      * <li>License can not be set in components. They are top level components.</li>
255      * <li>Matcher expects an "add" method that accepts an
256      * IHeaderMatcher.Builder.</li>
257      * <li>Parameter expects a {@code set(String)} method.</li>
258      * <li>Unlabeled expects a {@code set(String)} method.</li>
259      * <li>BuilderParam expects a {@code set} method that takes a
260      * {@code childeClass} argument.</li>
261      * </ul>
262      * 
263      * @param clazz the Class to get the getter from, generally a Builder class..
264      * @return the getter Method.
265      * @throws NoSuchMethodException if the class does not have the getter.
266      * @throws SecurityException if the getter can not be accessed.
267      */
268     public Method setter(Class<?> clazz) throws NoSuchMethodException, SecurityException {
269         String methodName = methodName(isCollection ? "add" : "set");
270         switch (type) {
271         case LICENSE:
272             throw new NoSuchMethodException("Can not set a License as a child");
273         case MATCHER:
274             return clazz.getMethod(methodName, IHeaderMatcher.Builder.class);
275         case PARAMETER:
276             return clazz.getMethod(methodName,
277                     IHeaderMatcher.class.isAssignableFrom(childClass) ? IHeaderMatcher.Builder.class : childClass);
278         case BUILD_PARAMETER:
279             return clazz.getMethod(methodName, childClass);
280         }
281         // should not happen
282         throw new IllegalStateException("Type " + type + " not valid.");
283     }
284 
285     private void callSetter(Log log, Description description, IHeaderMatcher.Builder builder, String value) {
286         try {
287             description.setter(builder.getClass()).invoke(builder, value);
288         } catch (NoSuchMethodException e) {
289             String msg = String.format("No setter for '%s' on %s", description.getCommonName(),
290                     builder.getClass().getCanonicalName());
291             log.error(msg);
292             throw new ConfigurationException(msg);
293         } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | SecurityException e) {
294             String msg = String.format("Unable to call setter for '%s' on %s", description.getCommonName(),
295                     builder.getClass().getCanonicalName());
296             log.error(msg, e);
297             throw new ConfigurationException(msg, e);
298         }
299     }
300 
301     /**
302      * Sets the children of values in the builder. Sets the parameters to the values
303      * specified in the map. Only children that accept string arguments should be
304      * specified.
305      * 
306      * @param log The log to write messages to.
307      * @param builder The Matcher builder to set the values in.
308      * @param attributes a Map of parameter names to values.
309      */
310     public void setChildren(Log log, IHeaderMatcher.Builder builder, Map<String, String> attributes) {
311         attributes.entrySet().forEach(entry -> setChild(log, builder, entry.getKey(), entry.getValue()));
312     }
313 
314     /**
315      * Sets the child value in the builder.
316      * 
317      * @param log The log to write messages to.
318      * @param builder The Matcher builder to set the values in.
319      * @param name the name of the child to set
320      * @param value the value of the parameter.
321      */
322     public void setChild(Log log, IHeaderMatcher.Builder builder, String name, String value) {
323         Description d = getChildren().get(name);
324         if (d == null) {
325             log.error(String.format("%s does not define a ConfigComponent for a member %s.",
326                     builder.getClass().getCanonicalName(), name));
327         } else {
328             callSetter(log, d, builder, value);
329         }
330     }
331 
332     @Override
333     public String toString() {
334         String childList = children.isEmpty() ? ""
335                 : String.join(", ",
336                         children.values().stream().map(Description::getCommonName).collect(Collectors.toList()));
337 
338         return String.format("Description[%s t:%s c:%s %s children: [%s]] ", name, type, isCollection, childClass,
339                 childList);
340     }
341 
342     /**
343      * Write a description with indentation.
344      * 
345      * @param indent the number of spaces to indent.
346      * @return the string with the formatted data.
347      */
348     public String toString(int indent) {
349         char[] spaces = new char[indent];
350         Arrays.fill(spaces, ' ');
351         String padding = String.copyValueOf(spaces);
352         String top = String.format("%sDescription[ t:%s n:%s c:%s %s%n%s   %s] ", padding, type, name, isCollection,
353                 childClass, padding, desc);
354         if (children.isEmpty()) {
355             return top;
356         }
357         StringBuilder sb = new StringBuilder(top);
358         for (Description child : children.values()) {
359             sb.append(System.lineSeparator()).append(child.toString(indent + 2));
360         }
361         return sb.toString();
362     }
363 }