From 0c6674fae7cf110c4a12d0f0f80e93d61f0be391 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Sat, 11 Jun 2022 15:23:25 +0200 Subject: [PATCH] [MSHARED-1077] Bugfix for classifier in pattern (#26) Parsing of pattern with classifier was busted, that prevented use cases like before [rewrite](https://github.com/apache/maven-common-artifact-filters/commit/4a6f6d6241626ff1f8bce09e9d393126019219f9), that was again busted due early return (not happening since rewrite, but since rewrite we do no support classifiers in patterns). So, here is yet another rewrite, that not only fixes old use cases (w/ classifier) but also makes it even more faster. Last, but not least, new code is now in high level Java, instead of `char[][]` and is self documenting. --- .../filter/PatternExcludesArtifactFilter.java | 14 +- .../filter/PatternIncludesArtifactFilter.java | 1014 +++++++---------- .../AbstractPatternArtifactFilterTest.java | 35 + .../GNPatternIncludesArtifactFilter.java | 917 +++++++++++++++ .../filter/PatternFilterPerfTest.java | 35 + 5 files changed, 1395 insertions(+), 620 deletions(-) create mode 100644 src/test/java/org/apache/maven/shared/artifact/filter/GNPatternIncludesArtifactFilter.java diff --git a/src/main/java/org/apache/maven/shared/artifact/filter/PatternExcludesArtifactFilter.java b/src/main/java/org/apache/maven/shared/artifact/filter/PatternExcludesArtifactFilter.java index a78469a..c975886 100644 --- a/src/main/java/org/apache/maven/shared/artifact/filter/PatternExcludesArtifactFilter.java +++ b/src/main/java/org/apache/maven/shared/artifact/filter/PatternExcludesArtifactFilter.java @@ -53,7 +53,7 @@ public PatternExcludesArtifactFilter( Collection patterns, boolean actTr super( patterns, actTransitively ); } - /** {@inheritDoc} */ + @Override public boolean include( Artifact artifact ) { boolean shouldInclude = !patternMatches( artifact ); @@ -66,21 +66,13 @@ public boolean include( Artifact artifact ) return shouldInclude; } - /** - * {@inheritDoc} - * - * @return a {@link java.lang.String} object. - */ + @Override protected String getFilterDescription() { return "artifact exclusion filter"; } - /** - * {@inheritDoc} - * - * @return a {@link java.lang.String} object. - */ + @Override public String toString() { return "Excludes filter:" + getPatternsAsString(); diff --git a/src/main/java/org/apache/maven/shared/artifact/filter/PatternIncludesArtifactFilter.java b/src/main/java/org/apache/maven/shared/artifact/filter/PatternIncludesArtifactFilter.java index 02e4d1b..b44e6c3 100644 --- a/src/main/java/org/apache/maven/shared/artifact/filter/PatternIncludesArtifactFilter.java +++ b/src/main/java/org/apache/maven/shared/artifact/filter/PatternIncludesArtifactFilter.java @@ -20,15 +20,12 @@ */ 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.Iterator; import java.util.LinkedHashSet; import java.util.List; -import java.util.Map; -import java.util.Objects; import java.util.Set; import org.apache.maven.artifact.Artifact; @@ -38,28 +35,36 @@ 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 Brett Porter * @see StrictPatternIncludesArtifactFilter */ -public class PatternIncludesArtifactFilter - implements ArtifactFilter, StatisticsReportingArtifactFilter +public class PatternIncludesArtifactFilter implements ArtifactFilter, StatisticsReportingArtifactFilter { - /** Holds the set of compiled patterns */ - private final Set patterns; + private static final String SEP = System.lineSeparator(); - /** Holds simple patterns: those that can use direct matching */ - private final Map> simplePatterns; + /** + * Holds the set of compiled patterns + */ + private final Set patterns; - /** Whether the dependency trail should be checked */ + /** + * Whether the dependency trail should be checked + */ private final boolean actTransitively; - /** Set of patterns that have been triggered */ + /** + * Set of patterns that have been triggered + */ private final Set patternsTriggered = new HashSet<>(); - /** Set of artifacts that have been filtered out */ + /** + * Set of artifacts that have been filtered out + */ private final List filteredArtifact = new ArrayList<>(); /** @@ -75,64 +80,25 @@ public PatternIncludesArtifactFilter( final Collection patterns ) /** *

Constructor for PatternIncludesArtifactFilter.

* - * @param patterns The pattern to be used. + * @param patterns The pattern to be used. * @param actTransitively transitive yes/no. */ public PatternIncludesArtifactFilter( final Collection patterns, final boolean actTransitively ) { this.actTransitively = actTransitively; final Set pat = new LinkedHashSet<>(); - Map> simplePat = null; - boolean allPos = true; if ( patterns != null && !patterns.isEmpty() ) { for ( String pattern : patterns ) { - Pattern p = compile( pattern ); - allPos &= !( p instanceof NegativePattern ); pat.add( p ); } } - // If all patterns are positive, we can check for simple patterns - // Simple patterns will match the first tokens and contain no wildcards, - // so we can put them in a map and check them using a simple map lookup. - if ( allPos ) - { - for ( Iterator it = pat.iterator(); it.hasNext(); ) - { - Pattern p = it.next(); - String peq = p.translateEquals(); - if ( peq != null ) - { - int nb = 0; - for ( char ch : peq.toCharArray() ) - { - if ( ch == ':' ) - { - nb++; - } - } - if ( simplePat == null ) - { - simplePat = new HashMap<>(); - } - Map peqm = simplePat.get( nb ); - if ( peqm == null ) - { - peqm = new HashMap<>(); - simplePat.put( nb, peqm ); - } - peqm.put( peq, p ); - it.remove(); - } - } - } - this.simplePatterns = simplePat; this.patterns = pat; } - /** {@inheritDoc} */ + @Override public boolean include( final Artifact artifact ) { final boolean shouldInclude = patternMatches( artifact ); @@ -145,22 +111,9 @@ public boolean include( final Artifact artifact ) return shouldInclude; } - /** - *

patternMatches.

- * - * @param artifact to check for. - * @return true if the match is true false otherwise. - */ protected boolean patternMatches( final Artifact artifact ) { - // Check if the main artifact matches - char[][] artifactGatvCharArray = new char[][] { - emptyOrChars( artifact.getGroupId() ), - emptyOrChars( artifact.getArtifactId() ), - emptyOrChars( artifact.getType() ), - emptyOrChars( artifact.getBaseVersion() ) - }; - Boolean match = match( artifactGatvCharArray ); + Boolean match = match( adapt( artifact ) ); if ( match != null ) { return match; @@ -174,8 +127,8 @@ protected boolean patternMatches( final Artifact artifact ) { for ( String trailItem : depTrail ) { - char[][] depGatvCharArray = tokenizeAndSplit( trailItem ); - match = match( depGatvCharArray ); + Artifactoid artifactoid = adapt( trailItem ); + match = match( artifactoid ); if ( match != null ) { return match; @@ -187,36 +140,11 @@ protected boolean patternMatches( final Artifact artifact ) return false; } - private Boolean match( char[][] gatvCharArray ) + private Boolean match( Artifactoid artifactoid ) { - if ( simplePatterns != null && simplePatterns.size() > 0 ) - { - // We add the parts one by one to the builder - StringBuilder sb = new StringBuilder(); - for ( int i = 0; i < 4; i++ ) - { - if ( i > 0 ) - { - sb.append( ":" ); - } - sb.append( gatvCharArray[i] ); - Map map = simplePatterns.get( i ); - if ( map != null ) - { - // Check if one of the pattern matches - Pattern p = map.get( sb.toString() ); - if ( p != null ) - { - patternsTriggered.add( p ); - return true; - } - } - } - } - // Check all other patterns for ( Pattern pattern : patterns ) { - if ( pattern.matches( gatvCharArray ) ) + if ( pattern.matches( artifactoid ) ) { patternsTriggered.add( pattern ); return !( pattern instanceof NegativePattern ); @@ -236,7 +164,7 @@ protected void addFilteredArtifact( final Artifact artifact ) filteredArtifact.add( artifact ); } - /** {@inheritDoc} */ + @Override public void reportMissedCriteria( final Logger logger ) { // if there are no patterns, there is nothing to report. @@ -255,71 +183,56 @@ public void reportMissedCriteria( final Logger logger ) for ( Pattern pattern : missed ) { - buffer.append( "\no '" ).append( pattern ).append( "'" ); + buffer.append( SEP ) .append( "o '" ).append( pattern ).append( "'" ); } - buffer.append( "\n" ); + buffer.append( SEP ); logger.warn( buffer.toString() ); } } } - /** {@inheritDoc} */ @Override public String toString() { return "Includes filter:" + getPatternsAsString(); } - /** - *

getPatternsAsString.

- * - * @return pattern as a string. - */ protected String getPatternsAsString() { final StringBuilder buffer = new StringBuilder(); for ( Pattern pattern : patterns ) { - buffer.append( "\no '" ).append( pattern ).append( "'" ); + buffer.append( SEP ).append( "o '" ).append( pattern ).append( "'" ); } return buffer.toString(); } - /** - *

getFilterDescription.

- * - * @return description. - */ protected String getFilterDescription() { return "artifact inclusion filter"; } - /** {@inheritDoc} */ + @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() + ": " ); + final StringBuilder buffer = new StringBuilder( + "The following artifacts were removed by this " + getFilterDescription() + ": " ); for ( Artifact artifactId : filteredArtifact ) { - buffer.append( '\n' ).append( artifactId.getId() ); + buffer.append( SEP ).append( artifactId.getId() ); } logger.debug( buffer.toString() ); } } - /** - * {@inheritDoc} - * - * @return a boolean. - */ + @Override public boolean hasMissedCriteria() { // if there are no patterns, there is nothing to report. @@ -327,205 +240,104 @@ public boolean hasMissedCriteria() { final List missed = new ArrayList<>( patterns ); missed.removeAll( patternsTriggered ); - return !missed.isEmpty(); } return false; } - private static final char[] EMPTY = new char[0]; - - private static final char[] ANY = new char[] { '*' }; - - static char[] emptyOrChars( String str ) + private enum Coordinate { - return str != null && str.length() > 0 ? str.toCharArray() : EMPTY; + GROUP_ID, ARTIFACT_ID, TYPE, CLASSIFIER, BASE_VERSION } - static char[] anyOrChars( char[] str ) + private interface Artifactoid { - return str.length > 1 || ( str.length == 1 && str[0] != '*' ) ? str : ANY; + String getCoordinate( Coordinate coordinate ); } - static char[][] tokenizeAndSplit( String pattern ) + private static Artifactoid adapt( final Artifact artifact ) { - String[] stokens = pattern.split( ":" ); - char[][] tokens = new char[ stokens.length ][]; - for ( int i = 0; i < stokens.length; i++ ) + requireNonNull( artifact ); + return coordinate -> { - tokens[i] = emptyOrChars( stokens[i] ); - } - return tokens; - } - - @SuppressWarnings( "InnerAssignment" ) - static boolean match( char[] patArr, char[] strArr, boolean isVersion ) - { - int patIdxStart = 0; - int patIdxEnd = patArr.length - 1; - int strIdxStart = 0; - int strIdxEnd = strArr.length - 1; - char ch; - - boolean containsStar = false; - for ( char aPatArr : patArr ) - { - if ( aPatArr == '*' ) - { - containsStar = true; - break; - } - } - - if ( !containsStar ) - { - if ( isVersion && ( patArr[0] == '[' || patArr[0] == '(' ) ) - { - return isVersionIncludedInRange( String.valueOf( strArr ), String.valueOf( patArr ) ); - } - // 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++ ) + requireNonNull( coordinate ); + switch ( coordinate ) { - if ( patArr[i] != '*' ) - { - return false; - } + 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: } - return true; - } + throw new IllegalArgumentException( "unknown coordinate: " + coordinate ); + }; + } - // Process characters after last star - while ( ( ch = patArr[patIdxEnd] ) != '*' && strIdxStart <= strIdxEnd ) + /** + * 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 ) { - if ( ch != '?' && ch != strArr[strIdxEnd] ) - { - return false; // Character mismatch - } - patIdxEnd--; - strIdxEnd--; + throw new IllegalArgumentException( "Bad dep trail string: " + depTrailString ); } - if ( strIdxStart > strIdxEnd ) + final HashMap 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 ) { - // 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; + map.put( Coordinate.CLASSIFIER, coordinates[3] ); + map.put( Coordinate.BASE_VERSION, coordinates[4] ); } - - // process pattern between stars. padIdxStart and patIdxEnd point - // always to a '*'. - while ( patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd ) + else { - 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; + map.put( Coordinate.BASE_VERSION, coordinates[3] ); } - // 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++ ) + return coordinate -> { - if ( patArr[i] != '*' ) - { - return false; - } - } - return true; + requireNonNull( coordinate ); + return map.get( coordinate ); + }; } - static boolean isVersionIncludedInRange( final String version, final String range ) + 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 ) { - try - { - return VersionRange.createFromVersionSpec( range ).containsVersion( new DefaultArtifactVersion( version ) ); - } - catch ( final InvalidVersionSpecificationException e ) + String[] stokens = pattern.split( ":" ); + String[] tokens = new String[stokens.length]; + for ( int i = 0; i < stokens.length; i++ ) { - return false; + String str = stokens[i]; + tokens[i] = str != null && !str.isEmpty() ? str : ANY; } + return tokens; } - static Pattern compile( String pattern ) + /** + * 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( "!" ) ) { @@ -533,262 +345,166 @@ static Pattern compile( String pattern ) } else { - char[][] stokens = tokenizeAndSplit( pattern ); - char[][] tokens = new char[ stokens.length ][]; - for ( int i = 0; i < stokens.length; i++ ) - { - tokens[i] = anyOrChars( stokens[i] ); - } - if ( tokens.length > 5 ) + String[] tokens = splitAndTokenize( pattern ); + if ( tokens.length < 1 || tokens.length > 5 ) { throw new IllegalArgumentException( "Invalid pattern: " + pattern ); } - // we only accept 5 tokens if the classifier = '*' + + ArrayList patterns = new ArrayList<>( 5 ); + if ( tokens.length == 5 ) { - if ( tokens[3] != ANY ) + // 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/o classifier: G:A:T: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.BASE_VERSION ) ); + } + 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] ) ) { - throw new IllegalArgumentException( "Invalid pattern: " + pattern ); + patterns.add( MATCH_ALL_PATTERN ); } - tokens = new char[][] { tokens[0], tokens[1], tokens[2], tokens[4] }; - } - // - // Check the 4 tokens and build an appropriate Pattern - // Special care needs to be taken if the first or the last part is '*' - // because this allows the '*' to match multiple tokens - // - if ( tokens.length == 1 ) - { - if ( tokens[0] == ANY ) + else if ( ANY.equals( tokens[0] ) && ANY.equals( tokens[1] ) ) + { + patterns.add( new CoordinateMatchingPattern( pattern, tokens[2], + EnumSet.of( Coordinate.TYPE, Coordinate.CLASSIFIER ) ) ); + } + else if ( ANY.equals( tokens[0] ) && ANY.equals( tokens[2] ) ) + { + patterns.add( new CoordinateMatchingPattern( pattern, tokens[1], + EnumSet.of( Coordinate.ARTIFACT_ID, Coordinate.TYPE ) ) ); + } + else if ( ANY.equals( tokens[0] ) ) + { + patterns.add( new CoordinateMatchingPattern( pattern, tokens[1], + EnumSet.of( Coordinate.GROUP_ID, Coordinate.ARTIFACT_ID ) ) ); + patterns.add( new CoordinateMatchingPattern( pattern, tokens[2], + EnumSet.of( Coordinate.TYPE, Coordinate.CLASSIFIER ) ) ); + } + else if ( ANY.equals( tokens[1] ) && ANY.equals( tokens[2] ) ) + { + patterns.add( new CoordinateMatchingPattern( pattern, tokens[0], + EnumSet.of( Coordinate.GROUP_ID, Coordinate.ARTIFACT_ID ) ) ); + } + else if ( ANY.equals( tokens[1] ) ) + { + patterns.add( toPattern( tokens[0], Coordinate.GROUP_ID ) ); + patterns.add( new CoordinateMatchingPattern( pattern, tokens[2], + EnumSet.of( Coordinate.TYPE, Coordinate.CLASSIFIER ) ) ); + } + else if ( ANY.equals( tokens[2] ) ) { - // * - return all( pattern ); + patterns.add( toPattern( tokens[0], Coordinate.GROUP_ID ) ); + patterns.add( toPattern( tokens[1], Coordinate.ARTIFACT_ID ) ); } else { - // [pat0] - return match( pattern, tokens[0], 0 ); + patterns.add( toPattern( tokens[0], Coordinate.GROUP_ID ) ); + patterns.add( toPattern( tokens[1], Coordinate.ARTIFACT_ID ) ); + patterns.add( toPattern( tokens[2], Coordinate.TYPE ) ); } + } - if ( tokens.length == 2 ) + else if ( tokens.length == 2 ) { - if ( tokens[0] == ANY ) + // 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] ) ) { - if ( tokens[1] == ANY ) - { - // *:* - return all( pattern ); - } - else - { - // *:[pat1] - return match( pattern, tokens[1], 0, 3 ); - } + patterns.add( MATCH_ALL_PATTERN ); + } + else if ( ANY.equals( tokens[0] ) ) + { + patterns.add( new CoordinateMatchingPattern( pattern, tokens[1], + EnumSet.of( 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 { - if ( tokens[1] == ANY ) - { - // [pat0]:* - return match( pattern, tokens[0], 0 ); - } - else - { - // [pat0]:[pat1] - Pattern m00 = match( tokens[0], 0 ); - Pattern m11 = match( tokens[1], 1 ); - return and( pattern, m00, m11 ); - } + patterns.add( toPattern( tokens[0], Coordinate.GROUP_ID ) ); + patterns.add( toPattern( tokens[1], Coordinate.ARTIFACT_ID ) ); } } - if ( tokens.length == 3 ) + else + { + // trivial: G + patterns.add( toPattern( tokens[0], Coordinate.GROUP_ID ) ); + } + + // build result if needed and retains pattern string + if ( patterns.size() == 1 ) { - if ( tokens[0] == ANY ) + Pattern pat = patterns.get( 0 ); + if ( pat == MATCH_ALL_PATTERN ) { - if ( tokens[1] == ANY ) - { - if ( tokens[2] == ANY ) - { - // *:*:* - return all( pattern ); - } - else - { - // *:*:[pat2] - return match( pattern, tokens[2], 2, 3 ); - } - } - else - { - if ( tokens[2] == ANY ) - { - // *:[pat1]:* - return match( pattern, tokens[1], 1, 2 ); - } - else - { - // *:[pat1]:[pat2] - Pattern m11 = match( tokens[1], 1 ); - Pattern m12 = match( tokens[1], 2 ); - Pattern m22 = match( tokens[2], 2 ); - Pattern m23 = match( tokens[2], 3 ); - return or( pattern, and( m11, m22 ), and( m12, m23 ) ); - } - } + return new MatchAllPattern( pattern ); } else { - if ( tokens[1] == ANY ) - { - if ( tokens[2] == ANY ) - { - // [pat0]:*:* - return match( pattern, tokens[0], 0, 1 ); - } - else - { - // [pat0]:*:[pat2] - Pattern m00 = match( tokens[0], 0 ); - Pattern m223 = match( tokens[2], 2, 3 ); - return and( pattern, m00, m223 ); - } - } - else - { - if ( tokens[2] == ANY ) - { - // [pat0]:[pat1]:* - Pattern m00 = match( tokens[0], 0 ); - Pattern m11 = match( tokens[1], 1 ); - return and( pattern, m00, m11 ); - } - else - { - // [pat0]:[pat1]:[pat2] - Pattern m00 = match( tokens[0], 0 ); - Pattern m11 = match( tokens[1], 1 ); - Pattern m22 = match( tokens[2], 2 ); - return and( pattern, m00, m11, m22 ); - } - } + return pat; } } - if ( tokens.length == 4 ) + else { - List patterns = new ArrayList<>(); - for ( int i = 0; i < 4; i++ ) - { - if ( tokens[i] != ANY ) - { - patterns.add( match( tokens[i], i ) ); - } - } - return and( pattern, patterns.toArray( new Pattern[0] ) ); + return new AndPattern( pattern, patterns.toArray( new Pattern[0] ) ); } - throw new IllegalStateException(); } } - /** Creates a positional matching pattern */ - private static Pattern match( String pattern, char[] token, int posVal ) + private static Pattern toPattern( final String token, final Coordinate coordinate ) { - return match( pattern, token, posVal, posVal ); - } - - /** Creates a positional matching pattern */ - private static Pattern match( char[] token, int posVal ) - { - return match( "", token, posVal, posVal ); - } - - /** Creates a positional matching pattern */ - private static Pattern match( String pattern, char[] token, int posMin, int posMax ) - { - boolean hasWildcard = false; - for ( char ch : token ) + if ( ANY.equals( token ) ) { - if ( ch == '*' || ch == '?' ) - { - hasWildcard = true; - break; - } - } - if ( hasWildcard || posMax == 3 ) - { - return new PosPattern( pattern, token, posMin, posMax ); + return MATCH_ALL_PATTERN; } else { - return new EqPattern( pattern, token, posMin, posMax ); + return new CoordinateMatchingPattern( token, token, EnumSet.of( coordinate ) ); } } - /** Creates a positional matching pattern */ - private static Pattern match( char[] token, int posMin, int posMax ) - { - return new PosPattern( "", token, posMin, posMax ); - } + private static final Pattern MATCH_ALL_PATTERN = new MatchAllPattern( ANY ); - /** Creates an AND pattern */ - private static Pattern and( String pattern, Pattern... patterns ) + private abstract static class Pattern { - return new AndPattern( pattern, patterns ); - } - - /** Creates an AND pattern */ - private static Pattern and( Pattern... patterns ) - { - return and( "", patterns ); - } + protected final String pattern; - /** Creates an OR pattern */ - private static Pattern or( String pattern, Pattern... patterns ) - { - return new OrPattern( pattern, patterns ); - } - - /** Creates an OR pattern */ - private static Pattern or( Pattern... patterns ) - { - return or( "", patterns ); - } - - /** Creates a match-all pattern */ - private static Pattern all( String pattern ) - { - return new MatchAllPattern( pattern ); - } - - /** - * Abstract class for patterns - */ - abstract static class Pattern - { - private final String pattern; - - Pattern( String pattern ) - { - this.pattern = Objects.requireNonNull( pattern ); - } - - public abstract boolean matches( char[][] parts ); - - /** - * Returns a string containing a fixed artifact gatv coordinates - * or null if the pattern can not be translated. - */ - public String translateEquals() + private Pattern( String pattern ) { - return null; + this.pattern = requireNonNull( pattern ); } - /** - * Check if the this pattern is a fixed pattern on the specified pos. - */ - protected String translateEquals( int pos ) - { - return null; - } + public abstract boolean matches( Artifactoid artifact ); @Override public String toString() @@ -797,78 +513,93 @@ public String toString() } } - /** - * Simple pattern which performs a logical AND between one or more patterns. - */ - static class AndPattern extends Pattern + private static class AndPattern extends Pattern { private final Pattern[] patterns; - AndPattern( String pattern, Pattern[] patterns ) + private AndPattern( String pattern, Pattern[] patterns ) { super( pattern ); this.patterns = patterns; } @Override - public boolean matches( char[][] parts ) + public boolean matches( Artifactoid artifactoid ) { for ( Pattern pattern : patterns ) { - if ( !pattern.matches( parts ) ) + if ( !pattern.matches( artifactoid ) ) { return false; } } return true; } + } - @Override - public String translateEquals() + private static class CoordinateMatchingPattern extends Pattern + { + private final String token; + + private final EnumSet coordinates; + + private final boolean containsWildcard; + + private final boolean containsAsterisk; + + private final VersionRange optionalVersionRange; + + private CoordinateMatchingPattern( String pattern, String token, EnumSet coordinates ) { - String[] strings = new String[ patterns.length ]; - for ( int i = 0; i < patterns.length; i++ ) - { - strings[i] = patterns[i].translateEquals( i ); - if ( strings[i] == null ) + 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 { - return null; + this.optionalVersionRange = VersionRange.createFromVersionSpec( token ); } - } - StringBuilder sb = new StringBuilder(); - for ( int i = 0; i < strings.length; i++ ) - { - if ( i > 0 ) + catch ( InvalidVersionSpecificationException e ) { - sb.append( ":" ); + throw new IllegalArgumentException( "Wrong version spec: " + token, e ); } - sb.append( strings[i] ); } - return sb.toString(); - } - } - - /** - * Simple pattern which performs a logical OR between one or more patterns. - */ - static class OrPattern extends Pattern - { - private final Pattern[] patterns; - - OrPattern( String pattern, Pattern[] patterns ) - { - super( pattern ); - this.patterns = patterns; + else + { + this.optionalVersionRange = null; + } } @Override - public boolean matches( char[][] parts ) + public boolean matches( Artifactoid artifactoid ) { - for ( Pattern pattern : patterns ) + for ( Coordinate coordinate : coordinates ) { - if ( pattern.matches( parts ) ) + String value = artifactoid.getCoordinate( coordinate ); + if ( Coordinate.BASE_VERSION == coordinate && optionalVersionRange != null ) + { + if ( optionalVersionRange.containsVersion( new DefaultArtifactVersion( value ) ) ) + { + return true; + } + } + else if ( containsWildcard ) { - return true; + if ( match( token, containsAsterisk, value ) ) + { + return true; + } + } + else + { + if ( token.equals( value ) ) + { + return true; + } } } return false; @@ -876,119 +607,184 @@ public boolean matches( char[][] parts ) } /** - * A positional matching pattern, to check if a token in the gatv coordinates - * having a position between posMin and posMax (both inclusives) can match - * the pattern. + * Matches all input */ - static class PosPattern extends Pattern + private static class MatchAllPattern extends Pattern { - private final char[] patternCharArray; - private final int posMin; - private final int posMax; - - PosPattern( String pattern, char[] patternCharArray, int posMin, int posMax ) + private MatchAllPattern( String pattern ) { super( pattern ); - this.patternCharArray = patternCharArray; - this.posMin = posMin; - this.posMax = posMax; } @Override - public boolean matches( char[][] parts ) + public boolean matches( Artifactoid artifactoid ) { - for ( int i = posMin; i <= posMax; i++ ) - { - if ( match( patternCharArray, parts[i], i == 3 ) ) - { - return true; - } - } - return false; + return true; } } /** - * Looks for an exact match in the gatv coordinates between - * posMin and posMax (both inclusives) + * Negative pattern */ - static class EqPattern extends Pattern + private static class NegativePattern extends Pattern { - private final char[] token; - private final int posMin; - private final int posMax; + private final Pattern inner; - EqPattern( String pattern, char[] patternCharArray, int posMin, int posMax ) + private NegativePattern( String pattern, Pattern inner ) { super( pattern ); - this.token = patternCharArray; - this.posMin = posMin; - this.posMax = posMax; + this.inner = inner; } @Override - public boolean matches( char[][] parts ) + 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.toCharArray(); + int patIdxStart = 0; + int patIdxEnd = patArr.length - 1; + int strIdxStart = 0; + int strIdxEnd = strArr.length - 1; + char ch; + + if ( !containsAsterisk ) { - for ( int i = posMin; i <= posMax; i++ ) + // No '*'s, so we make a shortcut + if ( patIdxEnd != strIdxEnd ) { - if ( Arrays.equals( token, parts[i] ) ) + 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 true; + return false; // Character mismatch } } - return false; + return true; // String matches against pattern } - @Override - public String translateEquals() + if ( patIdxEnd == 0 ) { - return translateEquals( 0 ); + return true; // Pattern contains only '*', which matches anything } - public String translateEquals( int pos ) + // Process characters before first star + while ( ( ch = patArr[patIdxStart] ) != '*' && strIdxStart <= strIdxEnd ) { - return posMin == pos && posMax == pos - && ( pos < 3 || ( token[0] != '[' && token[0] != '(' ) ) - ? String.valueOf( token ) : null; + if ( ch != '?' && ch != strArr[strIdxStart] ) + { + return false; // Character mismatch + } + patIdxStart++; + strIdxStart++; } - - } - - /** - * Matches all input - */ - static class MatchAllPattern extends Pattern - { - MatchAllPattern( String pattern ) + if ( strIdxStart > strIdxEnd ) { - super( pattern ); + // 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; } - @Override - public boolean matches( char[][] parts ) + // 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; } - } - - /** - * Negative pattern - */ - static class NegativePattern extends Pattern - { - private final Pattern inner; - NegativePattern( String pattern, Pattern inner ) + // process pattern between stars. padIdxStart and patIdxEnd point + // always to a '*'. + while ( patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd ) { - super( pattern ); - this.inner = inner; + 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; } - @Override - public boolean matches( char[][] parts ) + // 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++ ) { - return inner.matches( parts ); + if ( patArr[i] != '*' ) + { + return false; + } } + return true; } - } diff --git a/src/test/java/org/apache/maven/shared/artifact/filter/AbstractPatternArtifactFilterTest.java b/src/test/java/org/apache/maven/shared/artifact/filter/AbstractPatternArtifactFilterTest.java index cb4ad8d..2a6d914 100644 --- a/src/test/java/org/apache/maven/shared/artifact/filter/AbstractPatternArtifactFilterTest.java +++ b/src/test/java/org/apache/maven/shared/artifact/filter/AbstractPatternArtifactFilterTest.java @@ -544,4 +544,39 @@ public void testWithVersionRange() assertTrue( filter.include( artifact ) ); } } + + @Test + public void testmassembly955() + { + Artifact artifact1 = mock( Artifact.class ); + when( artifact1.getGroupId() ).thenReturn( "org.python" ); + when( artifact1.getArtifactId() ).thenReturn( "jython-standalone" ); + when( artifact1.getType() ).thenReturn( "jar" ); + when( artifact1.getBaseVersion() ).thenReturn( "1.0" ); + + Artifact artifact2 = mock( Artifact.class ); + when( artifact2.getGroupId() ).thenReturn( "org.teiid" ); + when( artifact2.getArtifactId() ).thenReturn( "teiid" ); + when( artifact2.getType() ).thenReturn( "jar" ); + when( artifact2.hasClassifier() ).thenReturn( true ); + when( artifact2.getClassifier() ).thenReturn( "jdbc" ); + when( artifact2.getBaseVersion() ).thenReturn( "1.0" ); + + final List patterns = new ArrayList<>(); + patterns.add( "org.teiid:teiid:*:jdbc:*" ); + patterns.add( "org.python:jython-standalone" ); + + final ArtifactFilter filter = createFilter( patterns ); + + if ( isInclusionNotExpected() ) + { + assertFalse( filter.include( artifact1 ) ); + assertFalse( filter.include( artifact2 ) ); + } + else + { + assertTrue( filter.include( artifact1 ) ); + assertTrue( filter.include( artifact2 ) ); + } + } } diff --git a/src/test/java/org/apache/maven/shared/artifact/filter/GNPatternIncludesArtifactFilter.java b/src/test/java/org/apache/maven/shared/artifact/filter/GNPatternIncludesArtifactFilter.java new file mode 100644 index 0000000..669a60a --- /dev/null +++ b/src/test/java/org/apache/maven/shared/artifact/filter/GNPatternIncludesArtifactFilter.java @@ -0,0 +1,917 @@ +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.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +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; + +/** + * TODO: include in maven-artifact in future + * + * @author Brett Porter + * @see StrictPatternIncludesArtifactFilter + */ +public class GNPatternIncludesArtifactFilter + implements ArtifactFilter, StatisticsReportingArtifactFilter +{ + /** Holds the set of compiled patterns */ + private final Set patterns; + + /** Whether the dependency trail should be checked */ + private final boolean actTransitively; + + /** Set of patterns that have been triggered */ + private final Set patternsTriggered = new HashSet<>(); + + /** Set of artifacts that have been filtered out */ + private final List filteredArtifact = new ArrayList<>(); + + /** + *

Constructor for PatternIncludesArtifactFilter.

+ * + * @param patterns The pattern to be used. + */ + public GNPatternIncludesArtifactFilter( final Collection patterns ) + { + this( patterns, false ); + } + + /** + *

Constructor for PatternIncludesArtifactFilter.

+ * + * @param patterns The pattern to be used. + * @param actTransitively transitive yes/no. + */ + public GNPatternIncludesArtifactFilter( final Collection patterns, final boolean actTransitively ) + { + this.actTransitively = actTransitively; + final Set pat = new LinkedHashSet<>(); + if ( patterns != null && !patterns.isEmpty() ) + { + for ( String pattern : patterns ) + { + + Pattern p = compile( pattern ); + pat.add( p ); + } + } + this.patterns = pat; + } + + /** {@inheritDoc} */ + public boolean include( final Artifact artifact ) + { + final boolean shouldInclude = patternMatches( artifact ); + + if ( !shouldInclude ) + { + addFilteredArtifact( artifact ); + } + + return shouldInclude; + } + + /** + *

patternMatches.

+ * + * @param artifact to check for. + * @return true if the match is true false otherwise. + */ + protected boolean patternMatches( final Artifact artifact ) + { + // Check if the main artifact matches + char[][] artifactGatvCharArray = new char[][] { + emptyOrChars( artifact.getGroupId() ), + emptyOrChars( artifact.getArtifactId() ), + emptyOrChars( artifact.getType() ), + emptyOrChars( artifact.getClassifier() ), + emptyOrChars( artifact.getBaseVersion() ) + }; + Boolean match = match( artifactGatvCharArray ); + if ( match != null ) + { + return match; + } + + if ( actTransitively ) + { + final List depTrail = artifact.getDependencyTrail(); + + if ( depTrail != null && depTrail.size() > 1 ) + { + for ( String trailItem : depTrail ) + { + char[][] depGatvCharArray = tokenizeAndSplit( trailItem ); + match = match( depGatvCharArray ); + if ( match != null ) + { + return match; + } + } + } + } + + return false; + } + + private Boolean match( char[][] gatvCharArray ) + { + for ( Pattern pattern : patterns ) + { + if ( pattern.matches( gatvCharArray ) ) + { + patternsTriggered.add( pattern ); + return !( pattern instanceof NegativePattern ); + } + } + + return null; + } + + /** + *

addFilteredArtifact.

+ * + * @param artifact add artifact to the filtered artifacts list. + */ + protected void addFilteredArtifact( final Artifact artifact ) + { + filteredArtifact.add( artifact ); + } + + /** {@inheritDoc} */ + public void reportMissedCriteria( final Logger logger ) + { + // if there are no patterns, there is nothing to report. + if ( !patterns.isEmpty() ) + { + final List 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( "\no '" ).append( pattern ).append( "'" ); + } + + buffer.append( "\n" ); + + logger.warn( buffer.toString() ); + } + } + } + + /** {@inheritDoc} */ + @Override + public String toString() + { + return "Includes filter:" + getPatternsAsString(); + } + + /** + *

getPatternsAsString.

+ * + * @return pattern as a string. + */ + protected String getPatternsAsString() + { + final StringBuilder buffer = new StringBuilder(); + for ( Pattern pattern : patterns ) + { + buffer.append( "\no '" ).append( pattern ).append( "'" ); + } + + return buffer.toString(); + } + + /** + *

getFilterDescription.

+ * + * @return description. + */ + protected String getFilterDescription() + { + return "artifact inclusion filter"; + } + + /** {@inheritDoc} */ + 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( '\n' ).append( artifactId.getId() ); + } + + logger.debug( buffer.toString() ); + } + } + + /** + * {@inheritDoc} + * + * @return a boolean. + */ + public boolean hasMissedCriteria() + { + // if there are no patterns, there is nothing to report. + if ( !patterns.isEmpty() ) + { + final List missed = new ArrayList<>( patterns ); + missed.removeAll( patternsTriggered ); + + return !missed.isEmpty(); + } + + return false; + } + + private static final char[] EMPTY = new char[0]; + + private static final char[] ANY = new char[] { '*' }; + + static char[] emptyOrChars( String str ) + { + return str != null && str.length() > 0 ? str.toCharArray() : EMPTY; + } + + static char[] anyOrChars( char[] str ) + { + return str.length > 1 || ( str.length == 1 && str[0] != '*' ) ? str : ANY; + } + + static char[][] tokenizeAndSplit( String pattern ) + { + String[] stokens = pattern.split( ":" ); + char[][] tokens = new char[ stokens.length ][]; + for ( int i = 0; i < stokens.length; i++ ) + { + tokens[i] = emptyOrChars( stokens[i] ); + } + return tokens; + } + + @SuppressWarnings( "InnerAssignment" ) + static boolean match( char[] patArr, char[] strArr, boolean isVersion ) + { + int patIdxStart = 0; + int patIdxEnd = patArr.length - 1; + int strIdxStart = 0; + int strIdxEnd = strArr.length - 1; + char ch; + + boolean containsStar = false; + for ( char aPatArr : patArr ) + { + if ( aPatArr == '*' ) + { + containsStar = true; + break; + } + } + + if ( !containsStar ) + { + if ( isVersion && ( patArr[0] == '[' || patArr[0] == '(' ) ) + { + return isVersionIncludedInRange( String.valueOf( strArr ), String.valueOf( patArr ) ); + } + // 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; + } + + static boolean isVersionIncludedInRange( final String version, final String range ) + { + try + { + return VersionRange.createFromVersionSpec( range ).containsVersion( new DefaultArtifactVersion( version ) ); + } + catch ( final InvalidVersionSpecificationException e ) + { + return false; + } + } + + static Pattern compile( String pattern ) + { + if ( pattern.startsWith( "!" ) ) + { + return new NegativePattern( pattern, compile( pattern.substring( 1 ) ) ); + } + else + { + char[][] stokens = tokenizeAndSplit( pattern ); + char[][] tokens = new char[ stokens.length ][]; + for ( int i = 0; i < stokens.length; i++ ) + { + tokens[i] = anyOrChars( stokens[i] ); + } + if ( tokens.length > 5 ) + { + throw new IllegalArgumentException( "Invalid pattern: " + pattern ); + } + // + // Check the tokens and build an appropriate Pattern + // Special care needs to be taken if the first or the last part is '*' + // because this allows the '*' to match multiple tokens + // + if ( tokens.length == 1 ) + { + if ( tokens[0] == ANY ) + { + // * + return all( pattern ); + } + else + { + // [pat0] + return match( pattern, tokens[0], 0 ); + } + } + if ( tokens.length == 2 ) + { + if ( tokens[0] == ANY ) + { + if ( tokens[1] == ANY ) + { + // *:* + return all( pattern ); + } + else + { + // *:[pat1] + return match( pattern, tokens[1], 0, 3 ); + } + } + else + { + if ( tokens[1] == ANY ) + { + // [pat0]:* + return match( pattern, tokens[0], 0 ); + } + else + { + // [pat0]:[pat1] + Pattern m00 = match( tokens[0], 0 ); + Pattern m11 = match( tokens[1], 1 ); + return and( pattern, m00, m11 ); + } + } + } + if ( tokens.length == 3 ) + { + if ( tokens[0] == ANY ) + { + if ( tokens[1] == ANY ) + { + if ( tokens[2] == ANY ) + { + // *:*:* + return all( pattern ); + } + else + { + // *:*:[pat2] + return match( pattern, tokens[2], 2, 3 ); + } + } + else + { + if ( tokens[2] == ANY ) + { + // *:[pat1]:* + return match( pattern, tokens[1], 1, 2 ); + } + else + { + // *:[pat1]:[pat2] + Pattern m11 = match( tokens[1], 1 ); + Pattern m12 = match( tokens[1], 2 ); + Pattern m22 = match( tokens[2], 2 ); + Pattern m23 = match( tokens[2], 3 ); + return or( pattern, and( m11, m22 ), and( m12, m23 ) ); + } + } + } + else + { + if ( tokens[1] == ANY ) + { + if ( tokens[2] == ANY ) + { + // [pat0]:*:* + return match( pattern, tokens[0], 0, 1 ); + } + else + { + // [pat0]:*:[pat2] + Pattern m00 = match( tokens[0], 0 ); + Pattern m223 = match( tokens[2], 2, 3 ); + return and( pattern, m00, m223 ); + } + } + else + { + if ( tokens[2] == ANY ) + { + // [pat0]:[pat1]:* + Pattern m00 = match( tokens[0], 0 ); + Pattern m11 = match( tokens[1], 1 ); + return and( pattern, m00, m11 ); + } + else + { + // [pat0]:[pat1]:[pat2] + Pattern m00 = match( tokens[0], 0 ); + Pattern m11 = match( tokens[1], 1 ); + Pattern m22 = match( tokens[2], 2 ); + return and( pattern, m00, m11, m22 ); + } + } + } + } + if ( tokens.length >= 4 ) + { + List patterns = new ArrayList<>(); + for ( int i = 0; i < tokens.length; i++ ) + { + if ( tokens[i] != ANY ) + { + patterns.add( match( tokens[i], i ) ); + } + } + return and( pattern, patterns.toArray( new Pattern[0] ) ); + } + throw new IllegalStateException(); + } + } + + /** Creates a positional matching pattern */ + private static Pattern match( String pattern, char[] token, int posVal ) + { + return match( pattern, token, posVal, posVal ); + } + + /** Creates a positional matching pattern */ + private static Pattern match( char[] token, int posVal ) + { + return match( "", token, posVal, posVal ); + } + + /** Creates a positional matching pattern */ + private static Pattern match( String pattern, char[] token, int posMin, int posMax ) + { + boolean hasWildcard = false; + for ( char ch : token ) + { + if ( ch == '*' || ch == '?' ) + { + hasWildcard = true; + break; + } + } + if ( hasWildcard || posMax == 4 ) + { + return new PosPattern( pattern, token, posMin, posMax ); + } + else + { + return new EqPattern( pattern, token, posMin, posMax ); + } + } + + /** Creates a positional matching pattern */ + private static Pattern match( char[] token, int posMin, int posMax ) + { + return new PosPattern( "", token, posMin, posMax ); + } + + /** Creates an AND pattern */ + private static Pattern and( String pattern, Pattern... patterns ) + { + return new AndPattern( pattern, patterns ); + } + + /** Creates an AND pattern */ + private static Pattern and( Pattern... patterns ) + { + return and( "", patterns ); + } + + /** Creates an OR pattern */ + private static Pattern or( String pattern, Pattern... patterns ) + { + return new OrPattern( pattern, patterns ); + } + + /** Creates an OR pattern */ + private static Pattern or( Pattern... patterns ) + { + return or( "", patterns ); + } + + /** Creates a match-all pattern */ + private static Pattern all( String pattern ) + { + return new MatchAllPattern( pattern ); + } + + /** + * Abstract class for patterns + */ + abstract static class Pattern + { + private final String pattern; + + Pattern( String pattern ) + { + this.pattern = Objects.requireNonNull( pattern ); + } + + public abstract boolean matches( char[][] parts ); + + /** + * Returns a string containing a fixed artifact gatv coordinates + * or null if the pattern can not be translated. + */ + public String translateEquals() + { + return null; + } + + /** + * Check if the this pattern is a fixed pattern on the specified pos. + */ + protected String translateEquals( int pos ) + { + return null; + } + + @Override + public String toString() + { + return pattern; + } + } + + /** + * Simple pattern which performs a logical AND between one or more patterns. + */ + static class AndPattern extends Pattern + { + private final Pattern[] patterns; + + AndPattern( String pattern, Pattern[] patterns ) + { + super( pattern ); + this.patterns = patterns; + } + + @Override + public boolean matches( char[][] parts ) + { + for ( Pattern pattern : patterns ) + { + if ( !pattern.matches( parts ) ) + { + return false; + } + } + return true; + } + + @Override + public String translateEquals() + { + String[] strings = new String[ patterns.length ]; + for ( int i = 0; i < patterns.length; i++ ) + { + strings[i] = patterns[i].translateEquals( i ); + if ( strings[i] == null ) + { + return null; + } + } + StringBuilder sb = new StringBuilder(); + for ( int i = 0; i < strings.length; i++ ) + { + if ( i > 0 ) + { + sb.append( ":" ); + } + sb.append( strings[i] ); + } + return sb.toString(); + } + } + + /** + * Simple pattern which performs a logical OR between one or more patterns. + */ + static class OrPattern extends Pattern + { + private final Pattern[] patterns; + + OrPattern( String pattern, Pattern[] patterns ) + { + super( pattern ); + this.patterns = patterns; + } + + @Override + public boolean matches( char[][] parts ) + { + for ( Pattern pattern : patterns ) + { + if ( pattern.matches( parts ) ) + { + return true; + } + } + return false; + } + } + + /** + * A positional matching pattern, to check if a token in the gatv coordinates + * having a position between posMin and posMax (both inclusives) can match + * the pattern. + */ + static class PosPattern extends Pattern + { + private final char[] patternCharArray; + private final int posMin; + private final int posMax; + + PosPattern( String pattern, char[] patternCharArray, int posMin, int posMax ) + { + super( pattern ); + this.patternCharArray = patternCharArray; + this.posMin = posMin; + this.posMax = posMax; + } + + @Override + public boolean matches( char[][] parts ) + { + for ( int i = posMin; i <= posMax; i++ ) + { + if ( match( patternCharArray, parts[i], i == 4 ) ) + { + return true; + } + } + return false; + } + } + + /** + * Looks for an exact match in the gatv coordinates between + * posMin and posMax (both inclusives) + */ + static class EqPattern extends Pattern + { + private final char[] token; + private final int posMin; + private final int posMax; + + EqPattern( String pattern, char[] patternCharArray, int posMin, int posMax ) + { + super( pattern ); + this.token = patternCharArray; + this.posMin = posMin; + this.posMax = posMax; + } + + @Override + public boolean matches( char[][] parts ) + { + for ( int i = posMin; i <= posMax; i++ ) + { + if ( Arrays.equals( token, parts[i] ) ) + { + return true; + } + } + return false; + } + + @Override + public String translateEquals() + { + return translateEquals( 0 ); + } + + public String translateEquals( int pos ) + { + return posMin == pos && posMax == pos + && ( pos < 3 || ( token[0] != '[' && token[0] != '(' ) ) + ? String.valueOf( token ) : null; + } + + } + + /** + * Matches all input + */ + static class MatchAllPattern extends Pattern + { + MatchAllPattern( String pattern ) + { + super( pattern ); + } + + @Override + public boolean matches( char[][] parts ) + { + return true; + } + } + + /** + * Negative pattern + */ + static class NegativePattern extends Pattern + { + private final Pattern inner; + + NegativePattern( String pattern, Pattern inner ) + { + super( pattern ); + this.inner = inner; + } + + @Override + public boolean matches( char[][] parts ) + { + return inner.matches( parts ); + } + } + +} diff --git a/src/test/java/org/apache/maven/shared/artifact/filter/PatternFilterPerfTest.java b/src/test/java/org/apache/maven/shared/artifact/filter/PatternFilterPerfTest.java index 6cbe7a5..7ee0013 100644 --- a/src/test/java/org/apache/maven/shared/artifact/filter/PatternFilterPerfTest.java +++ b/src/test/java/org/apache/maven/shared/artifact/filter/PatternFilterPerfTest.java @@ -75,6 +75,35 @@ public void setup() } + @State(Scope.Benchmark) + static public class GNPatternState { + + @Param({ + "groupId:artifact-00,groupId:artifact-01,groupId:artifact-02,groupId:artifact-03,groupId:artifact-04,groupId:artifact-05,groupId:artifact-06,groupId:artifact-07,groupId:artifact-08,groupId:artifact-09", + "groupId:artifact-99", + "groupId:artifact-*", + "*:artifact-99", + "*:artifact-*", + "*:artifact-*:*", + "*:artifact-99:*", + }) + public String patterns; + + ArtifactFilter filter; + Artifact artifact; + + @Setup(Level.Invocation) + public void setup() + { + filter = new GNPatternIncludesArtifactFilter( Arrays.asList( patterns.split( "," ) ) ); + artifact = new DefaultArtifact( + "groupId", "artifact-99", "1.0", "runtime", + "jar", "", null + ); + } + + } + @State(Scope.Benchmark) static public class NewPatternState { @@ -111,6 +140,12 @@ public boolean newPatternTest(NewPatternState state ) return state.filter.include( state.artifact ); } + @Benchmark + public boolean gnPatternTest(GNPatternState state ) + { + return state.filter.include( state.artifact ); + } + @Benchmark public boolean oldPatternTest(OldPatternState state ) {