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.maven.enforcer.rules;
20  
21  import javax.inject.Inject;
22  import javax.inject.Named;
23  import javax.xml.transform.Transformer;
24  import javax.xml.transform.TransformerConfigurationException;
25  import javax.xml.transform.TransformerException;
26  import javax.xml.transform.TransformerFactory;
27  import javax.xml.transform.TransformerFactoryConfigurationError;
28  import javax.xml.transform.stream.StreamResult;
29  import javax.xml.transform.stream.StreamSource;
30  
31  import java.io.ByteArrayInputStream;
32  import java.io.ByteArrayOutputStream;
33  import java.io.File;
34  import java.io.IOException;
35  import java.io.InputStream;
36  import java.nio.charset.StandardCharsets;
37  import java.nio.file.Files;
38  import java.util.Objects;
39  
40  import org.apache.maven.enforcer.rule.api.AbstractEnforcerRuleConfigProvider;
41  import org.apache.maven.enforcer.rule.api.EnforcerRuleError;
42  import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
43  import org.apache.maven.enforcer.rules.utils.ExpressionEvaluator;
44  import org.apache.maven.plugin.MojoExecution;
45  import org.codehaus.plexus.util.xml.Xpp3Dom;
46  import org.codehaus.plexus.util.xml.Xpp3DomBuilder;
47  import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
48  
49  /**
50   * An enforcer rule that will provide rules configuration from an external resource.
51   *
52   * @author <a href="mailto:gastaldi@apache.org">George Gastaldi</a>
53   * @since 3.2.0
54   */
55  @Named("externalRules")
56  public final class ExternalRules extends AbstractEnforcerRuleConfigProvider {
57      private static final String LOCATION_PREFIX_CLASSPATH = "classpath:";
58  
59      /**
60       * The external rules location. If it starts with <code>classpath:</code> the resource is read from the classpath.
61       * Otherwise, it is handled as a filesystem path, either absolute, or relative to <code>${project.basedir}</code>
62       *
63       * @since 3.2.0
64       */
65      private String location;
66  
67      /**
68       * An optional location of an XSLT file used to transform the rule document available via {@link #location} before
69       * it is applied. If it starts with <code>classpath:</code> the resource is read from the classpath.
70       * Otherwise, it is handled as a filesystem path, either absolute, or relative to <code>${project.basedir}</code>
71       * <p>
72       * This is useful, when you want to consume rules defined in an external project, but you need to
73       * remove or adapt some of those for the local circumstances.
74       * <p>
75       * <strong>Example</strong>
76       * <p>
77       * If <code>location</code> points at the following rule set:
78       *
79       * <pre>{@code
80       * <enforcer>
81       *   <rules>
82       *     <bannedDependencies>
83       *        <excludes>
84       *          <exclude>com.google.code.findbugs:jsr305</exclude>
85       *          <exclude>com.google.guava:listenablefuture</exclude>
86       *        </excludes>
87       *     </bannedDependencies>
88       *   </rules>
89       * </enforcer>
90       * }</pre>
91       *
92       * And if <code>xsltLocation</code> points at the following transformation
93       *
94       * <pre>{@code
95       * <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
96       *   <xsl:output omit-xml-declaration="yes"/>
97       *
98       *   <!-- Copy everything unless there is a template with a more specific matcher -->
99       *   <xsl:template match="node()|@*">
100      *     <xsl:copy>
101      *       <xsl:apply-templates select="node()|@*"/>
102      *     </xsl:copy>
103      *   </xsl:template>
104      *
105      *   <!-- An empty template will effectively remove the matching nodes -->
106      *   <xsl:template match=
107      * "//bannedDependencies/excludes/exclude[contains(text(), 'com.google.code.findbugs:jsr305')]"/>
108      * </xsl:stylesheet>
109      * }</pre>
110      *
111      * Then the effective rule set will look like to following:
112      *
113      * <pre>{@code
114      * <enforcer>
115      *   <rules>
116      *     <bannedDependencies>
117      *        <excludes>
118      *          <exclude>com.google.guava:listenablefuture</exclude>
119      *        </excludes>
120      *     </bannedDependencies>
121      *   </rules>
122      * </enforcer>
123      * }</pre>
124      *
125      * @since 3.6.0
126      */
127     private String xsltLocation;
128 
129     private final MojoExecution mojoExecution;
130 
131     private final ExpressionEvaluator evaluator;
132 
133     @Inject
134     public ExternalRules(MojoExecution mojoExecution, ExpressionEvaluator evaluator) {
135         this.mojoExecution = Objects.requireNonNull(mojoExecution);
136         this.evaluator = Objects.requireNonNull(evaluator);
137     }
138 
139     public void setLocation(String location) {
140         this.location = location;
141     }
142 
143     public void setXsltLocation(String xsltLocation) {
144         this.xsltLocation = xsltLocation;
145     }
146 
147     @Override
148     public Xpp3Dom getRulesConfig() throws EnforcerRuleError {
149 
150         try (InputStream descriptorStream = transform(location, resolveDescriptor(location), xsltLocation)) {
151             Xpp3Dom enforcerRules = Xpp3DomBuilder.build(descriptorStream, "UTF-8");
152             if (enforcerRules.getChildCount() == 1 && "enforcer".equals(enforcerRules.getName())) {
153                 return enforcerRules.getChild(0);
154             } else {
155                 throw new EnforcerRuleError("Enforcer rules configuration not found in: " + location);
156             }
157         } catch (IOException | XmlPullParserException e) {
158             throw new EnforcerRuleError(e);
159         }
160     }
161 
162     private InputStream resolveDescriptor(String path) throws EnforcerRuleError {
163         InputStream descriptorStream;
164         if (path != null) {
165             if (path.startsWith(LOCATION_PREFIX_CLASSPATH)) {
166                 String classpathLocation = path.substring(LOCATION_PREFIX_CLASSPATH.length());
167                 getLog().debug("Read rules form classpath location: " + classpathLocation);
168                 ClassLoader classRealm = mojoExecution.getMojoDescriptor().getRealm();
169                 descriptorStream = classRealm.getResourceAsStream(classpathLocation);
170                 if (descriptorStream == null) {
171                     throw new EnforcerRuleError("Location '" + classpathLocation + "' not found in classpath");
172                 }
173             } else {
174                 File descriptorFile = evaluator.alignToBaseDirectory(new File(path));
175                 getLog().debug("Read rules form file location: " + descriptorFile);
176                 try {
177                     descriptorStream = Files.newInputStream(descriptorFile.toPath());
178                 } catch (IOException e) {
179                     throw new EnforcerRuleError("Could not read descriptor in " + descriptorFile, e);
180                 }
181             }
182         } else {
183             throw new EnforcerRuleError("No location provided");
184         }
185         return descriptorStream;
186     }
187 
188     @Override
189     public String toString() {
190         return String.format("ExternalRules[location=%s, xsltLocation=%s]", location, xsltLocation);
191     }
192 
193     InputStream transform(String sourceLocation, InputStream sourceXml, String xsltLocation) {
194         if (xsltLocation == null || xsltLocation.trim().isEmpty()) {
195             return sourceXml;
196         }
197 
198         try (InputStream in = resolveDescriptor(xsltLocation);
199                 ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
200             Transformer transformer = TransformerFactory.newInstance().newTransformer(new StreamSource(in));
201             transformer.transform(new StreamSource(sourceXml), new StreamResult(baos));
202             final byte[] bytes = baos.toByteArray();
203             getLog().info(() -> ("Rules transformed by " + xsltLocation + " from " + location + ":\n\n"
204                     + new String(bytes, StandardCharsets.UTF_8)));
205             return new ByteArrayInputStream(bytes);
206         } catch (IOException
207                 | EnforcerRuleException
208                 | TransformerConfigurationException
209                 | TransformerFactoryConfigurationError e) {
210             throw new RuntimeException("Could not open resource " + xsltLocation);
211         } catch (TransformerException e) {
212             throw new RuntimeException("Could not transform " + sourceLocation + " usinng XSLT " + xsltLocation);
213         }
214     }
215 }