View Javadoc
1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  
20  package org.eclipse.jetty.webapp;
21  
22  import java.io.File;
23  import java.net.URL;
24  import java.nio.file.Path;
25  import java.util.AbstractSet;
26  import java.util.ArrayList;
27  import java.util.Arrays;
28  import java.util.HashMap;
29  import java.util.HashSet;
30  import java.util.Iterator;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.function.Predicate;
34  
35  import org.eclipse.jetty.util.ArrayTernaryTrie;
36  import org.eclipse.jetty.util.IncludeExcludeSet;
37  import org.eclipse.jetty.util.TypeUtil;
38  import org.eclipse.jetty.util.URIUtil;
39  import org.eclipse.jetty.util.log.Log;
40  import org.eclipse.jetty.util.log.Logger;
41  import org.eclipse.jetty.util.resource.Resource;
42  
43  /**
44   * Classpath classes list performs sequential pattern matching of a class name 
45   * against an internal array of classpath pattern entries.
46   * A class pattern is a string of one of the forms:<ul>
47   * <li>'org.package.SomeClass' will match a specific class
48   * <li>'org.package.' will match a specific package hierarchy
49   * <li>'-org.package.Classname' excludes a specific class
50   * <li>'-org.package.' excludes a specific package hierarchy
51   * <li>Nested classes must be specified with the '$' separator if they 
52   * are to be explicitly included or excluded (eg. org.example.MyClass$NestedClass).
53   * <li>Nested classes are matched by their containing class. (eg. -org.example.MyClass
54   * would exclude org.example.MyClass$AnyNestedClass)
55   * </ul>
56   * When class is initialized from a classpath pattern string, entries 
57   * in this string should be separated by ':' (semicolon) or ',' (comma).
58   */
59  
60  public class ClasspathPattern extends AbstractSet<String>
61  {
62      private static final Logger LOG = Log.getLogger(ClasspathPattern.class);
63      
64      enum Type { PACKAGE, CLASSNAME, LOCATION }
65  
66      private static class Entry
67      {
68          private final String _pattern;
69          private final String _name;
70          private final boolean _inclusive;
71          private final Type _type;
72          
73          Entry(String pattern)
74          {
75              _pattern=pattern;
76              _inclusive = !pattern.startsWith("-");
77              _name = _inclusive ? pattern : pattern.substring(1).trim();
78              _type = (_name.startsWith("file:"))?Type.LOCATION:(_name.endsWith(".")?Type.PACKAGE:Type.CLASSNAME);
79          }
80          
81          Entry(String name, boolean include)
82          {
83              _pattern=include?name:("-"+name);
84              _inclusive = include;
85              _name = name;
86              _type = (_name.startsWith("file:"))?Type.LOCATION:(_name.endsWith(".")?Type.PACKAGE:Type.CLASSNAME);
87          }
88          
89  
90          public String getPattern()
91          {
92              return _pattern;
93          }
94          
95          public boolean isPackage()
96          {
97              return _type==Type.PACKAGE;
98          }
99          
100         public boolean isClassName()
101         {
102             return _type==Type.CLASSNAME;
103         }
104         
105         public boolean isLocation()
106         {
107             return _type==Type.LOCATION;
108         }
109 
110         public String getName()
111         {
112             return _name;
113         }
114         
115         @Override
116         public String toString()
117         {
118             return _pattern;
119         }
120         
121         @Override 
122         public int hashCode()
123         {
124             return _pattern.hashCode();
125         }
126         
127         @Override 
128         public boolean equals(Object o)
129         {
130             return (o instanceof Entry) 
131                 && _pattern.equals(((Entry)o)._pattern);
132         }
133 
134         public boolean isInclusive()
135         {
136             return _inclusive;
137         }
138     }
139     
140     
141     public static class ByPackage extends AbstractSet<Entry> implements Predicate<String> 
142     {
143         private final ArrayTernaryTrie.Growing<Entry> _entries = new ArrayTernaryTrie.Growing<>(false,512,512);
144 
145         @Override
146         public boolean test(String name)
147         {
148             return _entries.getBest(name)!=null;
149         }
150 
151         @Override
152         public Iterator<Entry> iterator()
153         {
154             return _entries.keySet().stream().map(_entries::get).iterator();
155         }
156 
157         @Override
158         public int size()
159         {
160             return _entries.size();
161         }
162         
163         @Override
164         public boolean isEmpty()
165         {
166             return _entries.isEmpty();
167         }
168         
169         @Override
170         public boolean add(Entry entry)
171         {
172             String name = entry.getName();
173             if (entry.isClassName())
174                 name+="$";
175             else if (entry.isLocation())
176                 throw new IllegalArgumentException(entry.toString());
177             else if (".".equals(name))
178                 name="";
179                 
180             if (_entries.get(name)!=null)
181                 return false;
182             
183             _entries.put(name,entry);
184             return true;
185         }
186         
187         @Override
188         public boolean remove(Object entry)
189         {
190             if (!(entry instanceof Entry))
191                 return false;
192 
193             return _entries.remove(((Entry)entry).getName())!=null;
194         }
195         
196         @Override
197         public void clear()
198         {
199             _entries.clear();
200         }
201     }
202     
203     @SuppressWarnings("serial")
204     public static class ByName extends HashSet<Entry> implements Predicate<String> 
205     {
206         private final Map<String,Entry> _entries = new HashMap<>();
207 
208         @Override
209         public boolean test(String name)
210         {
211             return _entries.containsKey(name);
212         }
213 
214         @Override
215         public Iterator<Entry> iterator()
216         {
217             return _entries.values().iterator();
218         }
219 
220         @Override
221         public int size()
222         {
223             return _entries.size();
224         }
225         
226         @Override
227         public boolean add(Entry entry)
228         {
229             if (!entry.isClassName())
230                 throw new IllegalArgumentException(entry.toString());
231             return _entries.put(entry.getName(),entry)==null;
232         }
233         
234         @Override
235         public boolean remove(Object entry)
236         {
237             if (!(entry instanceof Entry))
238                 return false;
239 
240             return _entries.remove(((Entry)entry).getName())!=null;
241         }
242     }
243 
244     public static class ByPackageOrName extends AbstractSet<Entry> implements Predicate<String> 
245     {
246         private final ByName _byName = new ByName();
247         private final ByPackage _byPackage = new ByPackage();
248         
249         @Override
250         public boolean test(String name)
251         {
252             return  _byPackage.test(name) 
253                 || _byName.test(name) ;
254         }
255 
256         @Override
257         public Iterator<Entry> iterator()
258         {
259             // by package contains all entries (classes are also $ packages).
260             return _byPackage.iterator();
261         }
262 
263         @Override
264         public int size()
265         {
266             return _byPackage.size();
267         }
268 
269         @Override
270         public boolean add(Entry e)
271         {
272             if (e.isLocation())
273                 throw new IllegalArgumentException();
274             
275             if (e.isPackage())
276                 return _byPackage.add(e);
277             
278             // Add class name to packages also as classes act
279             // as packages for nested classes.
280             boolean added = _byPackage.add(e);
281             added = _byName.add(e) || added;
282             return added;
283         }
284 
285         @Override
286         public boolean remove(Object o)
287         {
288             if (!(o instanceof Entry))
289                 return false;
290 
291             boolean removed = _byPackage.remove(o);
292             
293             if (!((Entry)o).isPackage())
294                 removed = _byName.remove(o) || removed;
295             
296             return removed;
297         }
298 
299         @Override
300         public void clear()
301         {
302             _byPackage.clear();
303             _byName.clear();
304         }
305     }
306     
307     @SuppressWarnings("serial")
308     public static class ByLocation extends HashSet<File> implements Predicate<Path>
309     {        
310         @Override
311         public boolean test(Path path)
312         {
313             for (File file: this)
314             {
315                 if (file.isDirectory())
316                 {
317                     if (path.startsWith(file.toPath()))
318                         return true;
319                 }
320                 else
321                 {
322                     if (path.equals(file.toPath()))
323                         return true;
324                 }
325             }
326                 
327             return false;
328         }
329     }
330     
331     
332     Map<String,Entry> _entries = new HashMap<>();
333     IncludeExcludeSet<Entry,String> _patterns = new IncludeExcludeSet<>(ByPackageOrName.class);
334     IncludeExcludeSet<File,Path> _locations = new IncludeExcludeSet<>(ByLocation.class);
335     
336     public ClasspathPattern()
337     {
338     }
339     
340     public ClasspathPattern(String[] patterns)
341     {
342         setAll(patterns);
343     }
344     
345     public ClasspathPattern(String pattern)
346     {
347         add(pattern);
348     }
349     
350     public boolean include(String name)
351     {
352         if (name==null)
353             return false;
354         return add(new Entry(name,true));
355     }
356     
357     public boolean include(String... name)
358     {
359         boolean added = false;
360         for (String n:name)
361             if (n!=null)
362                 added = add(new Entry(n,true)) || added;
363         return added;
364     }
365     
366     public boolean exclude(String name)
367     {
368         if (name==null)
369             return false;
370         return add(new Entry(name,false));
371     }
372     
373     public boolean exclude(String... name)
374     {
375         boolean added = false;
376         for (String n:name)
377             if (n!=null)
378                 added = add(new Entry(n,false)) || added;
379         return added;
380     }
381     
382     @Override
383     public boolean add(String pattern)
384     {
385         if (pattern==null)
386             return false;
387         return add(new Entry(pattern));
388     }
389     
390     public boolean add(String... pattern)
391     {
392         boolean added = false;
393         for (String p:pattern)
394             if (p!=null)
395                 added = add(new Entry(p)) || added;
396         return added;
397     }
398     
399     protected boolean add(Entry entry)
400     {
401         if (_entries.containsKey(entry.getPattern()))
402             return false;
403         _entries.put(entry.getPattern(),entry);
404 
405         if (entry.isLocation())
406         {
407             try
408             {
409                 File file = Resource.newResource(entry.getName()).getFile().getAbsoluteFile().getCanonicalFile();
410                 if (entry.isInclusive())
411                     _locations.include(file);
412                 else
413                     _locations.exclude(file);
414             }
415             catch (Exception e)
416             {
417                 throw new IllegalArgumentException(e);
418             }
419         }
420         else
421         {
422             if (entry.isInclusive())
423                 _patterns.include(entry);
424             else
425                 _patterns.exclude(entry);
426         }
427         return true;
428     }
429 
430     @Override
431     public boolean remove(Object o)
432     {
433         if (!(o instanceof String))
434             return false;
435         String pattern = (String)o;
436 
437         Entry entry = _entries.remove(pattern);
438         if (entry==null)
439             return false;
440 
441         List<Entry> saved = new ArrayList<>(_entries.values());
442         clear();
443         for (Entry e:saved)
444             add(e);
445         return true;
446     }
447 
448     @Override
449     public void clear()
450     {
451         _entries.clear();
452         _patterns.clear();
453         _locations.clear();
454     }
455 
456     @Override
457     public Iterator<String> iterator()
458     {
459         return _entries.keySet().iterator();
460     }
461 
462     @Override
463     public int size()
464     {
465         return _entries.size();
466     }
467 
468     /**
469      * Initialize the matcher by parsing each classpath pattern in an array
470      * 
471      * @param classes array of classpath patterns
472      */
473     private void setAll(String[] classes)
474     {
475         _entries.clear();
476         addAll(classes);
477     }
478     
479     /**
480      * @param classes array of classpath patterns
481      */
482     private void addAll(String[] classes)
483     {
484         if (classes!=null)
485             addAll(Arrays.asList(classes));
486     }
487     
488     /**
489      * @return array of classpath patterns
490      */
491     public String[] getPatterns()
492     {
493         return toArray(new String[_entries.size()]);
494     }
495 
496     
497     /**
498      * Match the class name against the pattern
499      *
500      * @param name name of the class to match
501      * @return true if class matches the pattern
502      */
503     public boolean match(String name)
504     {       
505         return _patterns.test(name);
506     }
507     
508     /**
509      * Match the class name against the pattern
510      *
511      * @param clazz A class to try to match
512      * @return true if class matches the pattern
513      */
514     public boolean match(Class<?> clazz)
515     {       
516         try
517         {
518             Boolean byName = _patterns.isIncludedAndNotExcluded(clazz.getName());
519             Resource resource = TypeUtil.getLoadedFrom(clazz);
520             Boolean byLocation = resource == null || resource.getFile() == null
521                     ? null
522                     : _locations.isIncludedAndNotExcluded(resource.getFile().toPath());
523 
524             // Combine the tri-state match of both IncludeExclude Sets
525             boolean included = byName==Boolean.TRUE || byLocation==Boolean.TRUE
526                 || (byName==null && !_patterns.hasIncludes() && byLocation==null && !_locations.hasIncludes());
527             boolean excluded = byName==Boolean.FALSE || byLocation==Boolean.FALSE;
528             return included && !excluded;
529         }
530         catch (Exception e)
531         {
532             LOG.warn(e);
533         }
534         return false;
535     }
536 
537     public boolean match(String name, URL url)
538     {
539         // Strip class suffix for name matching
540         if (name.endsWith(".class"))
541             name=name.substring(0,name.length()-6);
542         
543         // Treat path elements as packages for name matching
544         name=name.replace("/",".");
545 
546         Boolean byName = _patterns.isIncludedAndNotExcluded(name);
547         
548         // Try to find a file path for location matching
549         Boolean byLocation = null;
550         try
551         {
552             Resource resource = Resource.newResource(URIUtil.getJarSource(url.toURI()));
553             File file = resource.getFile();
554             byLocation = _locations.isIncludedAndNotExcluded(file.toPath());
555         }
556         catch(Exception e)
557         {
558             LOG.ignore(e);
559         }
560 
561         // Combine the tri-state match of both IncludeExclude Sets
562         boolean included = byName==Boolean.TRUE || byLocation==Boolean.TRUE
563             || (byName==null && !_patterns.hasIncludes() && byLocation==null && !_locations.hasIncludes());
564         boolean excluded = byName==Boolean.FALSE || byLocation==Boolean.FALSE;
565         return included && !excluded;
566     }
567 }