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.configuration;
20  
21  import java.io.IOException;
22  import java.io.Writer;
23  import java.lang.reflect.InvocationTargetException;
24  import java.util.Arrays;
25  import java.util.Collection;
26  import java.util.HashSet;
27  import java.util.Optional;
28  import java.util.Set;
29  import java.util.SortedSet;
30  import java.util.UUID;
31  import java.util.function.Predicate;
32  
33  import org.apache.commons.lang3.StringUtils;
34  import org.apache.rat.ImplementationException;
35  import org.apache.rat.ReportConfiguration;
36  import org.apache.rat.analysis.IHeaderMatcher;
37  import org.apache.rat.api.RatException;
38  import org.apache.rat.config.parameters.ComponentType;
39  import org.apache.rat.config.parameters.Description;
40  import org.apache.rat.configuration.builders.MatcherRefBuilder;
41  import org.apache.rat.license.ILicense;
42  import org.apache.rat.license.ILicenseFamily;
43  import org.apache.rat.license.LicenseSetFactory.LicenseFilter;
44  import org.apache.rat.report.xml.writer.XmlWriter;
45  
46  /**
47   * Writes the XML configuration file format.
48   */
49  public class XMLConfigurationWriter {
50      /** The configuration that is being written. */
51      private final ReportConfiguration configuration;
52      /** The set of defined matcher IDs. */
53      private final Set<String> matchers;
54      /** The set of defined license IDs. */
55      private final Set<String> licenseChildren;
56  
57      /**
58       * Constructor
59       * @param configuration the configuration information to write.
60       */
61      public XMLConfigurationWriter(final ReportConfiguration configuration) {
62          this.configuration = configuration;
63          this.matchers = new HashSet<>();
64          licenseChildren = new HashSet<>(Arrays.asList(XMLConfig.LICENSE_CHILDREN));
65      }
66  
67      private Predicate<Description> attributeFilter(final Description parent) {
68          return d -> {
69              if (d.getType() == ComponentType.PARAMETER) {
70                  return switch (parent.getType()) {
71                      case MATCHER -> !XMLConfig.isInlineNode(parent.getCommonName(), d.getCommonName());
72                      case LICENSE -> !licenseChildren.contains(d.getCommonName());
73                      default -> true;
74                  };
75              }
76              return false;
77          };
78      }
79  
80      /**
81       * Writes the configuration to the specified writer.
82       * @param plainWriter a writer to write the XML to.
83       * @throws RatException on error.
84       */
85      public void write(final Writer plainWriter) throws RatException {
86          write(new XmlWriter(plainWriter));
87      }
88  
89      private void writeFamilies(final XmlWriter writer, final SortedSet<ILicenseFamily> families) throws IOException, RatException {
90          if (!families.isEmpty()) {
91              writer.startElement(XMLConfig.FAMILIES);
92              for (ILicenseFamily family : families) {
93                  writeFamily(writer, family);
94              }
95              writer.closeElement(); // FAMILIES
96          }
97      }
98  
99      private void writeLicenses(final XmlWriter writer, final SortedSet<ILicense> licenses) throws IOException, RatException {
100         if (!licenses.isEmpty()) {
101             writer.startElement(XMLConfig.LICENSES);
102             for (ILicense license : licenses) {
103                 writeDescription(writer, license.getDescription(), license);
104             }
105             writer.closeElement(); // LICENSES
106         }
107     }
108 
109     private void writeApproved(final XmlWriter writer) throws IOException {
110         writer.startElement(XMLConfig.APPROVED);
111         for (String family : configuration.getLicenseCategories(LicenseFilter.APPROVED)) {
112             writer.startElement(XMLConfig.APPROVED).attribute(XMLConfig.ATT_LICENSE_REF, family.trim())
113                     .closeElement();
114         }
115         writer.closeElement(); // APPROVED
116     }
117 
118     private void writeMatchers(final XmlWriter writer) throws IOException {
119         MatcherBuilderTracker tracker = MatcherBuilderTracker.instance();
120         writer.startElement(XMLConfig.MATCHERS);
121         for (Class<?> clazz : tracker.getClasses()) {
122             writer.startElement(XMLConfig.MATCHER).attribute(XMLConfig.ATT_CLASS_NAME, clazz.getCanonicalName())
123                     .closeElement();
124         }
125         writer.closeElement(); // MATCHERS
126     }
127 
128     /**
129      * Writes the configuration to an XmlWriter instance.
130      * @param writer the XmlWriter to write to.
131      * @throws RatException on error.
132      */
133     public void write(final XmlWriter writer) throws RatException {
134         if (configuration.listFamilies() != LicenseFilter.NONE || configuration.listLicenses() != LicenseFilter.NONE) {
135             try {
136                 writer.startElement(XMLConfig.ROOT);
137 
138                 writeFamilies(writer, configuration.getLicenseFamilies(configuration.listFamilies()));
139                 writeLicenses(writer, configuration.getLicenses(configuration.listLicenses()));
140                 writeApproved(writer);
141                 writeMatchers(writer);
142 
143                 writer.closeElement(); // ROOT
144             } catch (IOException e) {
145                 throw new RatException(e);
146             }
147         }
148     }
149 
150     private void writeFamily(final XmlWriter writer, final ILicenseFamily family) throws RatException {
151         try {
152             writer.startElement(XMLConfig.FAMILY).attribute(XMLConfig.ATT_ID, family.getFamilyCategory().trim())
153                     .attribute(XMLConfig.ATT_NAME, family.getFamilyName());
154             writer.closeElement();
155         } catch (IOException e) {
156             throw new RatException(e);
157         }
158     }
159 
160     private void writeDescriptions(final XmlWriter writer, final Collection<Description> descriptions, final IHeaderMatcher component)
161             throws RatException {
162         for (Description description : descriptions) {
163             writeDescription(writer, description, component);
164         }
165     }
166 
167     private void writeChildren(final XmlWriter writer, final Description description, final IHeaderMatcher component)
168             throws RatException {
169         writeAttributes(writer, description.filterChildren(attributeFilter(component.getDescription())), component);
170         writeDescriptions(writer, description.filterChildren(attributeFilter(component.getDescription()).negate()),
171                 component);
172     }
173 
174     private void writeAttributes(final XmlWriter writer, final Collection<Description> descriptions, final IHeaderMatcher component)
175             throws RatException {
176         for (Description d : descriptions) {
177             try {
178                 writeAttribute(writer, d, component);
179             } catch (IOException e) {
180                 throw new RatException(e);
181             }
182         }
183     }
184 
185     private void writeComment(final XmlWriter writer, final Description description) throws IOException {
186         if (StringUtils.isNotBlank(description.getDescription())) {
187             writer.comment(description.getDescription().replace("-->", "-&ndash;>"));
188         }
189     }
190 
191     private void writeAttribute(final XmlWriter writer, final Description description, final IHeaderMatcher component)
192             throws IOException {
193         String paramValue = description.getParamValue(component);
194         if (paramValue != null) {
195             writer.attribute(description.getCommonName(), paramValue);
196         }
197     }
198 
199     private boolean hasUUIDId(final Description desc, final IHeaderMatcher comp) {
200         if ("id".equals(desc.getCommonName())) {
201             try {
202                 String paramId = desc.getParamValue(comp);
203                 // if a UUID skip it.
204                 if (paramId != null) {
205                     UUID.fromString(paramId);
206                     return true;
207                 }
208             } catch (IllegalArgumentException expected) {
209                 // do nothing.
210             }
211         }
212         return false;
213     }
214 
215     private void writeChildParameter(final XmlWriter writer, final Description description, final IHeaderMatcher component) throws IOException {
216         boolean inline = XMLConfig.isInlineNode(component.getDescription().getCommonName(),
217                 description.getCommonName());
218         String s = description.getParamValue(component);
219         if (StringUtils.isNotBlank(s)) {
220             if (!inline) {
221                 writer.startElement(description.getCommonName());
222             }
223             writer.content(description.getParamValue(component));
224             if (!inline) {
225                 writer.closeElement();
226             }
227         }
228 
229     }
230     @SuppressWarnings("unchecked")
231     private void writeParameterDescription(final XmlWriter writer, final Description description, final IHeaderMatcher component) throws IOException {
232         if (hasUUIDId(description, component)) {
233             return;
234         }
235 
236         if (description.getChildType() == String.class) {
237             writeChildParameter(writer, description, component);
238         } else {
239             try {
240                 if (description.isCollection()) {
241                     for (IHeaderMatcher matcher : (Collection<IHeaderMatcher>) description
242                             .getter(component.getClass()).invoke(component)) {
243                         writeDescription(writer, matcher.getDescription(), matcher);
244                     }
245                 } else {
246                     IHeaderMatcher matcher = (IHeaderMatcher) description.getter(component.getClass())
247                             .invoke(component);
248                     writeDescription(writer, matcher.getDescription(), matcher);
249                 }
250             } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
251                      | NoSuchMethodException | SecurityException | RatException e) {
252                 throw new ImplementationException(e);
253             }
254         }
255     }
256 
257     private void writeMatcherDescription(final XmlWriter writer, final Description desc, final IHeaderMatcher comp) throws IOException, RatException {
258         Description description = desc;
259         IHeaderMatcher component = comp;
260         // see if id was registered
261         Optional<Description> id = description.childrenOfType(ComponentType.PARAMETER).stream()
262                 .filter(i -> XMLConfig.ATT_ID.equals(i.getCommonName())).findFirst();
263 
264         // id will not be present in matcherRef
265         if (id.isPresent()) {
266             String matcherId = id.get().getParamValue(component);
267             // if we have seen the id before, put a reference to the other one.
268             if (matchers.contains(matcherId)) {
269                 component = new MatcherRefBuilder.IHeaderMatcherProxy(matcherId, null);
270                 description = component.getDescription();
271             } else {
272                 matchers.add(matcherId);
273             }
274             // remove the matcher id if it is a UUID
275             try {
276                 UUID.fromString(matcherId);
277                 description.getChildren().remove(XMLConfig.ATT_ID);
278             } catch (IllegalArgumentException expected) {
279                 if (description.getCommonName().equals("spdx")) {
280                     description.getChildren().remove(XMLConfig.ATT_ID);
281                 }
282             }
283         }
284 
285         // if only a resource, list the resource not the contents of the matcher
286         Optional<Description> resource = description.childrenOfType(ComponentType.PARAMETER).stream()
287                 .filter(i -> XMLConfig.ATT_RESOURCE.equals(i.getCommonName())).findFirst();
288         if (resource.isPresent()) {
289             String resourceStr = resource.get().getParamValue(component);
290             if (StringUtils.isNotBlank(resourceStr)) {
291                 description.getChildren().remove("enclosed");
292             }
293         }
294         writeComment(writer, description);
295         writer.startElement(description.getCommonName());
296         writeChildren(writer, description, component);
297         writer.closeElement();
298     }
299 
300     /* package private for testing */
301     void writeDescription(final XmlWriter writer, final Description description, final IHeaderMatcher component) throws RatException {
302         try {
303             switch (description.getType()) {
304             case MATCHER:
305                 writeMatcherDescription(writer, description, component);
306                 break;
307             case LICENSE:
308                 writer.startElement(XMLConfig.LICENSE);
309                 writeChildren(writer, description, component);
310                 writer.closeElement();
311                 break;
312             case PARAMETER:
313                 writeParameterDescription(writer, description, component);
314                 break;
315             case BUILD_PARAMETER:
316                 // ignore;
317                 break;
318             }
319         } catch (IOException e) {
320             throw new RatException(e);
321         }
322     }
323 }