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.HashSet;
25  import java.util.LinkedHashSet;
26  import java.util.List;
27  import java.util.Objects;
28  import java.util.Set;
29  
30  import org.apache.maven.artifact.Artifact;
31  import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
32  import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
33  import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
34  import org.apache.maven.artifact.versioning.VersionRange;
35  import org.slf4j.Logger;
36  
37  /**
38   * TODO: include in maven-artifact in future
39   *
40   * @author <a href="mailto:brett@apache.org">Brett Porter</a>
41   * @see StrictPatternIncludesArtifactFilter
42   */
43  public class GNPatternIncludesArtifactFilter implements ArtifactFilter, StatisticsReportingArtifactFilter {
44      /** Holds the set of compiled patterns */
45      private final Set<Pattern> patterns;
46  
47      /** Whether the dependency trail should be checked */
48      private final boolean actTransitively;
49  
50      /** Set of patterns that have been triggered */
51      private final Set<Pattern> patternsTriggered = new HashSet<>();
52  
53      /** Set of artifacts that have been filtered out */
54      private final List<Artifact> filteredArtifact = new ArrayList<>();
55  
56      /**
57       * <p>Constructor for PatternIncludesArtifactFilter.</p>
58       *
59       * @param patterns The pattern to be used.
60       */
61      public GNPatternIncludesArtifactFilter(final Collection<String> patterns) {
62          this(patterns, false);
63      }
64  
65      /**
66       * <p>Constructor for PatternIncludesArtifactFilter.</p>
67       *
68       * @param patterns The pattern to be used.
69       * @param actTransitively transitive yes/no.
70       */
71      public GNPatternIncludesArtifactFilter(final Collection<String> patterns, final boolean actTransitively) {
72          this.actTransitively = actTransitively;
73          final Set<Pattern> pat = new LinkedHashSet<>();
74          if (patterns != null && !patterns.isEmpty()) {
75              for (String pattern : patterns) {
76  
77                  Pattern p = compile(pattern);
78                  pat.add(p);
79              }
80          }
81          this.patterns = pat;
82      }
83  
84      /** {@inheritDoc} */
85      public boolean include(final Artifact artifact) {
86          final boolean shouldInclude = patternMatches(artifact);
87  
88          if (!shouldInclude) {
89              addFilteredArtifact(artifact);
90          }
91  
92          return shouldInclude;
93      }
94  
95      /**
96       * <p>patternMatches.</p>
97       *
98       * @param artifact to check for.
99       * @return true if the match is true false otherwise.
100      */
101     protected boolean patternMatches(final Artifact artifact) {
102         // Check if the main artifact matches
103         char[][] artifactGatvCharArray = new char[][] {
104             emptyOrChars(artifact.getGroupId()),
105             emptyOrChars(artifact.getArtifactId()),
106             emptyOrChars(artifact.getType()),
107             emptyOrChars(artifact.getClassifier()),
108             emptyOrChars(artifact.getBaseVersion())
109         };
110         Boolean match = match(artifactGatvCharArray);
111         if (match != null) {
112             return match;
113         }
114 
115         if (actTransitively) {
116             final List<String> depTrail = artifact.getDependencyTrail();
117 
118             if (depTrail != null && depTrail.size() > 1) {
119                 for (String trailItem : depTrail) {
120                     char[][] depGatvCharArray = tokenizeAndSplit(trailItem);
121                     match = match(depGatvCharArray);
122                     if (match != null) {
123                         return match;
124                     }
125                 }
126             }
127         }
128 
129         return false;
130     }
131 
132     private Boolean match(char[][] gatvCharArray) {
133         for (Pattern pattern : patterns) {
134             if (pattern.matches(gatvCharArray)) {
135                 patternsTriggered.add(pattern);
136                 return !(pattern instanceof NegativePattern);
137             }
138         }
139 
140         return null;
141     }
142 
143     /**
144      * <p>addFilteredArtifact.</p>
145      *
146      * @param artifact add artifact to the filtered artifacts list.
147      */
148     protected void addFilteredArtifact(final Artifact artifact) {
149         filteredArtifact.add(artifact);
150     }
151 
152     /** {@inheritDoc} */
153     public void reportMissedCriteria(final Logger logger) {
154         // if there are no patterns, there is nothing to report.
155         if (!patterns.isEmpty()) {
156             final List<Pattern> missed = new ArrayList<>(patterns);
157             missed.removeAll(patternsTriggered);
158 
159             if (!missed.isEmpty() && logger.isWarnEnabled()) {
160                 final StringBuilder buffer = new StringBuilder();
161 
162                 buffer.append("The following patterns were never triggered in this ");
163                 buffer.append(getFilterDescription());
164                 buffer.append(':');
165 
166                 for (Pattern pattern : missed) {
167                     buffer.append("\no  '").append(pattern).append("'");
168                 }
169 
170                 buffer.append("\n");
171 
172                 logger.warn(buffer.toString());
173             }
174         }
175     }
176 
177     /** {@inheritDoc} */
178     @Override
179     public String toString() {
180         return "Includes filter:" + getPatternsAsString();
181     }
182 
183     /**
184      * <p>getPatternsAsString.</p>
185      *
186      * @return pattern as a string.
187      */
188     protected String getPatternsAsString() {
189         final StringBuilder buffer = new StringBuilder();
190         for (Pattern pattern : patterns) {
191             buffer.append("\no '").append(pattern).append("'");
192         }
193 
194         return buffer.toString();
195     }
196 
197     /**
198      * <p>getFilterDescription.</p>
199      *
200      * @return description.
201      */
202     protected String getFilterDescription() {
203         return "artifact inclusion filter";
204     }
205 
206     /** {@inheritDoc} */
207     public void reportFilteredArtifacts(final Logger logger) {
208         if (!filteredArtifact.isEmpty() && logger.isDebugEnabled()) {
209             final StringBuilder buffer =
210                     new StringBuilder("The following artifacts were removed by this " + getFilterDescription() + ": ");
211 
212             for (Artifact artifactId : filteredArtifact) {
213                 buffer.append('\n').append(artifactId.getId());
214             }
215 
216             logger.debug(buffer.toString());
217         }
218     }
219 
220     /**
221      * {@inheritDoc}
222      *
223      * @return a boolean.
224      */
225     public boolean hasMissedCriteria() {
226         // if there are no patterns, there is nothing to report.
227         if (!patterns.isEmpty()) {
228             final List<Pattern> missed = new ArrayList<>(patterns);
229             missed.removeAll(patternsTriggered);
230 
231             return !missed.isEmpty();
232         }
233 
234         return false;
235     }
236 
237     private static final char[] EMPTY = new char[0];
238 
239     private static final char[] ANY = new char[] {'*'};
240 
241     static char[] emptyOrChars(String str) {
242         return str != null && str.length() > 0 ? str.toCharArray() : EMPTY;
243     }
244 
245     static char[] anyOrChars(char[] str) {
246         return str.length > 1 || (str.length == 1 && str[0] != '*') ? str : ANY;
247     }
248 
249     static char[][] tokenizeAndSplit(String pattern) {
250         String[] stokens = pattern.split(":");
251         char[][] tokens = new char[stokens.length][];
252         for (int i = 0; i < stokens.length; i++) {
253             tokens[i] = emptyOrChars(stokens[i]);
254         }
255         return tokens;
256     }
257 
258     @SuppressWarnings("InnerAssignment")
259     static boolean match(char[] patArr, char[] strArr, boolean isVersion) {
260         int patIdxStart = 0;
261         int patIdxEnd = patArr.length - 1;
262         int strIdxStart = 0;
263         int strIdxEnd = strArr.length - 1;
264         char ch;
265 
266         boolean containsStar = false;
267         for (char aPatArr : patArr) {
268             if (aPatArr == '*') {
269                 containsStar = true;
270                 break;
271             }
272         }
273 
274         if (!containsStar) {
275             if (isVersion && (patArr[0] == '[' || patArr[0] == '(')) {
276                 return isVersionIncludedInRange(String.valueOf(strArr), String.valueOf(patArr));
277             }
278             // No '*'s, so we make a shortcut
279             if (patIdxEnd != strIdxEnd) {
280                 return false; // Pattern and string do not have the same size
281             }
282             for (int i = 0; i <= patIdxEnd; i++) {
283                 ch = patArr[i];
284                 if (ch != '?' && ch != strArr[i]) {
285                     return false; // Character mismatch
286                 }
287             }
288             return true; // String matches against pattern
289         }
290 
291         if (patIdxEnd == 0) {
292             return true; // Pattern contains only '*', which matches anything
293         }
294 
295         // Process characters before first star
296         while ((ch = patArr[patIdxStart]) != '*' && strIdxStart <= strIdxEnd) {
297             if (ch != '?' && ch != strArr[strIdxStart]) {
298                 return false; // Character mismatch
299             }
300             patIdxStart++;
301             strIdxStart++;
302         }
303         if (strIdxStart > strIdxEnd) {
304             // All characters in the string are used. Check if only '*'s are
305             // left in the pattern. If so, we succeeded. Otherwise failure.
306             for (int i = patIdxStart; i <= patIdxEnd; i++) {
307                 if (patArr[i] != '*') {
308                     return false;
309                 }
310             }
311             return true;
312         }
313 
314         // Process characters after last star
315         while ((ch = patArr[patIdxEnd]) != '*' && strIdxStart <= strIdxEnd) {
316             if (ch != '?' && ch != strArr[strIdxEnd]) {
317                 return false; // Character mismatch
318             }
319             patIdxEnd--;
320             strIdxEnd--;
321         }
322         if (strIdxStart > strIdxEnd) {
323             // All characters in the string are used. Check if only '*'s are
324             // left in the pattern. If so, we succeeded. Otherwise failure.
325             for (int i = patIdxStart; i <= patIdxEnd; i++) {
326                 if (patArr[i] != '*') {
327                     return false;
328                 }
329             }
330             return true;
331         }
332 
333         // process pattern between stars. padIdxStart and patIdxEnd point
334         // always to a '*'.
335         while (patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd) {
336             int patIdxTmp = -1;
337             for (int i = patIdxStart + 1; i <= patIdxEnd; i++) {
338                 if (patArr[i] == '*') {
339                     patIdxTmp = i;
340                     break;
341                 }
342             }
343             if (patIdxTmp == patIdxStart + 1) {
344                 // Two stars next to each other, skip the first one.
345                 patIdxStart++;
346                 continue;
347             }
348             // Find the pattern between padIdxStart & padIdxTmp in str between
349             // strIdxStart & strIdxEnd
350             int patLength = (patIdxTmp - patIdxStart - 1);
351             int strLength = (strIdxEnd - strIdxStart + 1);
352             int foundIdx = -1;
353             strLoop:
354             for (int i = 0; i <= strLength - patLength; i++) {
355                 for (int j = 0; j < patLength; j++) {
356                     ch = patArr[patIdxStart + j + 1];
357                     if (ch != '?' && ch != strArr[strIdxStart + i + j]) {
358                         continue strLoop;
359                     }
360                 }
361 
362                 foundIdx = strIdxStart + i;
363                 break;
364             }
365 
366             if (foundIdx == -1) {
367                 return false;
368             }
369 
370             patIdxStart = patIdxTmp;
371             strIdxStart = foundIdx + patLength;
372         }
373 
374         // All characters in the string are used. Check if only '*'s are left
375         // in the pattern. If so, we succeeded. Otherwise failure.
376         for (int i = patIdxStart; i <= patIdxEnd; i++) {
377             if (patArr[i] != '*') {
378                 return false;
379             }
380         }
381         return true;
382     }
383 
384     static boolean isVersionIncludedInRange(final String version, final String range) {
385         try {
386             return VersionRange.createFromVersionSpec(range).containsVersion(new DefaultArtifactVersion(version));
387         } catch (final InvalidVersionSpecificationException e) {
388             return false;
389         }
390     }
391 
392     static Pattern compile(String pattern) {
393         if (pattern.startsWith("!")) {
394             return new NegativePattern(pattern, compile(pattern.substring(1)));
395         } else {
396             char[][] stokens = tokenizeAndSplit(pattern);
397             char[][] tokens = new char[stokens.length][];
398             for (int i = 0; i < stokens.length; i++) {
399                 tokens[i] = anyOrChars(stokens[i]);
400             }
401             if (tokens.length > 5) {
402                 throw new IllegalArgumentException("Invalid pattern: " + pattern);
403             }
404             //
405             // Check the tokens and build an appropriate Pattern
406             // Special care needs to be taken if the first or the last part is '*'
407             // because this allows the '*' to match multiple tokens
408             //
409             if (tokens.length == 1) {
410                 if (tokens[0] == ANY) {
411                     // *
412                     return all(pattern);
413                 } else {
414                     // [pat0]
415                     return match(pattern, tokens[0], 0);
416                 }
417             }
418             if (tokens.length == 2) {
419                 if (tokens[0] == ANY) {
420                     if (tokens[1] == ANY) {
421                         // *:*
422                         return all(pattern);
423                     } else {
424                         // *:[pat1]
425                         return match(pattern, tokens[1], 0, 3);
426                     }
427                 } else {
428                     if (tokens[1] == ANY) {
429                         // [pat0]:*
430                         return match(pattern, tokens[0], 0);
431                     } else {
432                         // [pat0]:[pat1]
433                         Pattern m00 = match(tokens[0], 0);
434                         Pattern m11 = match(tokens[1], 1);
435                         return and(pattern, m00, m11);
436                     }
437                 }
438             }
439             if (tokens.length == 3) {
440                 if (tokens[0] == ANY) {
441                     if (tokens[1] == ANY) {
442                         if (tokens[2] == ANY) {
443                             // *:*:*
444                             return all(pattern);
445                         } else {
446                             // *:*:[pat2]
447                             return match(pattern, tokens[2], 2, 3);
448                         }
449                     } else {
450                         if (tokens[2] == ANY) {
451                             // *:[pat1]:*
452                             return match(pattern, tokens[1], 1, 2);
453                         } else {
454                             // *:[pat1]:[pat2]
455                             Pattern m11 = match(tokens[1], 1);
456                             Pattern m12 = match(tokens[1], 2);
457                             Pattern m22 = match(tokens[2], 2);
458                             Pattern m23 = match(tokens[2], 3);
459                             return or(pattern, and(m11, m22), and(m12, m23));
460                         }
461                     }
462                 } else {
463                     if (tokens[1] == ANY) {
464                         if (tokens[2] == ANY) {
465                             // [pat0]:*:*
466                             return match(pattern, tokens[0], 0, 1);
467                         } else {
468                             // [pat0]:*:[pat2]
469                             Pattern m00 = match(tokens[0], 0);
470                             Pattern m223 = match(tokens[2], 2, 3);
471                             return and(pattern, m00, m223);
472                         }
473                     } else {
474                         if (tokens[2] == ANY) {
475                             // [pat0]:[pat1]:*
476                             Pattern m00 = match(tokens[0], 0);
477                             Pattern m11 = match(tokens[1], 1);
478                             return and(pattern, m00, m11);
479                         } else {
480                             // [pat0]:[pat1]:[pat2]
481                             Pattern m00 = match(tokens[0], 0);
482                             Pattern m11 = match(tokens[1], 1);
483                             Pattern m22 = match(tokens[2], 2);
484                             return and(pattern, m00, m11, m22);
485                         }
486                     }
487                 }
488             }
489             if (tokens.length >= 4) {
490                 List<Pattern> patterns = new ArrayList<>();
491                 for (int i = 0; i < tokens.length; i++) {
492                     if (tokens[i] != ANY) {
493                         patterns.add(match(tokens[i], i));
494                     }
495                 }
496                 return and(pattern, patterns.toArray(new Pattern[0]));
497             }
498             throw new IllegalStateException();
499         }
500     }
501 
502     /** Creates a positional matching pattern */
503     private static Pattern match(String pattern, char[] token, int posVal) {
504         return match(pattern, token, posVal, posVal);
505     }
506 
507     /** Creates a positional matching pattern */
508     private static Pattern match(char[] token, int posVal) {
509         return match("", token, posVal, posVal);
510     }
511 
512     /** Creates a positional matching pattern */
513     private static Pattern match(String pattern, char[] token, int posMin, int posMax) {
514         boolean hasWildcard = false;
515         for (char ch : token) {
516             if (ch == '*' || ch == '?') {
517                 hasWildcard = true;
518                 break;
519             }
520         }
521         if (hasWildcard || posMax == 4) {
522             return new PosPattern(pattern, token, posMin, posMax);
523         } else {
524             return new EqPattern(pattern, token, posMin, posMax);
525         }
526     }
527 
528     /** Creates a positional matching pattern */
529     private static Pattern match(char[] token, int posMin, int posMax) {
530         return new PosPattern("", token, posMin, posMax);
531     }
532 
533     /** Creates an AND pattern */
534     private static Pattern and(String pattern, Pattern... patterns) {
535         return new AndPattern(pattern, patterns);
536     }
537 
538     /** Creates an AND pattern */
539     private static Pattern and(Pattern... patterns) {
540         return and("", patterns);
541     }
542 
543     /** Creates an OR pattern */
544     private static Pattern or(String pattern, Pattern... patterns) {
545         return new OrPattern(pattern, patterns);
546     }
547 
548     /** Creates an OR pattern */
549     private static Pattern or(Pattern... patterns) {
550         return or("", patterns);
551     }
552 
553     /** Creates a match-all pattern */
554     private static Pattern all(String pattern) {
555         return new MatchAllPattern(pattern);
556     }
557 
558     /**
559      * Abstract class for patterns
560      */
561     abstract static class Pattern {
562         private final String pattern;
563 
564         Pattern(String pattern) {
565             this.pattern = Objects.requireNonNull(pattern);
566         }
567 
568         public abstract boolean matches(char[][] parts);
569 
570         /**
571          * Returns a string containing a fixed artifact gatv coordinates
572          * or null if the pattern can not be translated.
573          */
574         public String translateEquals() {
575             return null;
576         }
577 
578         /**
579          * Check if the this pattern is a fixed pattern on the specified pos.
580          */
581         protected String translateEquals(int pos) {
582             return null;
583         }
584 
585         @Override
586         public String toString() {
587             return pattern;
588         }
589     }
590 
591     /**
592      * Simple pattern which performs a logical AND between one or more patterns.
593      */
594     static class AndPattern extends Pattern {
595         private final Pattern[] patterns;
596 
597         AndPattern(String pattern, Pattern[] patterns) {
598             super(pattern);
599             this.patterns = patterns;
600         }
601 
602         @Override
603         public boolean matches(char[][] parts) {
604             for (Pattern pattern : patterns) {
605                 if (!pattern.matches(parts)) {
606                     return false;
607                 }
608             }
609             return true;
610         }
611 
612         @Override
613         public String translateEquals() {
614             String[] strings = new String[patterns.length];
615             for (int i = 0; i < patterns.length; i++) {
616                 strings[i] = patterns[i].translateEquals(i);
617                 if (strings[i] == null) {
618                     return null;
619                 }
620             }
621             StringBuilder sb = new StringBuilder();
622             for (int i = 0; i < strings.length; i++) {
623                 if (i > 0) {
624                     sb.append(":");
625                 }
626                 sb.append(strings[i]);
627             }
628             return sb.toString();
629         }
630     }
631 
632     /**
633      * Simple pattern which performs a logical OR between one or more patterns.
634      */
635     static class OrPattern extends Pattern {
636         private final Pattern[] patterns;
637 
638         OrPattern(String pattern, Pattern[] patterns) {
639             super(pattern);
640             this.patterns = patterns;
641         }
642 
643         @Override
644         public boolean matches(char[][] parts) {
645             for (Pattern pattern : patterns) {
646                 if (pattern.matches(parts)) {
647                     return true;
648                 }
649             }
650             return false;
651         }
652     }
653 
654     /**
655      * A positional matching pattern, to check if a token in the gatv coordinates
656      * having a position between posMin and posMax (both inclusives) can match
657      * the pattern.
658      */
659     static class PosPattern extends Pattern {
660         private final char[] patternCharArray;
661         private final int posMin;
662         private final int posMax;
663 
664         PosPattern(String pattern, char[] patternCharArray, int posMin, int posMax) {
665             super(pattern);
666             this.patternCharArray = patternCharArray;
667             this.posMin = posMin;
668             this.posMax = posMax;
669         }
670 
671         @Override
672         public boolean matches(char[][] parts) {
673             for (int i = posMin; i <= posMax; i++) {
674                 if (match(patternCharArray, parts[i], i == 4)) {
675                     return true;
676                 }
677             }
678             return false;
679         }
680     }
681 
682     /**
683      * Looks for an exact match in the gatv coordinates between
684      * posMin and posMax (both inclusives)
685      */
686     static class EqPattern extends Pattern {
687         private final char[] token;
688         private final int posMin;
689         private final int posMax;
690 
691         EqPattern(String pattern, char[] patternCharArray, int posMin, int posMax) {
692             super(pattern);
693             this.token = patternCharArray;
694             this.posMin = posMin;
695             this.posMax = posMax;
696         }
697 
698         @Override
699         public boolean matches(char[][] parts) {
700             for (int i = posMin; i <= posMax; i++) {
701                 if (Arrays.equals(token, parts[i])) {
702                     return true;
703                 }
704             }
705             return false;
706         }
707 
708         @Override
709         public String translateEquals() {
710             return translateEquals(0);
711         }
712 
713         public String translateEquals(int pos) {
714             return posMin == pos && posMax == pos && (pos < 3 || (token[0] != '[' && token[0] != '('))
715                     ? String.valueOf(token)
716                     : null;
717         }
718     }
719 
720     /**
721      * Matches all input
722      */
723     static class MatchAllPattern extends Pattern {
724         MatchAllPattern(String pattern) {
725             super(pattern);
726         }
727 
728         @Override
729         public boolean matches(char[][] parts) {
730             return true;
731         }
732     }
733 
734     /**
735      * Negative pattern
736      */
737     static class NegativePattern extends Pattern {
738         private final Pattern inner;
739 
740         NegativePattern(String pattern, Pattern inner) {
741             super(pattern);
742             this.inner = inner;
743         }
744 
745         @Override
746         public boolean matches(char[][] parts) {
747             return inner.matches(parts);
748         }
749     }
750 }