blob: 274e88583bb38247d8f962a82d4521d8f37450f8 [file] [log] [blame]
package org.apache.maven.shared.artifact.filter;
/*
* 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.
*/
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
import org.apache.maven.artifact.versioning.VersionRange;
import org.slf4j.Logger;
import static java.util.Objects.requireNonNull;
/**
* TODO: include in maven-artifact in future
*
* @author <a href="mailto:brett@apache.org">Brett Porter</a>
* @see StrictPatternIncludesArtifactFilter
*/
public class PatternIncludesArtifactFilter implements ArtifactFilter, StatisticsReportingArtifactFilter
{
private static final String SEP = System.lineSeparator();
/**
* Holds the set of compiled patterns
*/
private final Set<Pattern> patterns;
/**
* Whether the dependency trail should be checked
*/
private final boolean actTransitively;
/**
* Set of patterns that have been triggered
*/
private final Set<Pattern> patternsTriggered = new HashSet<>();
/**
* Set of artifacts that have been filtered out
*/
private final List<Artifact> filteredArtifact = new ArrayList<>();
/**
* <p>Constructor for PatternIncludesArtifactFilter.</p>
*
* @param patterns The pattern to be used.
*/
public PatternIncludesArtifactFilter( final Collection<String> patterns )
{
this( patterns, false );
}
/**
* <p>Constructor for PatternIncludesArtifactFilter.</p>
*
* @param patterns The pattern to be used.
* @param actTransitively transitive yes/no.
*/
public PatternIncludesArtifactFilter( final Collection<String> patterns, final boolean actTransitively )
{
this.actTransitively = actTransitively;
final Set<Pattern> pat = new LinkedHashSet<>();
if ( patterns != null && !patterns.isEmpty() )
{
for ( String pattern : patterns )
{
Pattern p = compile( pattern );
pat.add( p );
}
}
this.patterns = pat;
}
@Override
public boolean include( final Artifact artifact )
{
final boolean shouldInclude = patternMatches( artifact );
if ( !shouldInclude )
{
addFilteredArtifact( artifact );
}
return shouldInclude;
}
protected boolean patternMatches( final Artifact artifact )
{
Boolean match = match( adapt( artifact ) );
if ( match != null )
{
return match;
}
if ( actTransitively )
{
final List<String> depTrail = artifact.getDependencyTrail();
if ( depTrail != null && depTrail.size() > 1 )
{
for ( String trailItem : depTrail )
{
Artifactoid artifactoid = adapt( trailItem );
match = match( artifactoid );
if ( match != null )
{
return match;
}
}
}
}
return false;
}
private Boolean match( Artifactoid artifactoid )
{
for ( Pattern pattern : patterns )
{
if ( pattern.matches( artifactoid ) )
{
patternsTriggered.add( pattern );
return !( pattern instanceof NegativePattern );
}
}
return null;
}
/**
* <p>addFilteredArtifact.</p>
*
* @param artifact add artifact to the filtered artifacts list.
*/
protected void addFilteredArtifact( final Artifact artifact )
{
filteredArtifact.add( artifact );
}
@Override
public void reportMissedCriteria( final Logger logger )
{
// if there are no patterns, there is nothing to report.
if ( !patterns.isEmpty() )
{
final List<Pattern> missed = new ArrayList<>( patterns );
missed.removeAll( patternsTriggered );
if ( !missed.isEmpty() && logger.isWarnEnabled() )
{
final StringBuilder buffer = new StringBuilder();
buffer.append( "The following patterns were never triggered in this " );
buffer.append( getFilterDescription() );
buffer.append( ':' );
for ( Pattern pattern : missed )
{
buffer.append( SEP ) .append( "o '" ).append( pattern ).append( "'" );
}
buffer.append( SEP );
logger.warn( buffer.toString() );
}
}
}
@Override
public String toString()
{
return "Includes filter:" + getPatternsAsString();
}
protected String getPatternsAsString()
{
final StringBuilder buffer = new StringBuilder();
for ( Pattern pattern : patterns )
{
buffer.append( SEP ).append( "o '" ).append( pattern ).append( "'" );
}
return buffer.toString();
}
protected String getFilterDescription()
{
return "artifact inclusion filter";
}
@Override
public void reportFilteredArtifacts( final Logger logger )
{
if ( !filteredArtifact.isEmpty() && logger.isDebugEnabled() )
{
final StringBuilder buffer = new StringBuilder(
"The following artifacts were removed by this " + getFilterDescription() + ": " );
for ( Artifact artifactId : filteredArtifact )
{
buffer.append( SEP ).append( artifactId.getId() );
}
logger.debug( buffer.toString() );
}
}
@Override
public boolean hasMissedCriteria()
{
// if there are no patterns, there is nothing to report.
if ( !patterns.isEmpty() )
{
final List<Pattern> missed = new ArrayList<>( patterns );
missed.removeAll( patternsTriggered );
return !missed.isEmpty();
}
return false;
}
private enum Coordinate
{
GROUP_ID, ARTIFACT_ID, TYPE, CLASSIFIER, BASE_VERSION
}
private interface Artifactoid
{
String getCoordinate( Coordinate coordinate );
}
private static Artifactoid adapt( final Artifact artifact )
{
requireNonNull( artifact );
return coordinate ->
{
requireNonNull( coordinate );
switch ( coordinate )
{
case GROUP_ID:
return artifact.getGroupId();
case ARTIFACT_ID:
return artifact.getArtifactId();
case BASE_VERSION:
return artifact.getBaseVersion();
case CLASSIFIER:
return artifact.hasClassifier() ? artifact.getClassifier() : null;
case TYPE:
return artifact.getType();
default:
}
throw new IllegalArgumentException( "unknown coordinate: " + coordinate );
};
}
/**
* Parses elements of {@link Artifact#getDependencyTrail()} list, they are either {@code G:A:T:V} or if artifact
* has classifier {@code G:A:T:C:V}, so strictly 4 or 5 segments only.
*/
private static Artifactoid adapt( final String depTrailString )
{
requireNonNull( depTrailString );
String[] coordinates = depTrailString.split( ":" );
if ( coordinates.length != 4 && coordinates.length != 5 )
{
throw new IllegalArgumentException( "Bad dep trail string: " + depTrailString );
}
final HashMap<Coordinate, String> map = new HashMap<>();
map.put( Coordinate.GROUP_ID, coordinates[0] );
map.put( Coordinate.ARTIFACT_ID, coordinates[1] );
map.put( Coordinate.TYPE, coordinates[2] );
if ( coordinates.length == 5 )
{
map.put( Coordinate.CLASSIFIER, coordinates[3] );
map.put( Coordinate.BASE_VERSION, coordinates[4] );
}
else
{
map.put( Coordinate.BASE_VERSION, coordinates[3] );
}
return coordinate ->
{
requireNonNull( coordinate );
return map.get( coordinate );
};
}
private static final String ANY = "*";
/**
* Splits the pattern string into tokens, replacing empty tokens with {@link #ANY} for patterns like {@code ::val}
* so it retains the position of token.
*/
private static String[] splitAndTokenize( String pattern )
{
String[] stokens = pattern.split( ":" );
String[] tokens = new String[stokens.length];
for ( int i = 0; i < stokens.length; i++ )
{
String str = stokens[i];
tokens[i] = str != null && !str.isEmpty() ? str : ANY;
}
return tokens;
}
/**
* Compiles pattern string into {@link Pattern}.
*
* TODO: patterns seems NOT documented anywhere, so best we have is source below.
* TODO: patterns in some cases (3, 2 tokens) seems ambiguous, we may need to clean up the specs
*/
private static Pattern compile( String pattern )
{
if ( pattern.startsWith( "!" ) )
{
return new NegativePattern( pattern, compile( pattern.substring( 1 ) ) );
}
else
{
String[] tokens = splitAndTokenize( pattern );
if ( tokens.length < 1 || tokens.length > 5 )
{
throw new IllegalArgumentException( "Invalid pattern: " + pattern );
}
ArrayList<Pattern> patterns = new ArrayList<>( 5 );
if ( tokens.length == 5 )
{
// trivial, full pattern w/ classifier: G:A:T:C:V
patterns.add( toPattern( tokens[0], Coordinate.GROUP_ID ) );
patterns.add( toPattern( tokens[1], Coordinate.ARTIFACT_ID ) );
patterns.add( toPattern( tokens[2], Coordinate.TYPE ) );
patterns.add( toPattern( tokens[3], Coordinate.CLASSIFIER ) );
patterns.add( toPattern( tokens[4], Coordinate.BASE_VERSION ) );
}
else if ( tokens.length == 4 )
{
// trivial, full pattern w/ version or classifier: G:A:T:V or G:A:T:C
patterns.add( toPattern( tokens[0], Coordinate.GROUP_ID ) );
patterns.add( toPattern( tokens[1], Coordinate.ARTIFACT_ID ) );
patterns.add( toPattern( tokens[2], Coordinate.TYPE ) );
patterns.add( toPattern( tokens[3], Coordinate.BASE_VERSION, Coordinate.CLASSIFIER ) );
}
else if ( tokens.length == 3 )
{
// tricky: may be "*:artifact:*" but also "*:war:*"
// *:*:* -> ALL
// *:*:xxx -> TC(xxx)
// *:xxx:* -> AT(xxx)
// *:xxx:yyy -> GA(xxx) + TC(XXX)
// xxx:*:* -> GA(xxx)
// xxx:*:yyy -> G(xxx) + TC(yyy)
// xxx:yyy:* -> G(xxx)+A(yyy)
// xxx:yyy:zzz -> G(xxx)+A(yyy)+T(zzz)
if ( ANY.equals( tokens[0] ) && ANY.equals( tokens[1] ) && ANY.equals( tokens[2] ) )
{
patterns.add( MATCH_ALL_PATTERN );
}
else if ( ANY.equals( tokens[0] ) && ANY.equals( tokens[1] ) )
{
patterns.add( toPattern( pattern, tokens[2], Coordinate.TYPE, Coordinate.CLASSIFIER ) );
}
else if ( ANY.equals( tokens[0] ) && ANY.equals( tokens[2] ) )
{
patterns.add( toPattern( pattern, tokens[1], Coordinate.ARTIFACT_ID, Coordinate.TYPE ) );
}
else if ( ANY.equals( tokens[0] ) )
{
patterns.add( toPattern( pattern, tokens[1], Coordinate.GROUP_ID, Coordinate.ARTIFACT_ID ) );
patterns.add( toPattern( pattern, tokens[2], Coordinate.TYPE, Coordinate.CLASSIFIER ) );
}
else if ( ANY.equals( tokens[1] ) && ANY.equals( tokens[2] ) )
{
patterns.add( toPattern( pattern, tokens[0], Coordinate.GROUP_ID, Coordinate.ARTIFACT_ID ) );
}
else if ( ANY.equals( tokens[1] ) )
{
patterns.add( toPattern( tokens[0], tokens[0], Coordinate.GROUP_ID ) );
patterns.add( toPattern( pattern, tokens[2], Coordinate.TYPE, Coordinate.CLASSIFIER ) );
}
else if ( ANY.equals( tokens[2] ) )
{
patterns.add( toPattern( tokens[0], Coordinate.GROUP_ID ) );
patterns.add( toPattern( tokens[1], Coordinate.ARTIFACT_ID ) );
}
else
{
patterns.add( toPattern( tokens[0], Coordinate.GROUP_ID ) );
patterns.add( toPattern( tokens[1], Coordinate.ARTIFACT_ID ) );
patterns.add( toPattern( tokens[2], Coordinate.TYPE ) );
}
}
else if ( tokens.length == 2 )
{
// tricky: may be "*:artifact" but also "*:war"
// *:* -> ALL
// *:xxx -> GATV(xxx)
// xxx:* -> G(xxx)
// xxx:yyy -> G(xxx)+A(yyy)
if ( ANY.equals( tokens[0] ) && ANY.equals( tokens[1] ) )
{
patterns.add( MATCH_ALL_PATTERN );
}
else if ( ANY.equals( tokens[0] ) )
{
patterns.add( toPattern( pattern, tokens[1],
Coordinate.GROUP_ID, Coordinate.ARTIFACT_ID, Coordinate.TYPE, Coordinate.BASE_VERSION ) );
}
else if ( ANY.equals( tokens[1] ) )
{
patterns.add( toPattern( tokens[0], Coordinate.GROUP_ID ) );
}
else
{
patterns.add( toPattern( tokens[0], Coordinate.GROUP_ID ) );
patterns.add( toPattern( tokens[1], Coordinate.ARTIFACT_ID ) );
}
}
else
{
// trivial: G
patterns.add( toPattern( tokens[0], Coordinate.GROUP_ID ) );
}
// build result if needed and retains pattern string
if ( patterns.size() == 1 )
{
Pattern pat = patterns.get( 0 );
if ( pat == MATCH_ALL_PATTERN )
{
return new MatchAllPattern( pattern );
}
else
{
return pat;
}
}
else
{
return new AndPattern( pattern, patterns.toArray( new Pattern[0] ) );
}
}
}
private static Pattern toPattern( final String token, final Coordinate... coordinates )
{
return toPattern( token, token, coordinates );
}
private static Pattern toPattern( final String pattern, final String token, final Coordinate... coordinates )
{
if ( ANY.equals( token ) )
{
return MATCH_ALL_PATTERN;
}
else
{
EnumSet<Coordinate> coordinateSet = EnumSet.noneOf( Coordinate.class );
coordinateSet.addAll( Arrays.asList( coordinates ) );
return new CoordinateMatchingPattern( pattern, token, coordinateSet );
}
}
private static final Pattern MATCH_ALL_PATTERN = new MatchAllPattern( ANY );
private abstract static class Pattern
{
protected final String pattern;
private Pattern( String pattern )
{
this.pattern = requireNonNull( pattern );
}
public abstract boolean matches( Artifactoid artifact );
@Override
public String toString()
{
return pattern;
}
}
private static class AndPattern extends Pattern
{
private final Pattern[] patterns;
private AndPattern( String pattern, Pattern[] patterns )
{
super( pattern );
this.patterns = patterns;
}
@Override
public boolean matches( Artifactoid artifactoid )
{
for ( Pattern pattern : patterns )
{
if ( !pattern.matches( artifactoid ) )
{
return false;
}
}
return true;
}
}
private static class CoordinateMatchingPattern extends Pattern
{
private final String token;
private final EnumSet<Coordinate> coordinates;
private final boolean containsWildcard;
private final boolean containsAsterisk;
private final VersionRange optionalVersionRange;
private CoordinateMatchingPattern( String pattern, String token, EnumSet<Coordinate> coordinates )
{
super( pattern );
this.token = token;
this.coordinates = coordinates;
this.containsAsterisk = token.contains( "*" );
this.containsWildcard = this.containsAsterisk || token.contains( "?" );
if ( !this.containsWildcard && coordinates.equals( EnumSet.of( Coordinate.BASE_VERSION ) ) && (
token.startsWith( "[" ) || token.startsWith( "(" ) ) )
{
try
{
this.optionalVersionRange = VersionRange.createFromVersionSpec( token );
}
catch ( InvalidVersionSpecificationException e )
{
throw new IllegalArgumentException( "Wrong version spec: " + token, e );
}
}
else
{
this.optionalVersionRange = null;
}
}
@Override
public boolean matches( Artifactoid artifactoid )
{
for ( Coordinate coordinate : coordinates )
{
String value = artifactoid.getCoordinate( coordinate );
if ( Coordinate.BASE_VERSION == coordinate && optionalVersionRange != null )
{
if ( optionalVersionRange.containsVersion( new DefaultArtifactVersion( value ) ) )
{
return true;
}
}
else if ( containsWildcard )
{
if ( match( token, containsAsterisk, value ) )
{
return true;
}
}
else
{
if ( token.equals( value ) )
{
return true;
}
}
}
return false;
}
}
/**
* Matches all input
*/
private static class MatchAllPattern extends Pattern
{
private MatchAllPattern( String pattern )
{
super( pattern );
}
@Override
public boolean matches( Artifactoid artifactoid )
{
return true;
}
}
/**
* Negative pattern
*/
private static class NegativePattern extends Pattern
{
private final Pattern inner;
private NegativePattern( String pattern, Pattern inner )
{
super( pattern );
this.inner = inner;
}
@Override
public boolean matches( Artifactoid artifactoid )
{
return inner.matches( artifactoid );
}
}
// this beauty below must be salvaged
@SuppressWarnings( "InnerAssignment" )
private static boolean match( final String pattern, final boolean containsAsterisk, final String value )
{
char[] patArr = pattern.toCharArray();
char[] strArr = value != null ? value.toCharArray() : new char[0];
int patIdxStart = 0;
int patIdxEnd = patArr.length - 1;
int strIdxStart = 0;
int strIdxEnd = strArr.length - 1;
char ch;
if ( !containsAsterisk )
{
// No '*'s, so we make a shortcut
if ( patIdxEnd != strIdxEnd )
{
return false; // Pattern and string do not have the same size
}
for ( int i = 0; i <= patIdxEnd; i++ )
{
ch = patArr[i];
if ( ch != '?' && ch != strArr[i] )
{
return false; // Character mismatch
}
}
return true; // String matches against pattern
}
if ( patIdxEnd == 0 )
{
return true; // Pattern contains only '*', which matches anything
}
// Process characters before first star
while ( ( ch = patArr[patIdxStart] ) != '*' && strIdxStart <= strIdxEnd )
{
if ( ch != '?' && ch != strArr[strIdxStart] )
{
return false; // Character mismatch
}
patIdxStart++;
strIdxStart++;
}
if ( strIdxStart > strIdxEnd )
{
// All characters in the string are used. Check if only '*'s are
// left in the pattern. If so, we succeeded. Otherwise failure.
for ( int i = patIdxStart; i <= patIdxEnd; i++ )
{
if ( patArr[i] != '*' )
{
return false;
}
}
return true;
}
// Process characters after last star
while ( ( ch = patArr[patIdxEnd] ) != '*' && strIdxStart <= strIdxEnd )
{
if ( ch != '?' && ch != strArr[strIdxEnd] )
{
return false; // Character mismatch
}
patIdxEnd--;
strIdxEnd--;
}
if ( strIdxStart > strIdxEnd )
{
// All characters in the string are used. Check if only '*'s are
// left in the pattern. If so, we succeeded. Otherwise failure.
for ( int i = patIdxStart; i <= patIdxEnd; i++ )
{
if ( patArr[i] != '*' )
{
return false;
}
}
return true;
}
// process pattern between stars. padIdxStart and patIdxEnd point
// always to a '*'.
while ( patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd )
{
int patIdxTmp = -1;
for ( int i = patIdxStart + 1; i <= patIdxEnd; i++ )
{
if ( patArr[i] == '*' )
{
patIdxTmp = i;
break;
}
}
if ( patIdxTmp == patIdxStart + 1 )
{
// Two stars next to each other, skip the first one.
patIdxStart++;
continue;
}
// Find the pattern between padIdxStart & padIdxTmp in str between
// strIdxStart & strIdxEnd
int patLength = ( patIdxTmp - patIdxStart - 1 );
int strLength = ( strIdxEnd - strIdxStart + 1 );
int foundIdx = -1;
strLoop:
for ( int i = 0; i <= strLength - patLength; i++ )
{
for ( int j = 0; j < patLength; j++ )
{
ch = patArr[patIdxStart + j + 1];
if ( ch != '?' && ch != strArr[strIdxStart + i + j] )
{
continue strLoop;
}
}
foundIdx = strIdxStart + i;
break;
}
if ( foundIdx == -1 )
{
return false;
}
patIdxStart = patIdxTmp;
strIdxStart = foundIdx + patLength;
}
// All characters in the string are used. Check if only '*'s are left
// in the pattern. If so, we succeeded. Otherwise failure.
for ( int i = patIdxStart; i <= patIdxEnd; i++ )
{
if ( patArr[i] != '*' )
{
return false;
}
}
return true;
}
}