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}