View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.shared.artifact.filter;
20  
21  import java.util.ArrayList;
22  import java.util.Arrays;
23  import java.util.Collection;
24  import java.util.EnumSet;
25  import java.util.HashMap;
26  import java.util.HashSet;
27  import java.util.LinkedHashSet;
28  import java.util.List;
29  import java.util.Set;
30  
31  import org.apache.maven.artifact.Artifact;
32  import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
33  import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
34  import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
35  import org.apache.maven.artifact.versioning.VersionRange;
36  import org.slf4j.Logger;
37  
38  import static java.util.Objects.requireNonNull;
39  
40  /**
41   * TODO: include in maven-artifact in future
42   *
43   * @author <a href="mailto:brett@apache.org">Brett Porter</a>
44   * @see StrictPatternIncludesArtifactFilter
45   */
46  public class PatternIncludesArtifactFilter implements ArtifactFilter, StatisticsReportingArtifactFilter {
47      private static final String SEP = System.lineSeparator();
48  
49      /**
50       * Holds the set of compiled patterns
51       */
52      private final Set<Pattern> patterns;
53  
54      /**
55       * Whether the dependency trail should be checked
56       */
57      private final boolean actTransitively;
58  
59      /**
60       * Set of patterns that have been triggered
61       */
62      private final Set<Pattern> patternsTriggered = new HashSet<>();
63  
64      /**
65       * Set of artifacts that have been filtered out
66       */
67      private final List<Artifact> filteredArtifact = new ArrayList<>();
68  
69      /**
70       * <p>Constructor for PatternIncludesArtifactFilter.</p>
71       *
72       * @param patterns The pattern to be used.
73       */
74      public PatternIncludesArtifactFilter(final Collection<String> patterns) {
75          this(patterns, false);
76      }
77  
78      /**
79       * <p>Constructor for PatternIncludesArtifactFilter.</p>
80       *
81       * @param patterns        The pattern to be used.
82       * @param actTransitively transitive yes/no.
83       */
84      public PatternIncludesArtifactFilter(final Collection<String> patterns, final boolean actTransitively) {
85          this.actTransitively = actTransitively;
86          final Set<Pattern> pat = new LinkedHashSet<>();
87          if (patterns != null && !patterns.isEmpty()) {
88              for (String pattern : patterns) {
89                  Pattern p = compile(pattern);
90                  pat.add(p);
91              }
92          }
93          this.patterns = pat;
94      }
95  
96      @Override
97      public boolean include(final Artifact artifact) {
98          final boolean shouldInclude = patternMatches(artifact);
99  
100         if (!shouldInclude) {
101             addFilteredArtifact(artifact);
102         }
103 
104         return shouldInclude;
105     }
106 
107     protected boolean patternMatches(final Artifact artifact) {
108         Boolean match = match(adapt(artifact));
109         if (match != null) {
110             return match;
111         }
112 
113         if (actTransitively) {
114             final List<String> depTrail = artifact.getDependencyTrail();
115 
116             if (depTrail != null && depTrail.size() > 1) {
117                 for (String trailItem : depTrail) {
118                     Artifactoid artifactoid = adapt(trailItem);
119                     match = match(artifactoid);
120                     if (match != null) {
121                         return match;
122                     }
123                 }
124             }
125         }
126 
127         return false;
128     }
129 
130     private Boolean match(Artifactoid artifactoid) {
131         for (Pattern pattern : patterns) {
132             if (pattern.matches(artifactoid)) {
133                 patternsTriggered.add(pattern);
134                 return !(pattern instanceof NegativePattern);
135             }
136         }
137 
138         return null;
139     }
140 
141     /**
142      * <p>addFilteredArtifact.</p>
143      *
144      * @param artifact add artifact to the filtered artifacts list.
145      */
146     protected void addFilteredArtifact(final Artifact artifact) {
147         filteredArtifact.add(artifact);
148     }
149 
150     @Override
151     public void reportMissedCriteria(final Logger logger) {
152         // if there are no patterns, there is nothing to report.
153         if (!patterns.isEmpty()) {
154             final List<Pattern> missed = new ArrayList<>(patterns);
155             missed.removeAll(patternsTriggered);
156 
157             if (!missed.isEmpty() && logger.isWarnEnabled()) {
158                 final StringBuilder buffer = new StringBuilder();
159 
160                 buffer.append("The following patterns were never triggered in this ");
161                 buffer.append(getFilterDescription());
162                 buffer.append(':');
163 
164                 for (Pattern pattern : missed) {
165                     buffer.append(SEP).append("o  '").append(pattern).append("'");
166                 }
167 
168                 buffer.append(SEP);
169 
170                 logger.warn(buffer.toString());
171             }
172         }
173     }
174 
175     @Override
176     public String toString() {
177         return "Includes filter:" + getPatternsAsString();
178     }
179 
180     protected String getPatternsAsString() {
181         final StringBuilder buffer = new StringBuilder();
182         for (Pattern pattern : patterns) {
183             buffer.append(SEP).append("o '").append(pattern).append("'");
184         }
185 
186         return buffer.toString();
187     }
188 
189     protected String getFilterDescription() {
190         return "artifact inclusion filter";
191     }
192 
193     @Override
194     public void reportFilteredArtifacts(final Logger logger) {
195         if (!filteredArtifact.isEmpty() && logger.isDebugEnabled()) {
196             final StringBuilder buffer =
197                     new StringBuilder("The following artifacts were removed by this " + getFilterDescription() + ": ");
198 
199             for (Artifact artifactId : filteredArtifact) {
200                 buffer.append(SEP).append(artifactId.getId());
201             }
202 
203             logger.debug(buffer.toString());
204         }
205     }
206 
207     @Override
208     public boolean hasMissedCriteria() {
209         // if there are no patterns, there is nothing to report.
210         if (!patterns.isEmpty()) {
211             final List<Pattern> missed = new ArrayList<>(patterns);
212             missed.removeAll(patternsTriggered);
213             return !missed.isEmpty();
214         }
215 
216         return false;
217     }
218 
219     private enum Coordinate {
220         GROUP_ID,
221         ARTIFACT_ID,
222         TYPE,
223         CLASSIFIER,
224         BASE_VERSION
225     }
226 
227     private interface Artifactoid {
228         String getCoordinate(Coordinate coordinate);
229     }
230 
231     private static Artifactoid adapt(final Artifact artifact) {
232         requireNonNull(artifact);
233         return coordinate -> {
234             requireNonNull(coordinate);
235             switch (coordinate) {
236                 case GROUP_ID:
237                     return artifact.getGroupId();
238                 case ARTIFACT_ID:
239                     return artifact.getArtifactId();
240                 case BASE_VERSION:
241                     return artifact.getBaseVersion();
242                 case CLASSIFIER:
243                     return artifact.hasClassifier() ? artifact.getClassifier() : null;
244                 case TYPE:
245                     return artifact.getType();
246                 default:
247             }
248             throw new IllegalArgumentException("unknown coordinate: " + coordinate);
249         };
250     }
251 
252     /**
253      * Parses elements of {@link Artifact#getDependencyTrail()} list, they are either {@code G:A:T:V} or if artifact
254      * has classifier {@code G:A:T:C:V}, so strictly 4 or 5 segments only.
255      */
256     private static Artifactoid adapt(final String depTrailString) {
257         requireNonNull(depTrailString);
258         String[] coordinates = depTrailString.split(":");
259         if (coordinates.length != 4 && coordinates.length != 5) {
260             throw new IllegalArgumentException("Bad dep trail string: " + depTrailString);
261         }
262         final HashMap<Coordinate, String> map = new HashMap<>();
263         map.put(Coordinate.GROUP_ID, coordinates[0]);
264         map.put(Coordinate.ARTIFACT_ID, coordinates[1]);
265         map.put(Coordinate.TYPE, coordinates[2]);
266         if (coordinates.length == 5) {
267             map.put(Coordinate.CLASSIFIER, coordinates[3]);
268             map.put(Coordinate.BASE_VERSION, coordinates[4]);
269         } else {
270             map.put(Coordinate.BASE_VERSION, coordinates[3]);
271         }
272 
273         return coordinate -> {
274             requireNonNull(coordinate);
275             return map.get(coordinate);
276         };
277     }
278 
279     private static final String ANY = "*";
280 
281     /**
282      * Splits the pattern string into tokens, replacing empty tokens with {@link #ANY} for patterns like {@code ::val}
283      * so it retains the position of token.
284      */
285     private static String[] splitAndTokenize(String pattern) {
286         String[] stokens = pattern.split(":");
287         String[] tokens = new String[stokens.length];
288         for (int i = 0; i < stokens.length; i++) {
289             String str = stokens[i];
290             tokens[i] = str != null && !str.isEmpty() ? str : ANY;
291         }
292         return tokens;
293     }
294 
295     /**
296      * Compiles pattern string into {@link Pattern}.
297      *
298      * TODO: patterns seems NOT documented anywhere, so best we have is source below.
299      * TODO: patterns in some cases (3, 2 tokens) seems ambiguous, we may need to clean up the specs
300      */
301     private static Pattern compile(String pattern) {
302         if (pattern.startsWith("!")) {
303             return new NegativePattern(pattern, compile(pattern.substring(1)));
304         } else {
305             String[] tokens = splitAndTokenize(pattern);
306             if (tokens.length < 1 || tokens.length > 5) {
307                 throw new IllegalArgumentException("Invalid pattern: " + pattern);
308             }
309 
310             ArrayList<Pattern> patterns = new ArrayList<>(5);
311 
312             if (tokens.length == 5) {
313                 // trivial, full pattern w/ classifier: G:A:T:C:V
314                 patterns.add(toPattern(tokens[0], Coordinate.GROUP_ID));
315                 patterns.add(toPattern(tokens[1], Coordinate.ARTIFACT_ID));
316                 patterns.add(toPattern(tokens[2], Coordinate.TYPE));
317                 patterns.add(toPattern(tokens[3], Coordinate.CLASSIFIER));
318                 patterns.add(toPattern(tokens[4], Coordinate.BASE_VERSION));
319             } else if (tokens.length == 4) {
320                 // trivial, full pattern w/ version or classifier: G:A:T:V or G:A:T:C
321                 patterns.add(toPattern(tokens[0], Coordinate.GROUP_ID));
322                 patterns.add(toPattern(tokens[1], Coordinate.ARTIFACT_ID));
323                 patterns.add(toPattern(tokens[2], Coordinate.TYPE));
324                 patterns.add(toPattern(tokens[3], Coordinate.BASE_VERSION, Coordinate.CLASSIFIER));
325             } else if (tokens.length == 3) {
326                 // tricky: may be "*:artifact:*" but also "*:war:*"
327 
328                 // *:*:* -> ALL
329                 // *:*:xxx -> TC(xxx)
330                 // *:xxx:* -> AT(xxx)
331                 // *:xxx:yyy -> GA(xxx) + TC(XXX)
332                 // xxx:*:* -> GA(xxx)
333                 // xxx:*:yyy -> G(xxx) + TC(yyy)
334                 // xxx:yyy:* -> G(xxx)+A(yyy)
335                 // xxx:yyy:zzz -> G(xxx)+A(yyy)+T(zzz)
336                 if (ANY.equals(tokens[0]) && ANY.equals(tokens[1]) && ANY.equals(tokens[2])) {
337                     patterns.add(MATCH_ALL_PATTERN);
338                 } else if (ANY.equals(tokens[0]) && ANY.equals(tokens[1])) {
339                     patterns.add(toPattern(pattern, tokens[2], Coordinate.TYPE, Coordinate.CLASSIFIER));
340                 } else if (ANY.equals(tokens[0]) && ANY.equals(tokens[2])) {
341                     patterns.add(toPattern(pattern, tokens[1], Coordinate.ARTIFACT_ID, Coordinate.TYPE));
342                 } else if (ANY.equals(tokens[0])) {
343                     patterns.add(toPattern(pattern, tokens[1], Coordinate.GROUP_ID, Coordinate.ARTIFACT_ID));
344                     patterns.add(toPattern(pattern, tokens[2], Coordinate.TYPE, Coordinate.CLASSIFIER));
345                 } else if (ANY.equals(tokens[1]) && ANY.equals(tokens[2])) {
346                     patterns.add(toPattern(pattern, tokens[0], Coordinate.GROUP_ID, Coordinate.ARTIFACT_ID));
347                 } else if (ANY.equals(tokens[1])) {
348                     patterns.add(toPattern(tokens[0], tokens[0], Coordinate.GROUP_ID));
349                     patterns.add(toPattern(pattern, tokens[2], Coordinate.TYPE, Coordinate.CLASSIFIER));
350                 } else if (ANY.equals(tokens[2])) {
351                     patterns.add(toPattern(tokens[0], Coordinate.GROUP_ID));
352                     patterns.add(toPattern(tokens[1], Coordinate.ARTIFACT_ID));
353                 } else {
354                     patterns.add(toPattern(tokens[0], Coordinate.GROUP_ID));
355                     patterns.add(toPattern(tokens[1], Coordinate.ARTIFACT_ID));
356                     patterns.add(toPattern(tokens[2], Coordinate.TYPE));
357                 }
358 
359             } else if (tokens.length == 2) {
360                 // tricky: may be "*:artifact" but also "*:war"
361                 // *:* -> ALL
362                 // *:xxx -> GATV(xxx)
363                 // xxx:* -> G(xxx)
364                 // xxx:yyy -> G(xxx)+A(yyy)
365 
366                 if (ANY.equals(tokens[0]) && ANY.equals(tokens[1])) {
367                     patterns.add(MATCH_ALL_PATTERN);
368                 } else if (ANY.equals(tokens[0])) {
369                     patterns.add(toPattern(
370                             pattern,
371                             tokens[1],
372                             Coordinate.GROUP_ID,
373                             Coordinate.ARTIFACT_ID,
374                             Coordinate.TYPE,
375                             Coordinate.BASE_VERSION));
376                 } else if (ANY.equals(tokens[1])) {
377                     patterns.add(toPattern(tokens[0], Coordinate.GROUP_ID));
378                 } else {
379                     patterns.add(toPattern(tokens[0], Coordinate.GROUP_ID));
380                     patterns.add(toPattern(tokens[1], Coordinate.ARTIFACT_ID));
381                 }
382             } else {
383                 // trivial: G
384                 patterns.add(toPattern(tokens[0], Coordinate.GROUP_ID));
385             }
386 
387             // build result if needed and retains pattern string
388             if (patterns.size() == 1) {
389                 Pattern pat = patterns.get(0);
390                 if (pat == MATCH_ALL_PATTERN) {
391                     return new MatchAllPattern(pattern);
392                 } else {
393                     return pat;
394                 }
395             } else {
396                 return new AndPattern(pattern, patterns.toArray(new Pattern[0]));
397             }
398         }
399     }
400 
401     private static Pattern toPattern(final String token, final Coordinate... coordinates) {
402         return toPattern(token, token, coordinates);
403     }
404 
405     private static Pattern toPattern(final String pattern, final String token, final Coordinate... coordinates) {
406         if (ANY.equals(token)) {
407             return MATCH_ALL_PATTERN;
408         } else {
409             EnumSet<Coordinate> coordinateSet = EnumSet.noneOf(Coordinate.class);
410             coordinateSet.addAll(Arrays.asList(coordinates));
411             return new CoordinateMatchingPattern(pattern, token, coordinateSet);
412         }
413     }
414 
415     private static final Pattern MATCH_ALL_PATTERN = new MatchAllPattern(ANY);
416 
417     private abstract static class Pattern {
418         protected final String pattern;
419 
420         private Pattern(String pattern) {
421             this.pattern = requireNonNull(pattern);
422         }
423 
424         public abstract boolean matches(Artifactoid artifact);
425 
426         @Override
427         public String toString() {
428             return pattern;
429         }
430     }
431 
432     private static class AndPattern extends Pattern {
433         private final Pattern[] patterns;
434 
435         private AndPattern(String pattern, Pattern[] patterns) {
436             super(pattern);
437             this.patterns = patterns;
438         }
439 
440         @Override
441         public boolean matches(Artifactoid artifactoid) {
442             for (Pattern pattern : patterns) {
443                 if (!pattern.matches(artifactoid)) {
444                     return false;
445                 }
446             }
447             return true;
448         }
449     }
450 
451     private static class CoordinateMatchingPattern extends Pattern {
452         private final String token;
453 
454         private final EnumSet<Coordinate> coordinates;
455 
456         private final boolean containsWildcard;
457 
458         private final boolean containsAsterisk;
459 
460         private final VersionRange optionalVersionRange;
461 
462         private CoordinateMatchingPattern(String pattern, String token, EnumSet<Coordinate> coordinates) {
463             super(pattern);
464             this.token = token;
465             this.coordinates = coordinates;
466             this.containsAsterisk = token.contains("*");
467             this.containsWildcard = this.containsAsterisk || token.contains("?");
468             if (!this.containsWildcard
469                     && coordinates.equals(EnumSet.of(Coordinate.BASE_VERSION))
470                     && (token.startsWith("[") || token.startsWith("("))) {
471                 try {
472                     this.optionalVersionRange = VersionRange.createFromVersionSpec(token);
473                 } catch (InvalidVersionSpecificationException e) {
474                     throw new IllegalArgumentException("Wrong version spec: " + token, e);
475                 }
476             } else {
477                 this.optionalVersionRange = null;
478             }
479         }
480 
481         @Override
482         public boolean matches(Artifactoid artifactoid) {
483             for (Coordinate coordinate : coordinates) {
484                 String value = artifactoid.getCoordinate(coordinate);
485                 if (Coordinate.BASE_VERSION == coordinate && optionalVersionRange != null) {
486                     if (optionalVersionRange.containsVersion(new DefaultArtifactVersion(value))) {
487                         return true;
488                     }
489                 } else if (containsWildcard) {
490                     if (match(token, containsAsterisk, value)) {
491                         return true;
492                     }
493                 } else {
494                     if (token.equals(value)) {
495                         return true;
496                     }
497                 }
498             }
499             return false;
500         }
501     }
502 
503     /**
504      * Matches all input
505      */
506     private static class MatchAllPattern extends Pattern {
507         private MatchAllPattern(String pattern) {
508             super(pattern);
509         }
510 
511         @Override
512         public boolean matches(Artifactoid artifactoid) {
513             return true;
514         }
515     }
516 
517     /**
518      * Negative pattern
519      */
520     private static class NegativePattern extends Pattern {
521         private final Pattern inner;
522 
523         private NegativePattern(String pattern, Pattern inner) {
524             super(pattern);
525             this.inner = inner;
526         }
527 
528         @Override
529         public boolean matches(Artifactoid artifactoid) {
530             return inner.matches(artifactoid);
531         }
532     }
533 
534     // this beauty below must be salvaged
535 
536     @SuppressWarnings("InnerAssignment")
537     private static boolean match(final String pattern, final boolean containsAsterisk, final String value) {
538         char[] patArr = pattern.toCharArray();
539         char[] strArr = value != null ? value.toCharArray() : new char[0];
540         int patIdxStart = 0;
541         int patIdxEnd = patArr.length - 1;
542         int strIdxStart = 0;
543         int strIdxEnd = strArr.length - 1;
544         char ch;
545 
546         if (!containsAsterisk) {
547             // No '*'s, so we make a shortcut
548             if (patIdxEnd != strIdxEnd) {
549                 return false; // Pattern and string do not have the same size
550             }
551             for (int i = 0; i <= patIdxEnd; i++) {
552                 ch = patArr[i];
553                 if (ch != '?' && ch != strArr[i]) {
554                     return false; // Character mismatch
555                 }
556             }
557             return true; // String matches against pattern
558         }
559 
560         if (patIdxEnd == 0) {
561             return true; // Pattern contains only '*', which matches anything
562         }
563 
564         // Process characters before first star
565         while ((ch = patArr[patIdxStart]) != '*' && strIdxStart <= strIdxEnd) {
566             if (ch != '?' && ch != strArr[strIdxStart]) {
567                 return false; // Character mismatch
568             }
569             patIdxStart++;
570             strIdxStart++;
571         }
572         if (strIdxStart > strIdxEnd) {
573             // All characters in the string are used. Check if only '*'s are
574             // left in the pattern. If so, we succeeded. Otherwise failure.
575             for (int i = patIdxStart; i <= patIdxEnd; i++) {
576                 if (patArr[i] != '*') {
577                     return false;
578                 }
579             }
580             return true;
581         }
582 
583         // Process characters after last star
584         while ((ch = patArr[patIdxEnd]) != '*' && strIdxStart <= strIdxEnd) {
585             if (ch != '?' && ch != strArr[strIdxEnd]) {
586                 return false; // Character mismatch
587             }
588             patIdxEnd--;
589             strIdxEnd--;
590         }
591         if (strIdxStart > strIdxEnd) {
592             // All characters in the string are used. Check if only '*'s are
593             // left in the pattern. If so, we succeeded. Otherwise failure.
594             for (int i = patIdxStart; i <= patIdxEnd; i++) {
595                 if (patArr[i] != '*') {
596                     return false;
597                 }
598             }
599             return true;
600         }
601 
602         // process pattern between stars. padIdxStart and patIdxEnd point
603         // always to a '*'.
604         while (patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd) {
605             int patIdxTmp = -1;
606             for (int i = patIdxStart + 1; i <= patIdxEnd; i++) {
607                 if (patArr[i] == '*') {
608                     patIdxTmp = i;
609                     break;
610                 }
611             }
612             if (patIdxTmp == patIdxStart + 1) {
613                 // Two stars next to each other, skip the first one.
614                 patIdxStart++;
615                 continue;
616             }
617             // Find the pattern between padIdxStart & padIdxTmp in str between
618             // strIdxStart & strIdxEnd
619             int patLength = (patIdxTmp - patIdxStart - 1);
620             int strLength = (strIdxEnd - strIdxStart + 1);
621             int foundIdx = -1;
622             strLoop:
623             for (int i = 0; i <= strLength - patLength; i++) {
624                 for (int j = 0; j < patLength; j++) {
625                     ch = patArr[patIdxStart + j + 1];
626                     if (ch != '?' && ch != strArr[strIdxStart + i + j]) {
627                         continue strLoop;
628                     }
629                 }
630 
631                 foundIdx = strIdxStart + i;
632                 break;
633             }
634 
635             if (foundIdx == -1) {
636                 return false;
637             }
638 
639             patIdxStart = patIdxTmp;
640             strIdxStart = foundIdx + patLength;
641         }
642 
643         // All characters in the string are used. Check if only '*'s are left
644         // in the pattern. If so, we succeeded. Otherwise failure.
645         for (int i = patIdxStart; i <= patIdxEnd; i++) {
646             if (patArr[i] != '*') {
647                 return false;
648             }
649         }
650         return true;
651     }
652 }