001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.maven.enforcer.rules;
020
021import javax.inject.Inject;
022import javax.inject.Named;
023import javax.xml.transform.Transformer;
024import javax.xml.transform.TransformerConfigurationException;
025import javax.xml.transform.TransformerException;
026import javax.xml.transform.TransformerFactory;
027import javax.xml.transform.TransformerFactoryConfigurationError;
028import javax.xml.transform.stream.StreamResult;
029import javax.xml.transform.stream.StreamSource;
030
031import java.io.ByteArrayInputStream;
032import java.io.ByteArrayOutputStream;
033import java.io.File;
034import java.io.IOException;
035import java.io.InputStream;
036import java.nio.charset.StandardCharsets;
037import java.nio.file.Files;
038import java.util.Objects;
039
040import org.apache.maven.enforcer.rule.api.AbstractEnforcerRuleConfigProvider;
041import org.apache.maven.enforcer.rule.api.EnforcerRuleError;
042import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
043import org.apache.maven.enforcer.rules.utils.ExpressionEvaluator;
044import org.apache.maven.plugin.MojoExecution;
045import org.codehaus.plexus.util.xml.Xpp3Dom;
046import org.codehaus.plexus.util.xml.Xpp3DomBuilder;
047import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
048
049/**
050 * An enforcer rule that will provide rules configuration from an external resource.
051 *
052 * @author <a href="mailto:gastaldi@apache.org">George Gastaldi</a>
053 * @since 3.2.0
054 */
055@Named("externalRules")
056public final class ExternalRules extends AbstractEnforcerRuleConfigProvider {
057    private static final String LOCATION_PREFIX_CLASSPATH = "classpath:";
058
059    /**
060     * The external rules location. If it starts with <code>classpath:</code> the resource is read from the classpath.
061     * Otherwise, it is handled as a filesystem path, either absolute, or relative to <code>${project.basedir}</code>
062     *
063     * @since 3.2.0
064     */
065    private String location;
066
067    /**
068     * An optional location of an XSLT file used to transform the rule document available via {@link #location} before
069     * it is applied. If it starts with <code>classpath:</code> the resource is read from the classpath.
070     * Otherwise, it is handled as a filesystem path, either absolute, or relative to <code>${project.basedir}</code>
071     * <p>
072     * This is useful, when you want to consume rules defined in an external project, but you need to
073     * remove or adapt some of those for the local circumstances.
074     * <p>
075     * <strong>Example</strong>
076     * <p>
077     * If <code>location</code> points at the following rule set:
078     *
079     * <pre>{@code
080     * <enforcer>
081     *   <rules>
082     *     <bannedDependencies>
083     *        <excludes>
084     *          <exclude>com.google.code.findbugs:jsr305</exclude>
085     *          <exclude>com.google.guava:listenablefuture</exclude>
086     *        </excludes>
087     *     </bannedDependencies>
088     *   </rules>
089     * </enforcer>
090     * }</pre>
091     *
092     * And if <code>xsltLocation</code> points at the following transformation
093     *
094     * <pre>{@code
095     * <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
096     *   <xsl:output omit-xml-declaration="yes"/>
097     *
098     *   <!-- Copy everything unless there is a template with a more specific matcher -->
099     *   <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}