1 /*
   2  * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 package jdk.internal.jimage;
  26 
  27 import java.io.IOException;
  28 import java.io.InputStream;
  29 import java.io.UncheckedIOException;
  30 import java.nio.ByteBuffer;
  31 import java.nio.ByteOrder;
  32 import java.nio.IntBuffer;
  33 import java.nio.file.Files;
  34 import java.nio.file.attribute.BasicFileAttributes;
  35 import java.nio.file.attribute.FileTime;
  36 import java.nio.file.Path;
  37 import java.util.ArrayList;
  38 import java.util.Collections;
  39 import java.util.HashMap;
  40 import java.util.HashSet;
  41 import java.util.List;
  42 import java.util.Map;
  43 import java.util.Objects;
  44 import java.util.Set;
  45 import java.util.function.Consumer;
  46 
  47 /**
  48  * @implNote This class needs to maintain JDK 8 source compatibility.
  49  *
  50  * It is used internally in the JDK to implement jimage/jrtfs access,
  51  * but also compiled and delivered as part of the jrtfs.jar to support access
  52  * to the jimage file provided by the shipped JDK by tools running on JDK 8.
  53  */
  54 public final class ImageReader implements AutoCloseable {
  55     private SharedImageReader reader;
  56 
  57     private ImageReader(SharedImageReader reader) {
  58         this.reader = reader;
  59     }
  60 
  61     public static ImageReader open(Path imagePath, ByteOrder byteOrder) throws IOException {
  62         Objects.requireNonNull(imagePath);
  63         Objects.requireNonNull(byteOrder);
  64         
  65         return SharedImageReader.open(imagePath, byteOrder);
  66     }
  67 
  68     public static ImageReader open(Path imagePath) throws IOException {
  69         return open(imagePath, ByteOrder.nativeOrder());
  70     }
  71 
  72     @Override
  73     public void close() throws IOException {
  74         if (reader == null) {
  75             throw new IOException("image file already closed");
  76         }
  77 
  78         reader.close(this);
  79         reader = null;
  80     }
  81 
  82     // directory management interface
  83     public Directory getRootDirectory() throws IOException {
  84         if (reader == null) {
  85             throw new IOException("image file closed");
  86         }
  87         return reader.getRootDirectory();
  88     }
  89 
  90     public Node findNode(String name) throws IOException {
  91         if (reader == null) {
  92             throw new IOException("image file closed");
  93         }
  94         return reader.findNode(name);
  95     }
  96 
  97     public byte[] getResource(Node node) throws IOException {
  98         if (reader == null) {
  99             throw new IOException("image file closed");
 100         }
 101         return reader.getResource(node);
 102     }
 103 
 104     public byte[] getResource(Resource rs) throws IOException {
 105         if (reader == null) {
 106             throw new IOException("image file closed");
 107         }
 108         return reader.getResource(rs);
 109     }
 110 
 111     public ImageHeader getHeader() {
 112         Objects.requireNonNull(reader, "image file closed");
 113         return reader.getHeader();
 114     }
 115 
 116     public static void releaseByteBuffer(ByteBuffer buffer) {
 117         BasicImageReader.releaseByteBuffer(buffer);
 118     }
 119 
 120     public String getName() {
 121         Objects.requireNonNull(reader, "image file closed");
 122         return reader.getName() ;
 123     }
 124 
 125     public ByteOrder getByteOrder() {
 126         Objects.requireNonNull(reader, "image file closed");
 127         return reader.getByteOrder();
 128     }
 129 
 130     public Path getImagePath() {
 131         Objects.requireNonNull(reader, "image file closed");
 132         return reader.getImagePath();
 133     }
 134 
 135     public ImageStringsReader getStrings() {
 136         Objects.requireNonNull(reader, "image file closed");
 137         return reader.getStrings();
 138     }
 139 
 140     public ImageLocation findLocation(String mn, String rn) {
 141         Objects.requireNonNull(reader, "image file closed");
 142         return reader.findLocation(mn, rn);
 143     }
 144 
 145     public ImageLocation findLocation(String name) {
 146         Objects.requireNonNull(reader, "image file closed");
 147         return reader.findLocation(name);
 148     }
 149 
 150     public String[] getEntryNames() {
 151         Objects.requireNonNull(reader, "image file closed");
 152         return reader.getEntryNames();
 153     }
 154 
 155     public String[] getModuleNames() {
 156         Objects.requireNonNull(reader, "image file closed");
 157         int off = "/modules/".length();
 158         return reader.findNode("/modules")
 159                      .getChildren()
 160                      .stream()
 161                      .map(Node::getNameString)
 162                      .map(s -> s.substring(off, s.length()))
 163                      .toArray(String[]::new);
 164     }
 165 
 166     public long[] getAttributes(int offset) {
 167         Objects.requireNonNull(reader, "image file closed");
 168         return reader.getAttributes(offset);
 169     }
 170 
 171     public String getString(int offset) {
 172         Objects.requireNonNull(reader, "image file closed");
 173         return reader.getString(offset);
 174     }
 175 
 176     public byte[] getResource(String name) {
 177         Objects.requireNonNull(reader, "image file closed");
 178         return reader.getResource(name);
 179     }
 180 
 181     public byte[] getResource(ImageLocation loc) {
 182         Objects.requireNonNull(reader, "image file closed");
 183         return reader.getResource(loc);
 184     }
 185 
 186     public ByteBuffer getResourceBuffer(ImageLocation loc) {
 187         Objects.requireNonNull(reader, "image file closed");
 188         return reader.getResourceBuffer(loc);
 189     }
 190 
 191     public InputStream getResourceStream(ImageLocation loc) {
 192         Objects.requireNonNull(reader, "image file closed");
 193         return reader.getResourceStream(loc);
 194     }
 195 
 196     private final static class SharedImageReader extends BasicImageReader {
 197         static final int SIZE_OF_OFFSET = Integer.BYTES;
 198 
 199         static final Map<Path, SharedImageReader> OPEN_FILES = new HashMap<>();
 200 
 201         // List of openers for this shared image.
 202         final Set<ImageReader> openers;
 203 
 204         // attributes of the .jimage file. jimage file does not contain
 205         // attributes for the individual resources (yet). We use attributes
 206         // of the jimage file itself (creation, modification, access times).
 207         // Iniitalized lazily, see {@link #imageFileAttributes()}.
 208         BasicFileAttributes imageFileAttributes;
 209 
 210         // directory management implementation
 211         final HashMap<String, Node> nodes;
 212         volatile Directory rootDir;
 213 
 214         Directory packagesDir;
 215         Directory modulesDir;
 216 
 217         private SharedImageReader(Path imagePath, ByteOrder byteOrder) throws IOException {
 218             super(imagePath, byteOrder);
 219             this.openers = new HashSet<>();
 220             this.nodes = new HashMap<>();
 221         }
 222 
 223         public static ImageReader open(Path imagePath, ByteOrder byteOrder) throws IOException {
 224             Objects.requireNonNull(imagePath);
 225             Objects.requireNonNull(byteOrder);
 226             
 227             synchronized (OPEN_FILES) {
 228                 SharedImageReader reader = OPEN_FILES.get(imagePath);
 229 
 230                 if (reader == null) {
 231                     // Will fail with an IOException if wrong byteOrder.
 232                     reader =  new SharedImageReader(imagePath, byteOrder);
 233                     OPEN_FILES.put(imagePath, reader);
 234                 } else if (reader.getByteOrder() != byteOrder) {
 235                     throw new IOException("\"" + reader.getName() + "\" is not an image file");
 236                 }
 237 
 238                 ImageReader image = new ImageReader(reader);
 239                 reader.openers.add(image);
 240 
 241                 return image;
 242             }
 243         }
 244 
 245         public void close(ImageReader image) throws IOException {
 246             Objects.requireNonNull(image);
 247             
 248             synchronized (OPEN_FILES) {
 249                 if (!openers.remove(image)) {
 250                     throw new IOException("image file already closed");
 251                 }
 252 
 253                 if (openers.isEmpty()) {
 254                     close();
 255                     nodes.clear();
 256                     rootDir = null;
 257 
 258                     if (!OPEN_FILES.remove(this.getImagePath(), this)) {
 259                         throw new IOException("image file not found in open list");
 260                     }
 261                 }
 262             }
 263         }
 264 
 265         void addOpener(ImageReader reader) {
 266             synchronized (OPEN_FILES) {
 267                 openers.add(reader);
 268             }
 269         }
 270 
 271         boolean removeOpener(ImageReader reader) {
 272             synchronized (OPEN_FILES) {
 273                 return openers.remove(reader);
 274             }
 275         }
 276 
 277         // directory management interface
 278         Directory getRootDirectory() {
 279             return buildRootDirectory();
 280         }
 281 
 282         /**
 283          * Lazily build a node from a name.
 284         */
 285         synchronized Node buildNode(String name) {
 286             Node n;
 287             boolean isPackages = name.startsWith("/packages");
 288             boolean isModules = !isPackages && name.startsWith("/modules");
 289 
 290             if (!(isModules || isPackages)) {
 291                 return null;
 292             }
 293 
 294             ImageLocation loc = findLocation(name);
 295 
 296             if (loc != null) { // A sub tree node
 297                 if (isPackages) {
 298                     n = handlePackages(name, loc);
 299                 } else { // modules sub tree
 300                     n = handleModulesSubTree(name, loc);
 301                 }
 302             } else { // Asking for a resource? /modules/java.base/java/lang/Object.class
 303                 if (isModules) {
 304                     n = handleResource(name);
 305                 } else {
 306                     // Possibly ask for /packages/java.lang/java.base
 307                     // although /packages/java.base not created
 308                     n = handleModuleLink(name);
 309                 }
 310             }
 311             return n;
 312         }
 313 
 314         synchronized Directory buildRootDirectory() {
 315             Directory root = rootDir; // volatile read
 316             if (root != null) {
 317                 return root;
 318             }
 319 
 320             root = newDirectory(null, "/");
 321             root.setIsRootDir();
 322 
 323             // /packages dir
 324             packagesDir = newDirectory(root, "/packages");
 325             packagesDir.setIsPackagesDir();
 326 
 327             // /modules dir
 328             modulesDir = newDirectory(root, "/modules");
 329             modulesDir.setIsModulesDir();
 330 
 331             root.setCompleted(true);
 332             return rootDir = root;
 333         }
 334 
 335         /**
 336          * To visit sub tree resources.
 337          */
 338         interface LocationVisitor {
 339             void visit(ImageLocation loc);
 340         }
 341 
 342         void visitLocation(ImageLocation loc, LocationVisitor visitor) {
 343             byte[] offsets = getResource(loc);
 344             ByteBuffer buffer = ByteBuffer.wrap(offsets);
 345             buffer.order(getByteOrder());
 346             IntBuffer intBuffer = buffer.asIntBuffer();
 347             for (int i = 0; i < offsets.length / SIZE_OF_OFFSET; i++) {
 348                 int offset = intBuffer.get(i);
 349                 ImageLocation pkgLoc = getLocation(offset);
 350                 visitor.visit(pkgLoc);
 351             }
 352         }
 353 
 354         void visitPackageLocation(ImageLocation loc) {
 355             // Retrieve package name
 356             String pkgName = getBaseExt(loc);
 357             // Content is array of offsets in Strings table
 358             byte[] stringsOffsets = getResource(loc);
 359             ByteBuffer buffer = ByteBuffer.wrap(stringsOffsets);
 360             buffer.order(getByteOrder());
 361             IntBuffer intBuffer = buffer.asIntBuffer();
 362             // For each module, create a link node.
 363             for (int i = 0; i < stringsOffsets.length / SIZE_OF_OFFSET; i++) {
 364                 // skip empty state, useless.
 365                 intBuffer.get(i);
 366                 i++;
 367                 int offset = intBuffer.get(i);
 368                 String moduleName = getString(offset);
 369                 Node targetNode = findNode("/modules/" + moduleName);
 370                 if (targetNode != null) {
 371                     String pkgDirName = packagesDir.getName() + "/" + pkgName;
 372                     Directory pkgDir = (Directory) nodes.get(pkgDirName);
 373                     newLinkNode(pkgDir, pkgDir.getName() + "/" + moduleName, targetNode);
 374                 }
 375             }
 376         }
 377 
 378         Node handlePackages(String name, ImageLocation loc) {
 379             long size = loc.getUncompressedSize();
 380             Node n = null;
 381             // Only possiblities are /packages, /packages/package/module
 382             if (name.equals("/packages")) {
 383                 visitLocation(loc, (childloc) -> {
 384                     findNode(childloc.getFullName());
 385                 });
 386                 packagesDir.setCompleted(true);
 387                 n = packagesDir;
 388             } else {
 389                 if (size != 0) { // children are offsets to module in StringsTable
 390                     String pkgName = getBaseExt(loc);
 391                     Directory pkgDir = newDirectory(packagesDir, packagesDir.getName() + "/" + pkgName);
 392                     visitPackageLocation(loc);
 393                     pkgDir.setCompleted(true);
 394                     n = pkgDir;
 395                 } else { // Link to module
 396                     String pkgName = loc.getParent();
 397                     String modName = getBaseExt(loc);
 398                     Node targetNode = findNode("/modules/" + modName);
 399                     if (targetNode != null) {
 400                         String pkgDirName = packagesDir.getName() + "/" + pkgName;
 401                         Directory pkgDir = (Directory) nodes.get(pkgDirName);
 402                         Node linkNode = newLinkNode(pkgDir, pkgDir.getName() + "/" + modName, targetNode);
 403                         n = linkNode;
 404                     }
 405                 }
 406             }
 407             return n;
 408         }
 409 
 410         // Asking for /packages/package/module although
 411         // /packages/<pkg>/ not yet created, need to create it
 412         // prior to return the link to module node.
 413         Node handleModuleLink(String name) {
 414             // eg: unresolved /packages/package/module
 415             // Build /packages/package node
 416             Node ret = null;
 417             String radical = "/packages/";
 418             String path = name;
 419             if (path.startsWith(radical)) {
 420                 int start = radical.length();
 421                 int pkgEnd = path.indexOf('/', start);
 422                 if (pkgEnd != -1) {
 423                     String pkg = path.substring(start, pkgEnd);
 424                     String pkgPath = radical + pkg;
 425                     Node n = findNode(pkgPath);
 426                     // If not found means that this is a symbolic link such as:
 427                     // /packages/java.util/java.base/java/util/Vector.class
 428                     // and will be done by a retry of the filesystem
 429                     for (Node child : n.getChildren()) {
 430                         if (child.name.equals(name)) {
 431                             ret = child;
 432                             break;
 433                         }
 434                     }
 435                 }
 436             }
 437             return ret;
 438         }
 439 
 440         Node handleModulesSubTree(String name, ImageLocation loc) {
 441             Node n;
 442             assert (name.equals(loc.getFullName()));
 443             Directory dir = makeDirectories(name);
 444             visitLocation(loc, (childloc) -> {
 445                 String path = childloc.getFullName();
 446                 if (path.startsWith("/modules")) { // a package
 447                     makeDirectories(path);
 448                 } else { // a resource
 449                     makeDirectories(childloc.buildName(true, true, false));
 450                     newResource(dir, childloc);
 451                 }
 452             });
 453             dir.setCompleted(true);
 454             n = dir;
 455             return n;
 456         }
 457 
 458         Node handleResource(String name) {
 459             Node n = null;
 460             String locationPath = name.substring("/modules".length());
 461             ImageLocation resourceLoc = findLocation(locationPath);
 462             if (resourceLoc != null) {
 463                 Directory dir = makeDirectories(resourceLoc.buildName(true, true, false));
 464                 Resource res = newResource(dir, resourceLoc);
 465                 n = res;
 466             }
 467             return n;
 468         }
 469 
 470         String getBaseExt(ImageLocation loc) {
 471             String base = loc.getBase();
 472             String ext = loc.getExtension();
 473             if (ext != null && !ext.isEmpty()) {
 474                 base = base + "." + ext;
 475             }
 476             return base;
 477         }
 478 
 479         synchronized Node findNode(String name) {
 480             buildRootDirectory();
 481             Node n = nodes.get(name);
 482             if (n == null || !n.isCompleted()) {
 483                 n = buildNode(name);
 484             }
 485             return n;
 486         }
 487 
 488         /**
 489          * Returns the file attributes of the image file.
 490          */
 491         BasicFileAttributes imageFileAttributes() {
 492             BasicFileAttributes attrs = imageFileAttributes;
 493             if (attrs == null) {
 494                 try {
 495                     Path file = getImagePath();
 496                     attrs = Files.readAttributes(file, BasicFileAttributes.class);
 497                 } catch (IOException ioe) {
 498                     throw new UncheckedIOException(ioe);
 499                 }
 500                 imageFileAttributes = attrs;
 501             }
 502             return attrs;
 503         }
 504 
 505         Directory newDirectory(Directory parent, String name) {
 506             Directory dir = Directory.create(parent, name, imageFileAttributes());
 507             nodes.put(dir.getName(), dir);
 508             return dir;
 509         }
 510 
 511         Resource newResource(Directory parent, ImageLocation loc) {
 512             Resource res = Resource.create(parent, loc, imageFileAttributes());
 513             nodes.put(res.getName(), res);
 514             return res;
 515         }
 516 
 517         LinkNode newLinkNode(Directory dir, String name, Node link) {
 518             LinkNode linkNode = LinkNode.create(dir, name, link);
 519             nodes.put(linkNode.getName(), linkNode);
 520             return linkNode;
 521         }
 522 
 523         Directory makeDirectories(String parent) {
 524             Directory last = rootDir;
 525             for (int offset = parent.indexOf('/', 1);
 526                     offset != -1;
 527                     offset = parent.indexOf('/', offset + 1)) {
 528                 String dir = parent.substring(0, offset);
 529                 last = makeDirectory(dir, last);
 530             }
 531             return makeDirectory(parent, last);
 532 
 533         }
 534 
 535         Directory makeDirectory(String dir, Directory last) {
 536             Directory nextDir = (Directory) nodes.get(dir);
 537             if (nextDir == null) {
 538                 nextDir = newDirectory(last, dir);
 539             }
 540             return nextDir;
 541         }
 542 
 543         byte[] getResource(Node node) throws IOException {
 544             if (node.isResource()) {
 545                 return super.getResource(node.getLocation());
 546             }
 547             throw new IOException("Not a resource: " + node);
 548         }
 549 
 550         byte[] getResource(Resource rs) throws IOException {
 551             return super.getResource(rs.getLocation());
 552         }
 553     }
 554 
 555     // jimage file does not store directory structure. We build nodes
 556     // using the "path" strings found in the jimage file.
 557     // Node can be a directory or a resource
 558     public abstract static class Node {
 559         private static final int ROOT_DIR = 0b0000_0000_0000_0001;
 560         private static final int PACKAGES_DIR = 0b0000_0000_0000_0010;
 561         private static final int MODULES_DIR = 0b0000_0000_0000_0100;
 562 
 563         private int flags;
 564         private final String name;
 565         private final BasicFileAttributes fileAttrs;
 566         private boolean completed;
 567 
 568         protected Node(String name, BasicFileAttributes fileAttrs) {
 569             this.name = Objects.requireNonNull(name);
 570             this.fileAttrs = Objects.requireNonNull(fileAttrs);
 571         }
 572 
 573         /**
 574          * A node is completed when all its direct children have been built.
 575          *
 576          * @return
 577          */
 578         public boolean isCompleted() {
 579             return completed;
 580         }
 581 
 582         public void setCompleted(boolean completed) {
 583             this.completed = completed;
 584         }
 585 
 586         public final void setIsRootDir() {
 587             flags |= ROOT_DIR;
 588         }
 589 
 590         public final boolean isRootDir() {
 591             return (flags & ROOT_DIR) != 0;
 592         }
 593 
 594         public final void setIsPackagesDir() {
 595             flags |= PACKAGES_DIR;
 596         }
 597 
 598         public final boolean isPackagesDir() {
 599             return (flags & PACKAGES_DIR) != 0;
 600         }
 601 
 602         public final void setIsModulesDir() {
 603             flags |= MODULES_DIR;
 604         }
 605 
 606         public final boolean isModulesDir() {
 607             return (flags & MODULES_DIR) != 0;
 608         }
 609 
 610         public final String getName() {
 611             return name;
 612         }
 613 
 614         public final BasicFileAttributes getFileAttributes() {
 615             return fileAttrs;
 616         }
 617 
 618         // resolve this Node (if this is a soft link, get underlying Node)
 619         public final Node resolveLink() {
 620             return resolveLink(false);
 621         }
 622 
 623         public Node resolveLink(boolean recursive) {
 624             return this;
 625         }
 626 
 627         // is this a soft link Node?
 628         public boolean isLink() {
 629             return false;
 630         }
 631 
 632         public boolean isDirectory() {
 633             return false;
 634         }
 635 
 636         public List<Node> getChildren() {
 637             throw new IllegalArgumentException("not a directory: " + getNameString());
 638         }
 639 
 640         public boolean isResource() {
 641             return false;
 642         }
 643 
 644         public ImageLocation getLocation() {
 645             throw new IllegalArgumentException("not a resource: " + getNameString());
 646         }
 647 
 648         public long size() {
 649             return 0L;
 650         }
 651 
 652         public long compressedSize() {
 653             return 0L;
 654         }
 655 
 656         public String extension() {
 657             return null;
 658         }
 659 
 660         public long contentOffset() {
 661             return 0L;
 662         }
 663 
 664         public final FileTime creationTime() {
 665             return fileAttrs.creationTime();
 666         }
 667 
 668         public final FileTime lastAccessTime() {
 669             return fileAttrs.lastAccessTime();
 670         }
 671 
 672         public final FileTime lastModifiedTime() {
 673             return fileAttrs.lastModifiedTime();
 674         }
 675 
 676         public final String getNameString() {
 677             return name;
 678         }
 679 
 680         @Override
 681         public final String toString() {
 682             return getNameString();
 683         }
 684 
 685         @Override
 686         public final int hashCode() {
 687             return name.hashCode();
 688         }
 689 
 690         @Override
 691         public final boolean equals(Object other) {
 692             if (this == other) {
 693                 return true;
 694             }
 695 
 696             if (other instanceof Node) {
 697                 return name.equals(((Node) other).name);
 698             }
 699 
 700             return false;
 701         }
 702     }
 703 
 704     // directory node - directory has full path name without '/' at end.
 705     static final class Directory extends Node {
 706         private final List<Node> children;
 707 
 708         private Directory(String name, BasicFileAttributes fileAttrs) {
 709             super(name, fileAttrs);
 710             children = new ArrayList<>();
 711         }
 712 
 713         static Directory create(Directory parent, String name, BasicFileAttributes fileAttrs) {
 714             Directory d = new Directory(name, fileAttrs);
 715             if (parent != null) {
 716                 parent.addChild(d);
 717             }
 718             return d;
 719         }
 720 
 721         @Override
 722         public boolean isDirectory() {
 723             return true;
 724         }
 725 
 726         @Override
 727         public List<Node> getChildren() {
 728             return Collections.unmodifiableList(children);
 729         }
 730 
 731         void addChild(Node node) {
 732             children.add(node);
 733         }
 734 
 735         public void walk(Consumer<? super Node> consumer) {
 736             consumer.accept(this);
 737             for ( Node child : children ) {
 738                 if (child.isDirectory()) {
 739                     ((Directory)child).walk(consumer);
 740                 } else {
 741                     consumer.accept(child);
 742                 }
 743             }
 744         }
 745     }
 746 
 747     // "resource" is .class or any other resource (compressed/uncompressed) in a jimage.
 748     // full path of the resource is the "name" of the resource.
 749     static class Resource extends Node {
 750         private final ImageLocation loc;
 751 
 752         private Resource(ImageLocation loc, BasicFileAttributes fileAttrs) {
 753             super(loc.getFullName(true), fileAttrs);
 754             this.loc = loc;
 755         }
 756 
 757         static Resource create(Directory parent, ImageLocation loc, BasicFileAttributes fileAttrs) {
 758             Resource rs = new Resource(loc, fileAttrs);
 759             parent.addChild(rs);
 760             return rs;
 761         }
 762 
 763         @Override
 764         public boolean isCompleted() {
 765             return true;
 766         }
 767 
 768         @Override
 769         public boolean isResource() {
 770             return true;
 771         }
 772 
 773         @Override
 774         public ImageLocation getLocation() {
 775             return loc;
 776         }
 777 
 778         @Override
 779         public long size() {
 780             return loc.getUncompressedSize();
 781         }
 782 
 783         @Override
 784         public long compressedSize() {
 785             return loc.getCompressedSize();
 786         }
 787 
 788         @Override
 789         public String extension() {
 790             return loc.getExtension();
 791         }
 792 
 793         @Override
 794         public long contentOffset() {
 795             return loc.getContentOffset();
 796         }
 797     }
 798 
 799     // represents a soft link to another Node
 800     static class LinkNode extends Node {
 801         private final Node link;
 802 
 803         private LinkNode(String name, Node link) {
 804             super(name, link.getFileAttributes());
 805             this.link = link;
 806         }
 807 
 808         static LinkNode create(Directory parent, String name, Node link) {
 809             LinkNode ln = new LinkNode(name, link);
 810             parent.addChild(ln);
 811             return ln;
 812         }
 813 
 814         @Override
 815         public boolean isCompleted() {
 816             return true;
 817         }
 818 
 819         @Override
 820         public Node resolveLink(boolean recursive) {
 821             return (recursive && link instanceof LinkNode) ? ((LinkNode)link).resolveLink(true) : link;
 822         }
 823 
 824         @Override
 825         public boolean isLink() {
 826             return true;
 827         }
 828     }
 829 }