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<String>();
52  
53      private final List<String> filteredArtifactIds = new ArrayList<String>();
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<String>();
71          final List<String> neg = new ArrayList<String>();
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             final String[] patternTokens = pattern.split( ":" );
195 
196             // fail immediately if pattern tokens outnumber tokens to match
197             boolean matched = patternTokens.length <= tokens.length;
198 
199             for ( int i = 0; matched && i < patternTokens.length; i++ )
200             {
201                 matched = matches( tokens[i], patternTokens[i] );
202             }
203 
204             // case of starting '*' like '*:jar:*'
205             // This really only matches from the end instead.....
206             if ( !matched && patternTokens.length < tokens.length && isFirstPatternWildcard( patternTokens ) )
207             {
208                 matched = true;
209                 int tokenOffset = tokens.length - patternTokens.length;
210                 for ( int i = 0; matched && i < patternTokens.length; i++ )
211                 {
212                     matched = matches( tokens[i + tokenOffset], patternTokens[i] );
213                 }
214             }
215 
216             if ( matched )
217             {
218                 patternsTriggered.add( pattern );
219                 return true;
220             }
221 
222             if ( regionMatch && value.contains( pattern ) )
223             {
224                 patternsTriggered.add( pattern );
225                 return true;
226             }
227 
228         }
229         return false;
230 
231     }
232 
233     private boolean isFirstPatternWildcard( String[] patternTokens )
234     {
235         return patternTokens.length > 0 && "*".equals( patternTokens[0] );
236     }
237 
238     /**
239      * Gets whether the specified token matches the specified pattern segment.
240      * 
241      * @param token the token to check
242      * @param pattern the pattern segment to match, as defined above
243      * @return <code>true</code> if the specified token is matched by the specified pattern segment
244      */
245     private boolean matches( final String token, final String pattern )
246     {
247         boolean matches;
248 
249         // support full wildcard and implied wildcard
250         if ( "*".equals( pattern ) || pattern.length() == 0 )
251         {
252             matches = true;
253         }
254         // support contains wildcard
255         else if ( pattern.startsWith( "*" ) && pattern.endsWith( "*" ) )
256         {
257             final String contains = pattern.substring( 1, pattern.length() - 1 );
258 
259             matches = token.contains( contains );
260         }
261         // support leading wildcard
262         else if ( pattern.startsWith( "*" ) )
263         {
264             final String suffix = pattern.substring( 1, pattern.length() );
265 
266             matches = token.endsWith( suffix );
267         }
268         // support trailing wildcard
269         else if ( pattern.endsWith( "*" ) )
270         {
271             final String prefix = pattern.substring( 0, pattern.length() - 1 );
272 
273             matches = token.startsWith( prefix );
274         }
275         // support wildcards in the middle of a pattern segment
276         else if ( pattern.indexOf( '*' ) > -1 )
277         {
278             String[] parts = pattern.split( "\\*" );
279             int lastPartEnd = -1;
280             boolean match = true;
281 
282             for ( String part : parts )
283             {
284                 int idx = token.indexOf( part );
285                 if ( idx <= lastPartEnd )
286                 {
287                     match = false;
288                     break;
289                 }
290 
291                 lastPartEnd = idx + part.length();
292             }
293 
294             matches = match;
295         }
296         // support versions range
297         else if ( pattern.startsWith( "[" ) || pattern.startsWith( "(" ) )
298         {
299             matches = isVersionIncludedInRange( token, pattern );
300         }
301         // support exact match
302         else
303         {
304             matches = token.equals( pattern );
305         }
306 
307         return matches;
308     }
309 
310     private boolean isVersionIncludedInRange( final String version, final String range )
311     {
312         try
313         {
314             return VersionRange.createFromVersionSpec( range ).containsVersion( new DefaultArtifactVersion( version ) );
315         }
316         catch ( final InvalidVersionSpecificationException e )
317         {
318             return false;
319         }
320     }
321 
322     /** {@inheritDoc} */
323     public void reportMissedCriteria( final Logger logger )
324     {
325         // if there are no patterns, there is nothing to report.
326         if ( !positivePatterns.isEmpty() || !negativePatterns.isEmpty() )
327         {
328             final List<String> missed = new ArrayList<String>();
329             missed.addAll( positivePatterns );
330             missed.addAll( negativePatterns );
331 
332             missed.removeAll( patternsTriggered );
333 
334             if ( !missed.isEmpty() && logger.isWarnEnabled() )
335             {
336                 final StringBuilder buffer = new StringBuilder();
337 
338                 buffer.append( "The following patterns were never triggered in this " );
339                 buffer.append( getFilterDescription() );
340                 buffer.append( ':' );
341 
342                 for ( String pattern : missed )
343                 {
344                     buffer.append( "\no  \'" ).append( pattern ).append( "\'" );
345                 }
346 
347                 buffer.append( "\n" );
348 
349                 logger.warn( buffer.toString() );
350             }
351         }
352     }
353 
354     @Override
355     public String toString()
356     {
357         return "Includes filter:" + getPatternsAsString();
358     }
359 
360     /**
361      * @return pattern as a string.
362      */
363     protected String getPatternsAsString()
364     {
365         final StringBuilder buffer = new StringBuilder();
366         for ( String pattern : positivePatterns )
367         {
368             buffer.append( "\no \'" ).append( pattern ).append( "\'" );
369         }
370 
371         return buffer.toString();
372     }
373 
374     /**
375      * @return description.
376      */
377     protected String getFilterDescription()
378     {
379         return "artifact inclusion filter";
380     }
381 
382     /** {@inheritDoc} */
383     public void reportFilteredArtifacts( final Logger logger )
384     {
385         if ( !filteredArtifactIds.isEmpty() && logger.isDebugEnabled() )
386         {
387             final StringBuilder buffer =
388                 new StringBuilder( "The following artifacts were removed by this " + getFilterDescription() + ": " );
389 
390             for ( String artifactId : filteredArtifactIds )
391             {
392                 buffer.append( '\n' ).append( artifactId );
393             }
394 
395             logger.debug( buffer.toString() );
396         }
397     }
398 
399     /** {@inheritDoc} */
400     public boolean hasMissedCriteria()
401     {
402         // if there are no patterns, there is nothing to report.
403         if ( !positivePatterns.isEmpty() || !negativePatterns.isEmpty() )
404         {
405             final List<String> missed = new ArrayList<String>();
406             missed.addAll( positivePatterns );
407             missed.addAll( negativePatterns );
408 
409             missed.removeAll( patternsTriggered );
410 
411             return !missed.isEmpty();
412         }
413 
414         return false;
415     }
416 
417 }