Skip to content

Commit

Permalink
[MENFORCER-507] Add xsltLocation parameter to ExternalRules
Browse files Browse the repository at this point in the history
  • Loading branch information
ppalaga committed Aug 5, 2024
1 parent abca51d commit 26d2ba8
Show file tree
Hide file tree
Showing 5 changed files with 256 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,26 @@

import javax.inject.Inject;
import javax.inject.Named;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Objects;

import org.apache.maven.enforcer.rule.api.AbstractEnforcerRuleConfigProvider;
import org.apache.maven.enforcer.rule.api.EnforcerRuleError;
import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
import org.apache.maven.enforcer.rules.utils.ExpressionEvaluator;
import org.apache.maven.plugin.MojoExecution;
import org.codehaus.plexus.util.xml.Xpp3Dom;
Expand All @@ -46,11 +57,75 @@ public final class ExternalRules extends AbstractEnforcerRuleConfigProvider {
private static final String LOCATION_PREFIX_CLASSPATH = "classpath:";

/**
* The external rules location. If it starts with "classpath:", the resource is read from the classpath.
* The external rules location. If it starts with <code>classpath:</code> the resource is read from the classpath.
* Otherwise, it is handled as a filesystem path, either absolute, or relative to <code>${project.basedir}</code>
*
* @since 3.2.0
*/
private String location;

/**
* An optional location of an XSLT file used to transform the rule document available via {@link #location} before
* it is applied. If it starts with <code>classpath:</code> the resource is read from the classpath.
* Otherwise, it is handled as a filesystem path, either absolute, or relative to <code>${project.basedir}</code>
* <p>
* This is useful, when you want to consume rules defined in an external project, but you need to
* remove or adapt some of those for the local circumstances.
* <p>
* <strong>Example</strong>
* <p>
* If <code>location</code> points at the following rule set:
*
* <pre>{@code
* <enforcer>
* <rules>
* <bannedDependencies>
* <excludes>
* <exclude>com.google.code.findbugs:jsr305</exclude>
* <exclude>com.google.guava:listenablefuture</exclude>
* </excludes>
* </bannedDependencies>
* </rules>
* </enforcer>
* }</pre>
*
* And if <code>xsltLocation</code> points at the following transformation
*
* <pre>{@code
* <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
* <xsl:output omit-xml-declaration="yes"/>
*
* <!-- Copy everything unless there is a template with a more specific matcher -->
* <xsl:template match="node()|@*">
* <xsl:copy>
* <xsl:apply-templates select="node()|@*"/>
* </xsl:copy>
* </xsl:template>
*
* <!-- An empty template will effectively remove the matching nodes -->
* <xsl:template match=
* "//bannedDependencies/excludes/exclude[contains(text(), 'com.google.code.findbugs:jsr305')]"/>
* </xsl:stylesheet>
* }</pre>
*
* Then the effective rule set will look like to following:
*
* <pre>{@code
* <enforcer>
* <rules>
* <bannedDependencies>
* <excludes>
* <exclude>com.google.guava:listenablefuture</exclude>
* </excludes>
* </bannedDependencies>
* </rules>
* </enforcer>
* }</pre>
*
* @since 3.6.0
*/
private String xsltLocation;

private final MojoExecution mojoExecution;

private final ExpressionEvaluator evaluator;
Expand All @@ -65,10 +140,14 @@ public void setLocation(String location) {
this.location = location;
}

public void setXsltLocation(String xsltLocation) {
this.xsltLocation = xsltLocation;
}

@Override
public Xpp3Dom getRulesConfig() throws EnforcerRuleError {

try (InputStream descriptorStream = resolveDescriptor()) {
try (InputStream descriptorStream = transform(location, resolveDescriptor(location), xsltLocation)) {
Xpp3Dom enforcerRules = Xpp3DomBuilder.build(descriptorStream, "UTF-8");
if (enforcerRules.getChildCount() == 1 && "enforcer".equals(enforcerRules.getName())) {
return enforcerRules.getChild(0);
Expand All @@ -80,19 +159,19 @@ public Xpp3Dom getRulesConfig() throws EnforcerRuleError {
}
}

private InputStream resolveDescriptor() throws EnforcerRuleError {
private InputStream resolveDescriptor(String path) throws EnforcerRuleError {
InputStream descriptorStream;
if (location != null) {
if (location.startsWith(LOCATION_PREFIX_CLASSPATH)) {
String classpathLocation = location.substring(LOCATION_PREFIX_CLASSPATH.length());
if (path != null) {
if (path.startsWith(LOCATION_PREFIX_CLASSPATH)) {
String classpathLocation = path.substring(LOCATION_PREFIX_CLASSPATH.length());
getLog().debug("Read rules form classpath location: " + classpathLocation);
ClassLoader classRealm = mojoExecution.getMojoDescriptor().getRealm();
descriptorStream = classRealm.getResourceAsStream(classpathLocation);
if (descriptorStream == null) {
throw new EnforcerRuleError("Location '" + classpathLocation + "' not found in classpath");
}
} else {
File descriptorFile = evaluator.alignToBaseDirectory(new File(location));
File descriptorFile = evaluator.alignToBaseDirectory(new File(path));
getLog().debug("Read rules form file location: " + descriptorFile);
try {
descriptorStream = Files.newInputStream(descriptorFile.toPath());
Expand All @@ -108,6 +187,29 @@ private InputStream resolveDescriptor() throws EnforcerRuleError {

@Override
public String toString() {
return String.format("ExternalRules[location=%s]", location);
return String.format("ExternalRules[location=%s, xsltLocation=%s]", location, xsltLocation);
}

InputStream transform(String sourceLocation, InputStream sourceXml, String xsltLocation) {
if (xsltLocation == null || xsltLocation.trim().isEmpty()) {
return sourceXml;
}

try (InputStream in = resolveDescriptor(xsltLocation);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
Transformer transformer = TransformerFactory.newInstance().newTransformer(new StreamSource(in));
transformer.transform(new StreamSource(sourceXml), new StreamResult(baos));
final byte[] bytes = baos.toByteArray();
getLog().info(() -> (CharSequence) ("Rules transformed by " + xsltLocation + " from " + location + ":\n\n"
+ new String(bytes, StandardCharsets.UTF_8)));
return new ByteArrayInputStream(bytes);
} catch (IOException
| EnforcerRuleException
| TransformerConfigurationException
| TransformerFactoryConfigurationError e) {
throw new RuntimeException("Could not open resource " + xsltLocation);
} catch (TransformerException e) {
throw new RuntimeException("Could not transform " + sourceLocation + " usinng XSLT " + xsltLocation);
}
}
}
60 changes: 60 additions & 0 deletions enforcer-rules/src/site/apt/externalRules.apt.vm
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,63 @@ The External Enforcer Rule Descriptor
</rules>
</enforcer>
+---+

<<<xsltLocation>>>: Rule Descriptor Transformation (since Enforcer 3.6.0)

In addition to <<<location>>> parameter, <<<externalRules>>> accepts another optional parameter <<<xsltLocation>>>.

It is useful, when you want to consume rules defined in an external project, but you need to
remove or adapt some of those for the local circumstances.

<<<xsltLocation>>> specifies the location of an XSLT file used to transform the rule document available via
<<<location>>> before it is applied. If it starts with <<<classpath:>>> the resource is read from the classpath.
Otherwise, it is handled as a filesystem path, either absolute, or relative to <<<$\{project.basedir\}>>>.

Here is an example: If <<<location>>> points at the following rule set:

+---+
<enforcer>
<rules>
<bannedDependencies>
<excludes>
<exclude>com.google.code.findbugs:jsr305</exclude>
<exclude>com.google.guava:listenablefuture</exclude>
</excludes>
</bannedDependencies>
</rules>
</enforcer>
+---+

and if <<<xsltLocation>>> points at the following transformation

+---+
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes"/>

<!-- Copy everything unless there is a template with a more specific matcher -->
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>

<!-- An empty template will effectively remove the matching nodes -->
<xsl:template match=
"//bannedDependencies/excludes/exclude[contains(text(), 'com.google.code.findbugs:jsr305')]"/>
</xsl:stylesheet>
+---+

Then the effective rule set will look like to following:

+---+
<enforcer>
<rules>
<bannedDependencies>
<excludes>
<exclude>com.google.guava:listenablefuture</exclude>
</excludes>
</bannedDependencies>
</rules>
</enforcer>
+---+

Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,25 @@ void shouldLoadRulesFromClassPath() throws EnforcerRuleException {
assertNotNull(rulesConfig);
assertEquals(2, rulesConfig.getChildCount());
}

@Test
void shouldFilterRules() throws EnforcerRuleException {
MojoDescriptor mojoDescriptor = new MojoDescriptor();
mojoDescriptor.setRealm(EnforcerTestUtils.getTestClassRealm());
when(mojoExecution.getMojoDescriptor()).thenReturn(mojoDescriptor);
rule.setLocation("classpath:enforcer-rules/banned-dependencies.xml");
rule.setXsltLocation("classpath:enforcer-rules/allow-findbugs.xsl");

Xpp3Dom rulesConfig = rule.getRulesConfig();
assertNotNull(rulesConfig);
assertEquals(1, rulesConfig.getChildCount());
assertEquals("bannedDependencies", rulesConfig.getChild(0).getName());
assertEquals(1, rulesConfig.getChild(0).getChildCount());
assertEquals("excludes", rulesConfig.getChild(0).getChild(0).getName());
assertEquals(1, rulesConfig.getChild(0).getChild(0).getChildCount());
assertEquals("exclude", rulesConfig.getChild(0).getChild(0).getChild(0).getName());
assertEquals(
"com.google.guava:listenablefuture",
rulesConfig.getChild(0).getChild(0).getChild(0).getValue());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>

<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<xsl:output omit-xml-declaration="yes"/>

<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>

<!-- An empty template will effectively remove the maching nodes -->
<xsl:template match="//bannedDependencies/excludes/exclude[contains(text(), 'com.google.code.findbugs:jsr305')]"/>
</xsl:stylesheet>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>

<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->

<enforcer>
<rules>
<bannedDependencies>
<excludes>
<exclude>com.google.code.findbugs:jsr305</exclude>
<exclude>com.google.guava:listenablefuture</exclude>
</excludes>
</bannedDependencies>
</rules>
</enforcer>

0 comments on commit 26d2ba8

Please sign in to comment.