1 /*
   2  * Copyright (c) 2014, 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 
  26 package jdk.tools.jlink.internal;
  27 
  28 import java.io.IOException;
  29 import java.io.InputStream;
  30 import java.io.UncheckedIOException;
  31 import java.nio.file.Path;
  32 import java.util.HashSet;
  33 import java.util.List;
  34 import java.util.Map;
  35 import java.util.Objects;
  36 import java.util.Set;
  37 import java.util.jar.JarEntry;
  38 import java.util.jar.JarFile;
  39 import java.util.stream.Collectors;
  40 import java.util.stream.Stream;
  41 import java.util.zip.ZipFile;
  42 
  43 import jdk.tools.jlink.internal.Archive.Entry.EntryType;
  44 
  45 /**
  46  * An Archive backed by a jar file.
  47  */
  48 public abstract class JarArchive implements Archive {
  49 
  50     /**
  51      * An entry located in a jar file.
  52      */
  53     private class JarFileEntry extends Entry {
  54 
  55         private final long size;
  56         private final JarEntry entry;
  57         private final JarFile file;
  58 
  59         JarFileEntry(String path, String name, EntryType type, JarFile file, JarEntry entry) {
  60             super(JarArchive.this, path, name, type);
  61             this.entry = Objects.requireNonNull(entry);
  62             this.file = Objects.requireNonNull(file);
  63             size = entry.getSize();
  64         }
  65 
  66         /**
  67          * Returns the number of uncompressed bytes for this entry.
  68          */
  69         @Override
  70         public long size() {
  71             return size;
  72         }
  73 
  74         @Override
  75         public InputStream stream() throws IOException {
  76             return file.getInputStream(entry);
  77         }
  78     }
  79 
  80     private static final String MODULE_INFO = "module-info.class";
  81 
  82     private final Path file;
  83     private final String moduleName;
  84     // currently processed JarFile
  85     private JarFile jarFile;
  86 
  87     protected JarArchive(String mn, Path file) {
  88         Objects.requireNonNull(mn);
  89         Objects.requireNonNull(file);
  90         this.moduleName = mn;
  91         this.file = file;
  92     }
  93 
  94     @Override
  95     public String moduleName() {
  96         return moduleName;
  97     }
  98 
  99     @Override
 100     public Path getPath() {
 101         return file;
 102     }
 103 
 104     @Override
 105     public Stream<Entry> entries() {
 106         try {
 107             if (jarFile == null) {
 108                 open();
 109             }
 110         } catch (IOException ioe) {
 111             throw new UncheckedIOException(ioe);
 112         }
 113         return versionedStream(jarFile).map(this::toEntry).filter(n -> n != null);
 114     }
 115 
 116     abstract EntryType toEntryType(String entryName);
 117 
 118     abstract String getFileName(String entryName);
 119 
 120     private Entry toEntry(JarEntry je) {
 121         String name = je.getName();
 122         String fn = getFileName(name);
 123 
 124         if (je.isDirectory() || fn.startsWith("_")) {
 125             return null;
 126         }
 127 
 128         EntryType rt = toEntryType(name);
 129 
 130         if (fn.equals(MODULE_INFO)) {
 131             fn = moduleName + "/" + MODULE_INFO;
 132         }
 133         return new JarFileEntry(je.getName(), fn, rt, jarFile, je);
 134     }
 135 
 136     private Stream<JarEntry> versionedStream(JarFile jf) {
 137         if (!jf.isMultiRelease()) {
 138             return jf.stream();
 139         }
 140 
 141         class Name {
 142             private static final String VERSIONS_DIR = "META-INF/versions/";
 143             private static final int VERSIONS_DIR_LEN = 18; // VERSIONS_DIR.length();
 144             private final int version;
 145             private final String name;
 146 
 147             Name(JarEntry je) {
 148                 String name = je.getName();
 149                 if (name.startsWith(VERSIONS_DIR)) {
 150                     name = name.substring(VERSIONS_DIR_LEN);
 151                     int offset = name.indexOf('/');
 152                     if (offset == -1)
 153                         throw new RuntimeException("Can not obtain version in multi-release jar");
 154                     this.version = Integer.parseInt(name.substring(0, offset));
 155                     this.name = name.substring(offset + 1);
 156                 } else {
 157                     this.name = name;
 158                     this.version = JarFile.baseVersion().major();
 159                 }
 160             }
 161 
 162             public String toString() {
 163                 return name;
 164             }
 165         }
 166 
 167         int version = jf.getVersion().major();
 168         int base = JarFile.baseVersion().major();
 169         Set<String> finalNames = new HashSet<>();
 170 
 171         Map<Integer, List<Name>> versionsMap = jf
 172                 .stream()
 173                 .filter(je -> !je.isDirectory())
 174                 .filter(je -> !je.getName().equals("META-INF/MANIFEST.MF"))
 175                 .map(je -> new Name(je))
 176                 .filter(nm -> nm.version <= version)
 177                 .collect(Collectors.groupingBy(name -> name.version));
 178 
 179         // a legal multi-release jar always has a base entry
 180         Set<String> baseNames = versionsMap.get(base).stream()
 181                 .map(nm -> nm.name)
 182                 .collect(Collectors.toSet());
 183 
 184         versionsMap.remove(base);
 185 
 186         finalNames.addAll(baseNames);
 187 
 188         versionsMap.keySet().forEach(v -> {
 189             Stream<String> names = versionsMap.get(v).stream().map(nm -> nm.name);
 190             if (v == version) {
 191                 finalNames.addAll(names.collect(Collectors.toSet()));
 192             } else {
 193                 finalNames.addAll(
 194                         names.filter(nm -> {
 195                             if (nm.endsWith(".class")) {
 196                                 return baseNames.contains(nm);
 197                             } else {
 198                                 // resource entries are "promoted"
 199                                 return true;
 200                             }
 201                         }).collect(Collectors.toSet())
 202                 );
 203             }
 204         });
 205 
 206         return finalNames.stream().map(nm -> jf.getJarEntry(nm));
 207     }
 208 
 209     @Override
 210     public void close() throws IOException {
 211         if (jarFile != null) {
 212             jarFile.close();
 213         }
 214     }
 215 
 216     @Override
 217     public void open() throws IOException {
 218         if (jarFile != null) {
 219             jarFile.close();
 220         }
 221         jarFile = new JarFile(file.toFile(), true, ZipFile.OPEN_READ, JarFile.runtimeVersion());
 222     }
 223 }