View Javadoc
1   package org.apache.maven.shared.artifact.filter;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.util.ArrayList;
23  import java.util.Collection;
24  import java.util.HashSet;
25  import java.util.List;
26  import java.util.Set;
27  
28  import org.apache.maven.artifact.Artifact;
29  import org.apache.maven.artifact.ArtifactUtils;
30  import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
31  import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
32  import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
33  import org.apache.maven.artifact.versioning.VersionRange;
34  import org.codehaus.plexus.logging.Logger;
35  
36  /**
37   * TODO: include in maven-artifact in future
38   * 
39   * @author <a href="mailto:brett@apache.org">Brett Porter</a>
40   * @see StrictPatternIncludesArtifactFilter
41   */
42  public class PatternIncludesArtifactFilter
43      implements ArtifactFilter, StatisticsReportingArtifactFilter
44  {
45      private final List<String> positivePatterns;
46  
47      private final List<String> negativePatterns;
48  
49      private final boolean actTransitively;
50  
51      private final Set<String> patternsTriggered = new HashSet<>();
52  
53      private final List<String> filteredArtifactIds = new ArrayList<>();
54  
55      /**
56       * @param patterns The pattern to be used.
57       */
58      public PatternIncludesArtifactFilter( final Collection<String> patterns )
59      {
60          this( patterns, false );
61      }
62  
63      /**
64       * @param patterns The pattern to be used.
65       * @param actTransitively transitive yes/no.
66       */
67      public PatternIncludesArtifactFilter( final Collection<String> patterns, final boolean actTransitively )
68      {
69          this.actTransitively = actTransitively;
70          final List<String> pos = new ArrayList<>();
71          final List<String> neg = new ArrayList<>();
72          if ( patterns != null && !patterns.isEmpty() )
73          {
74              for ( String pattern : patterns )
75              {
76                  if ( pattern.startsWith( "!" ) )
77                  {
78                      neg.add( pattern.substring( 1 ) );
79                  }
80                  else
81                  {
82                      pos.add( pattern );
83                  }
84              }
85          }
86  
87          positivePatterns = pos;
88          negativePatterns = neg;
89      }
90  
91      /** {@inheritDoc} */
92      public boolean include( final Artifact artifact )
93      {
94          final boolean shouldInclude = patternMatches( artifact );
95  
96          if ( !shouldInclude )
97          {
98              addFilteredArtifactId( artifact.getId() );
99          }
100 
101         return shouldInclude;
102     }
103 
104     /**
105      * @param artifact to check for.
106      * @return true if the match is true false otherwise.
107      */
108     protected boolean patternMatches( final Artifact artifact )
109     {
110         return positiveMatch( artifact ) == Boolean.TRUE || negativeMatch( artifact ) == Boolean.FALSE;
111     }
112 
113     /**
114      * @param artifactId add artifact to the filtered artifacts list.
115      */
116     protected void addFilteredArtifactId( final String artifactId )
117     {
118         filteredArtifactIds.add( artifactId );
119     }
120 
121     private Boolean negativeMatch( final Artifact artifact )
122     {
123         if ( negativePatterns == null || negativePatterns.isEmpty() )
124         {
125             return null;
126         }
127         else
128         {
129             return match( artifact, negativePatterns );
130         }
131     }
132 
133     /**
134      * @param artifact check for positive match.
135      * @return true/false.
136      */
137     protected Boolean positiveMatch( final Artifact artifact )
138     {
139         if ( positivePatterns == null || positivePatterns.isEmpty() )
140         {
141             return null;
142         }
143         else
144         {
145             return match( artifact, positivePatterns );
146         }
147     }
148 
149     private boolean match( final Artifact artifact, final List<String> patterns )
150     {
151         final String shortId = ArtifactUtils.versionlessKey( artifact );
152         final String id = artifact.getDependencyConflictId();
153         final String wholeId = artifact.getId();
154 
155         if ( matchAgainst( wholeId, patterns, false ) )
156         {
157             return true;
158         }
159 
160         if ( matchAgainst( id, patterns, false ) )
161         {
162             return true;
163         }
164 
165         if ( matchAgainst( shortId, patterns, false ) )
166         {
167             return true;
168         }
169 
170         if ( actTransitively )
171         {
172             final List<String> depTrail = artifact.getDependencyTrail();
173 
174             if ( depTrail != null && depTrail.size() > 1 )
175             {
176                 for ( String trailItem : depTrail )
177                 {
178                     if ( matchAgainst( trailItem, patterns, true ) )
179                     {
180                         return true;
181                     }
182                 }
183             }
184         }
185 
186         return false;
187     }
188 
189     private boolean matchAgainst( final String value, final List<String> patterns, final boolean regionMatch )
190     {
191         final String[] tokens = value.split( ":" );
192         for ( String pattern : patterns )
193         {
194             String[] patternTokens = pattern.split( ":" );
195             
196             if ( patternTokens.length == 5 && tokens.length < 5 )
197             {
198                 // 4th element is the classifier
199                 if ( !"*".equals( patternTokens[3] ) )
200                 {
201                     // classifier required, cannot be a match
202                     return false;
203                 }
204                 patternTokens = new String[] { patternTokens[0], patternTokens[1], patternTokens[2], patternTokens[4] };
205             }
206 
207             // fail immediately if pattern tokens outnumber tokens to match
208             boolean matched = patternTokens.length <= tokens.length;
209 
210             for ( int i = 0; matched && i < patternTokens.length; i++ )
211             {
212                 matched = matches( tokens[i], patternTokens[i] );
213             }
214 
215             // case of starting '*' like '*:jar:*'
216             // This really only matches from the end instead.....
217             if ( !matched && patternTokens.length < tokens.length && isFirstPatternWildcard( patternTokens ) )
218             {
219                 matched = true;
220                 int tokenOffset = tokens.length - patternTokens.length;
221                 for ( int i = 0; matched && i < patternTokens.length; i++ )
222                 {
223                     matched = matches( tokens[i + tokenOffset], patternTokens[i] );
224                 }
225             }
226 
227             if ( matched )
228             {
229                 patternsTriggered.add( pattern );
230                 return true;
231             }
232 
233             if ( regionMatch && value.contains( pattern ) )
234             {
235                 patternsTriggered.add( pattern );
236                 return true;
237             }
238 
239         }
240         return false;
241 
242     }
243 
244     private boolean isFirstPatternWildcard( String[] patternTokens )
245     {
246         return patternTokens.length > 0 && "*".equals( patternTokens[0] );
247     }
248 
249     /**
250      * Gets whether the specified token matches the specified pattern segment.
251      * 
252      * @param token the token to check
253      * @param pattern the pattern segment to match, as defined above
254      * @return <code>true</code> if the specified token is matched by the specified pattern segment
255      */
256     private boolean matches( final String token, final String pattern )
257     {
258         boolean matches;
259 
260         // support full wildcard and implied wildcard
261         if ( "*".equals( pattern ) || pattern.length() == 0 )
262         {
263             matches = true;
264         }
265         // support contains wildcard
266         else if ( pattern.startsWith( "*" ) && pattern.endsWith( "*" ) )
267         {
268             final String contains = pattern.substring( 1, pattern.length() - 1 );
269 
270             matches = token.contains( contains );
271         }
272         // support leading wildcard
273         else if ( pattern.startsWith( "*" ) )
274         {
275             final String suffix = pattern.substring( 1 );
276 
277             matches = token.endsWith( suffix );
278         }
279         // support trailing wildcard
280         else if ( pattern.endsWith( "*" ) )
281         {
282             final String prefix = pattern.substring( 0, pattern.length() - 1 );
283 
284             matches = token.startsWith( prefix );
285         }
286         // support wildcards in the middle of a pattern segment
287         else if ( pattern.indexOf( '*' ) > -1 )
288         {
289             String[] parts = pattern.split( "\\*" );
290             int lastPartEnd = -1;
291             boolean match = true;
292 
293             for ( String part : parts )
294             {
295                 int idx = token.indexOf( part );
296                 if ( idx <= lastPartEnd )
297                 {
298                     match = false;
299                     break;
300                 }
301 
302                 lastPartEnd = idx + part.length();
303             }
304 
305             matches = match;
306         }
307         // support versions range
308         else if ( pattern.startsWith( "[" ) || pattern.startsWith( "(" ) )
309         {
310             matches = isVersionIncludedInRange( token, pattern );
311         }
312         // support exact match
313         else
314         {
315             matches = token.equals( pattern );
316         }
317 
318         return matches;
319     }
320 
321     private boolean isVersionIncludedInRange( final String version, final String range )
322     {
323         try
324         {
325             return VersionRange.createFromVersionSpec( range ).containsVersion( new DefaultArtifactVersion( version ) );
326         }
327         catch ( final InvalidVersionSpecificationException e )
328         {
329             return false;
330         }
331     }
332 
333     /** {@inheritDoc} */
334     public void reportMissedCriteria( final Logger logger )
335     {
336         // if there are no patterns, there is nothing to report.
337         if ( !positivePatterns.isEmpty() || !negativePatterns.isEmpty() )
338         {
339             final List<String> missed = new ArrayList<>();
340             missed.addAll( positivePatterns );
341             missed.addAll( negativePatterns );
342 
343             missed.removeAll( patternsTriggered );
344 
345             if ( !missed.isEmpty() && logger.isWarnEnabled() )
346             {
347                 final StringBuilder buffer = new StringBuilder();
348 
349                 buffer.append( "The following patterns were never triggered in this " );
350                 buffer.append( getFilterDescription() );
351                 buffer.append( ':' );
352 
353                 for ( String pattern : missed )
354                 {
355                     buffer.append( "\no  '" ).append( pattern ).append( "'" );
356                 }
357 
358                 buffer.append( "\n" );
359 
360                 logger.warn( buffer.toString() );
361             }
362         }
363     }
364 
365     @Override
366     public String toString()
367     {
368         return "Includes filter:" + getPatternsAsString();
369     }
370 
371     /**
372      * @return pattern as a string.
373      */
374     protected String getPatternsAsString()
375     {
376         final StringBuilder buffer = new StringBuilder();
377         for ( String pattern : positivePatterns )
378         {
379             buffer.append( "\no '" ).append( pattern ).append( "'" );
380         }
381 
382         return buffer.toString();
383     }
384 
385     /**
386      * @return description.
387      */
388     protected String getFilterDescription()
389     {
390         return "artifact inclusion filter";
391     }
392 
393     /** {@inheritDoc} */
394     public void reportFilteredArtifacts( final Logger logger )
395     {
396         if ( !filteredArtifactIds.isEmpty() && logger.isDebugEnabled() )
397         {
398             final StringBuilder buffer =
399                 new StringBuilder( "The following artifacts were removed by this " + getFilterDescription() + ": " );
400 
401             for ( String artifactId : filteredArtifactIds )
402             {
403                 buffer.append( '\n' ).append( artifactId );
404             }
405 
406             logger.debug( buffer.toString() );
407         }
408     }
409 
410     /** {@inheritDoc} */
411     public boolean hasMissedCriteria()
412     {
413         // if there are no patterns, there is nothing to report.
414         if ( !positivePatterns.isEmpty() || !negativePatterns.isEmpty() )
415         {
416             final List<String> missed = new ArrayList<>();
417             missed.addAll( positivePatterns );
418             missed.addAll( negativePatterns );
419 
420             missed.removeAll( patternsTriggered );
421 
422             return !missed.isEmpty();
423         }
424 
425         return false;
426     }
427 
428 }