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
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 }