View Javadoc

1   /***
2    * Copyright 2009 Martin Vysny.
3    *
4    * This file is part of WebVM.
5    *
6    * WebVM is free software: you can redistribute it and/or modify
7    * it under the terms of the GNU General Public License as published by
8    * the Free Software Foundation, either version 3 of the License, or
9    * (at your option) any later version.
10   *
11   * WebVM is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14   * GNU General Public License for more details.
15   *
16   * You should have received a copy of the GNU General Public License
17   * along with WebVM.  If not, see <http://www.gnu.org/licenses/>.
18   */
19  package sk.baka.webvm.analyzer.classloader;
20  
21  import java.io.File;
22  import java.io.FileFilter;
23  import java.io.FileInputStream;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.Serializable;
27  import java.util.ArrayList;
28  import java.util.Collection;
29  import java.util.Enumeration;
30  import java.util.HashSet;
31  import java.util.List;
32  import java.util.Set;
33  import java.util.logging.Level;
34  import java.util.logging.Logger;
35  import java.util.zip.ZipEntry;
36  import java.util.zip.ZipFile;
37  import static sk.baka.webvm.misc.Constants.*;
38  
39  /***
40   * Represents an on-disk package or a package item.
41   * @author Martin Vysny
42   */
43  public abstract class ResourceLink implements Serializable {
44  
45      /***
46       * Opens given directory or jar file and allows package and resource enumeration.
47       * @param file the resource directory or a jar file
48       * @return new resource enumerator.
49       */
50      public static ResourceLink newFor(final File file) {
51          if (file.isDirectory()) {
52              return new DirResourceLink(file, true);
53          } else {
54              return new JarResourceLink(file, "", true);
55          }
56      }
57  
58      /***
59       * Returns a list of names from given list of links
60       * @param links not null
61       * @return never null
62       */
63      public static List<String> getNames(final List<ResourceLink> links) {
64          final List<String> result = new ArrayList<String>(links.size());
65          for (final ResourceLink link : links) {
66              result.add(link.getName());
67          }
68          return result;
69      }
70  
71      /***
72       * Finds first link with given name.
73       * @param links a list of links, must not be null
74       * @param name the name of the resource.
75       * @return the link with given name, never null
76       * @throws IllegalArgumentException if no such link exists
77       */
78      public static ResourceLink findFirstByName(final List<ResourceLink> links, final String name) {
79          for (final ResourceLink link : links) {
80              if (link.getName().equals(name)) {
81                  return link;
82              }
83          }
84          throw new IllegalArgumentException("No such link: " + name);
85      }
86  
87      /***
88       * Performs a search for given substring. The substring matching must be performed in the last resource path item only - i.e.
89       * the function will match the following for "a": /a, /a/a, /b/a, but not /a/b.
90       * @param substring a string to search, must not be null.
91       * @return non-null list of matched resources, may be empty.
92       * @throws IOException on i/o error
93       */
94      public abstract List<ResourceLink> search(final String substring) throws IOException;
95  
96      /***
97       * Returns length of underlying resource.
98       * @return the length or -1 if not known or invoked on a package.
99       * @throws IOException on i/o error
100      */
101     public abstract long getLength() throws IOException;
102 
103     /***
104      * Lists all direct child packages and items of this package. It is invalid to call this method on a non-package resource. Groups single-package-child
105      * names together.
106      * @return list of all children.
107      * @throws IOException on i/o error
108      */
109     public final List<ResourceLink> listAndGroup() throws IOException {
110         assertPackage();
111         final List<ResourceLink> result = list();
112         for (int i = 0; i < result.size(); i++) {
113             result.set(i, groupIfNecessary(result.get(i)));
114         }
115         return result;
116     }
117 
118     private ResourceLink groupIfNecessary(final ResourceLink child) throws IOException {
119         if (!child.isPackage()) {
120             return child;
121         }
122         ResourceLink singlePackage = child;
123         ResourceLink prevPackage = null;
124         final StringBuilder sb = new StringBuilder(child.getName());
125         do {
126             final List<ResourceLink> children = singlePackage.list();
127             prevPackage = singlePackage;
128             singlePackage = getSinglePackage(children);
129             if (singlePackage != null) {
130                 sb.append('.');
131                 sb.append(singlePackage.getName());
132             }
133         } while (singlePackage != null);
134         if (prevPackage == child) {
135             return child;
136         }
137         return new ResourceLinkGroup(sb.toString(), prevPackage);
138     }
139 
140     private static ResourceLink getSinglePackage(final Collection<? extends ResourceLink> contents) {
141         if (contents.size() != 1) {
142             return null;
143         }
144         final ResourceLink link = contents.iterator().next();
145         if (!link.isPackage()) {
146             return null;
147         }
148         return link;
149     }
150 
151     /***
152      * Lists all direct child packages and items of this package. It is invalid to call this method on a non-package resource.
153      * @return list of all children.
154      * @throws IOException on i/o error
155      */
156     public abstract List<ResourceLink> list() throws IOException;
157 
158     /***
159      * Checks if resource denoted by this object is actually a package, or just a resource file.
160      * @return true if this is a package, false otherwise. {@link #root() Root link} is always a package.
161      */
162     public abstract boolean isPackage();
163 
164     /***
165      * Opens a stream to the file denoted by this link. It is invalid to call this method on a package resource.
166      * @return the file contents.
167      * @throws IOException on i/o error
168      */
169     public abstract InputStream open() throws IOException;
170 
171     /***
172      * Returns the package/resource name. Does not include names of the parent packages nor any slash characters. Name of the root package is a full directory/jar file name.
173      * @return the name of the resource denoted by this link.
174      */
175     public abstract String getName();
176 
177     /***
178      * Returns a full name, including parent packages and full path to the root. Must not end with a slash.
179      * @return a full name.
180      */
181     public abstract String getFullName();
182 
183     /***
184      * Checks if this resource link denotes a root of a jar/directory.
185      * @return true if this is a jar/directory root, false otherwise.
186      */
187     public abstract boolean isRoot();
188 
189     /***
190      * Returns a file link to the resource container containing these links. Required to be valid only for {@link #isRoot() root} links.
191      * @return file denoting the container or null.
192      */
193     public abstract File getContainer();
194 
195     @Override
196     public String toString() {
197         return getName();
198     }
199 
200     /***
201      * Asserts that this is a package.
202      */
203     protected void assertPackage() {
204         if (!isPackage()) {
205             throw new IllegalArgumentException(this + " must be a package");
206         }
207     }
208 
209     /***
210      * Asserts that this is not a package.
211      */
212     protected void assertNotPackage() {
213         if (isPackage()) {
214             throw new IllegalArgumentException(this + " must not be a package");
215         }
216     }
217 }
218 
219 /***
220  * Provides package information for a directory containing classpath items.
221  * @author Martin Vysny
222  */
223 final class DirResourceLink extends ResourceLink {
224 
225     private static final long serialVersionUID = 1L;
226     private final File file;
227     private final boolean isRoot;
228 
229     DirResourceLink(final File file, final boolean isRoot) {
230         this.file = file;
231         this.isRoot = isRoot;
232     }
233 
234     @Override
235     public List<ResourceLink> list() {
236         assertPackage();
237         final File[] children = file.listFiles();
238         final List<ResourceLink> result = new ArrayList<ResourceLink>(children.length);
239         for (final File child : children) {
240             result.add(new DirResourceLink(child, false));
241         }
242         return result;
243     }
244 
245     @Override
246     public boolean isPackage() {
247         return file.isDirectory();
248     }
249 
250     @Override
251     public InputStream open() throws IOException {
252         return new FileInputStream(file);
253     }
254 
255     @Override
256     public String getName() {
257         return isRoot ? file.getAbsolutePath() : file.getName();
258     }
259 
260     @Override
261     public long getLength() {
262         return isPackage() ? -1 : file.length();
263     }
264 
265     @Override
266     public boolean isRoot() {
267         return isRoot;
268     }
269 
270     @Override
271     public File getContainer() {
272         return isRoot ? file : null;
273     }
274 
275     @Override
276     public List<ResourceLink> search(String substring) {
277         assertPackage();
278         final List<ResourceLink> result = new ArrayList<ResourceLink>();
279         searchRecurse(result, substring.toLowerCase(), file);
280         return result;
281     }
282 
283     private void searchRecurse(final List<ResourceLink> result, final String substring, final File file) {
284         for (final File f : file.listFiles(new FileFilter() {
285 
286             public boolean accept(File pathname) {
287                 return pathname.isDirectory() || pathname.getName().toLowerCase().contains(substring);
288             }
289         })) {
290             if (f.getName().toLowerCase().contains(substring)) {
291                 result.add(new DirResourceLink(f, false));
292             }
293             if (f.isDirectory()) {
294                 searchRecurse(result, substring, f);
295             }
296         }
297     }
298 
299     @Override
300     public String getFullName() {
301         return file.getAbsolutePath();
302     }
303 }
304 
305 /***
306  * Provides package information for an on-disk jar file.
307  * @author Martin Vysny
308  */
309 final class JarResourceLink extends ResourceLink {
310 
311     private static final long serialVersionUID = 1L;
312     private final File jarFile;
313     private final String fullEntryName;
314     private final boolean isRoot;
315 
316     JarResourceLink(final File jarFile, final String fullEntryName, final boolean isRoot) {
317         this.jarFile = jarFile;
318         this.fullEntryName = fullEntryName;
319         this.isRoot = isRoot;
320     }
321 
322     @Override
323     public List<ResourceLink> list() throws IOException {
324         assertPackage();
325         final List<ResourceLink> result = new ArrayList<ResourceLink>();
326         final ZipFile zfile = new ZipFile(jarFile);
327         try {
328             final Set<String> resultNames = new HashSet<String>();
329             for (final Enumeration<? extends ZipEntry> e = zfile.entries(); e.hasMoreElements();) {
330                 final ZipEntry entry = e.nextElement();
331                 final String name = entry.getName();
332                 if (!name.startsWith(fullEntryName) || name.equals(fullEntryName)) {
333                     continue;
334                 }
335                 String itemname = name.substring(fullEntryName.length());
336                 final int slash = itemname.indexOf('/');
337                 if (slash >= 0) {
338                     itemname = itemname.substring(0, slash + 1);
339                 }
340                 if (resultNames.add(itemname)) {
341                     final ResourceLink link = new JarResourceLink(jarFile, fullEntryName + itemname, false);
342                     result.add(link);
343                 }
344             }
345             return result;
346         } finally {
347             closeQuietly(zfile);
348         }
349     }
350 
351     private static void closeQuietly(final ZipFile zf) {
352         try {
353             zf.close();
354         } catch (IOException ex) {
355             Logger.getLogger(JarResourceLink.class.getName()).log(Level.SEVERE, null, ex);
356         }
357     }
358 
359     @Override
360     public boolean isPackage() {
361         return fullEntryName.length() == 0 || fullEntryName.endsWith("/");
362     }
363 
364     @Override
365     public InputStream open() throws IOException {
366         final ZipFile zfile = new ZipFile(jarFile);
367         final ZipEntry entry = zfile.getEntry(fullEntryName);
368         return zfile.getInputStream(entry);
369     }
370 
371     @Override
372     public String getName() {
373         if (isRoot) {
374             return jarFile.getAbsolutePath();
375         }
376         String fullName = fullEntryName;
377         if (fullEntryName.endsWith("/")) {
378             fullName = fullName.substring(0, fullEntryName.length() - 1);
379         }
380         final int lastSlash = fullName.lastIndexOf('/');
381         return fullName.substring(lastSlash + 1, fullName.length());
382     }
383 
384     @Override
385     public long getLength() throws IOException {
386         if (isPackage()) {
387             return -1;
388         }
389         final ZipFile zfile = new ZipFile(jarFile);
390         final ZipEntry entry = zfile.getEntry(fullEntryName);
391         if (entry == null) {
392             throw new IOException("No such entry: " + fullEntryName);
393         }
394         return entry.getSize();
395     }
396 
397     @Override
398     public boolean isRoot() {
399         return isRoot;
400     }
401 
402     @Override
403     public File getContainer() {
404         return jarFile;
405     }
406 
407     @Override
408     public String toString() {
409         if (isRoot()) {
410             return getName() + " [" + (jarFile.length() / KIBIBYTES) + "K]";
411         }
412         return super.toString();
413     }
414 
415     @Override
416     public List<ResourceLink> search(String substring) throws IOException {
417         assertPackage();
418         final String substr = substring.toLowerCase();
419         final List<ResourceLink> result = new ArrayList<ResourceLink>();
420         final Set<String> ignorePrefixes = new HashSet<String>();
421         final ZipFile zfile = new ZipFile(jarFile);
422         try {
423             for (final Enumeration<? extends ZipEntry> e = zfile.entries(); e.hasMoreElements();) {
424                 final ZipEntry entry = e.nextElement();
425                 final String name = entry.getName();
426                 if (!isAccepted(name, ignorePrefixes, substr)) {
427                     continue;
428                 }
429                 final String itemname = name.substring(fullEntryName.length());
430                 if (!itemname.toLowerCase().contains(substr)) {
431                     continue;
432                 }
433                 ignorePrefixes.add(name);
434                 final ResourceLink link = new JarResourceLink(jarFile, name, false);
435                 result.add(link);
436             }
437             return result;
438         } finally {
439             closeQuietly(zfile);
440         }
441     }
442 
443     private boolean isAccepted(String name, Set<String> ignorePrefixes, String substring) {
444         if (!name.startsWith(fullEntryName) || name.equals(fullEntryName)) {
445             return false;
446         }
447         for (final String prefix : ignorePrefixes) {
448             if (name.startsWith(prefix)) {
449                 // check if the rest of the string contains given substring
450                 if (!name.substring(prefix.length()).contains(substring)) {
451                     return false;
452                 }
453             }
454         }
455         return true;
456     }
457 
458     @Override
459     public String getFullName() {
460         if (isRoot()) {
461             return jarFile.getAbsolutePath();
462         }
463         String fullName = fullEntryName;
464         if (fullEntryName.endsWith("/")) {
465             fullName = fullName.substring(0, fullEntryName.length() - 1);
466         }
467         return jarFile.getAbsolutePath() + "!/" + fullName;
468     }
469 }
470 
471 /***
472  * A delegate for a real resource link. Serves for grouping of multiple package names. Always a package.
473  * @author Martin Vysny
474  */
475 final class ResourceLinkGroup extends ResourceLink {
476 
477     private static final long serialVersionUID = 1L;
478     private final String name;
479     private final ResourceLink delegate;
480 
481     public ResourceLinkGroup(final String name, final ResourceLink delegate) {
482         this.name = name;
483         this.delegate = delegate;
484 
485     }
486 
487     @Override
488     public List<ResourceLink> list() throws IOException {
489         return delegate.list();
490     }
491 
492     @Override
493     public boolean isPackage() {
494         return true;
495     }
496 
497     @Override
498     public InputStream open() throws IOException {
499         throw new IOException("Not a file");
500     }
501 
502     @Override
503     public String getName() {
504         return name;
505     }
506 
507     @Override
508     public long getLength() {
509         return -1;
510     }
511 
512     @Override
513     public boolean isRoot() {
514         return false;
515     }
516 
517     @Override
518     public File getContainer() {
519         return delegate.getContainer();
520     }
521 
522     @Override
523     public List<ResourceLink> search(String substring) throws IOException {
524         return delegate.search(substring);
525     }
526 
527     @Override
528     public String getFullName() {
529         return delegate.getFullName();
530     }
531 }