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.Comparator; 33 import java.util.Map; 34 import java.util.Objects; 35 import java.util.jar.JarEntry; 36 import java.util.jar.JarFile; 37 import java.util.stream.Collectors; 38 import java.util.stream.Stream; 39 import java.util.zip.ZipFile; 40 import jdk.tools.jlink.internal.Archive.Entry.EntryType; 41 42 /** 43 * An Archive backed by a jar file. 44 */ 45 public abstract class JarArchive implements Archive { 46 47 /** 48 * An entry located in a jar file. 49 */ 50 private class JarFileEntry extends Entry { 51 52 private final long size; 53 private final JarEntry entry; 54 private final JarFile file; 55 56 JarFileEntry(String path, String name, EntryType type, JarFile file, JarEntry entry) { 57 super(JarArchive.this, path, name, type); 58 this.entry = Objects.requireNonNull(entry); 59 this.file = Objects.requireNonNull(file); 60 size = entry.getSize(); 61 } 62 63 /** 64 * Returns the number of uncompressed bytes for this entry. 65 */ 66 @Override 67 public long size() { 68 return size; 69 } 70 71 @Override 72 public InputStream stream() throws IOException { 73 return file.getInputStream(entry); 74 } 75 } 76 77 private static final String MANIFEST = "META-INF/MANIFEST.MF"; 78 private static final String MODULE_INFO = "module-info.class"; 79 private static final String VERSIONS_DIR = "META-INF/versions/"; 80 private static final int VERSIONS_DIR_LEN = VERSIONS_DIR.length(); 81 82 private final Path file; 83 private final String moduleName; 84 // currently processed JarFile 85 private JarFile jarFile; 86 private int version; 87 private int offset; 88 89 protected JarArchive(String mn, Path file) { 90 Objects.requireNonNull(mn); 91 Objects.requireNonNull(file); 92 this.moduleName = mn; 93 this.file = file; 94 } 95 96 @Override 97 public String moduleName() { 98 return moduleName; 99 } 100 101 @Override 102 public Path getPath() { 103 return file; 104 } 105 106 @Override 107 public Stream<Entry> entries() { 108 try { 109 if (jarFile == null) { 110 open(); 111 } 112 } catch (IOException ioe) { 113 throw new UncheckedIOException(ioe); 114 } 115 if (jarFile.isMultiRelease()) { 116 // sort entries in ascending version order, extract the base name 117 // and add them to a map, then return the Entries as with regular jar 118 return jarFile.stream() 119 .filter(je -> !je.isDirectory()) 120 .filter(je -> !je.getName().equals(MANIFEST)) 121 .filter(this::versionAcceptable) 122 .sorted(entryComparator) 123 .collect(Collectors.toMap(this::extractBaseName, je -> je, (v1, v2) -> v2)) 124 .entrySet() 125 .stream().map(this::toEntry).filter(n -> n != null); 126 } 127 return jarFile.stream().map(this::toEntry).filter(n -> n != null); 128 } 129 130 abstract EntryType toEntryType(String entryName); 131 132 abstract String getFileName(String entryName); 133 134 // sort base entries before versioned entries 135 private Comparator<JarEntry> entryComparator = (je1, je2) -> { 136 String s1 = je1.getName(); 137 String s2 = je2.getName(); 138 if (s1.equals(s2)) return 0; 139 boolean b1 = s1.startsWith(VERSIONS_DIR); 140 boolean b2 = s2.startsWith(VERSIONS_DIR); 141 if (b1 && !b2) return 1; 142 if (!b1 && b2) return -1; 143 int n = 0; // starting char for String compare 144 if (b1 && b2) { 145 // normally strings would be sorted so "10" goes before "9", but 146 // version number strings need to be sorted numerically 147 n = VERSIONS_DIR.length(); // skip the common prefix 148 int i1 = s1.indexOf('/', n); 149 int i2 = s1.indexOf('/', n); 150 if (i1 == -1) throw new RuntimeException(s1); // fixme, better message 151 if (i2 == -1) throw new RuntimeException(s2); // fixme, better message 152 // shorter version numbers go first 153 if (i1 != i2) return i1 - i2; 154 // otherwise, handle equal length numbers below 155 } 156 int l1 = s1.length(); 157 int l2 = s2.length(); 158 int lim = Math.min(l1, l2); 159 for (int k = n; k < lim; k++) { 160 char c1 = s1.charAt(k); 161 char c2 = s2.charAt(k); 162 if (c1 != c2) { 163 return c1 - c2; 164 } 165 } 166 return l1 - l2; 167 }; 168 169 // must be invoked after versionAcceptable 170 private String extractBaseName(JarEntry je) { 171 String name = je.getName(); 172 if (name.startsWith(VERSIONS_DIR)) { 173 return name.substring(VERSIONS_DIR_LEN + offset + 1); 174 } 175 return name; 176 } 177 178 private Entry toEntry(JarEntry je) { 179 String name = je.getName(); 180 return toEntry(name, je); 181 } 182 183 private Entry toEntry(Map.Entry<String,JarEntry> entry) { 184 String name = entry.getKey(); 185 JarEntry je = entry.getValue(); 186 return toEntry(name, je); 187 } 188 189 private Entry toEntry(String name, JarEntry je) { 190 String fn = getFileName(name); 191 192 if (je.isDirectory() || fn.startsWith("_")) { 193 return null; 194 } 195 196 EntryType rt = toEntryType(name); 197 198 if (fn.equals(MODULE_INFO)) { 199 fn = moduleName + "/" + MODULE_INFO; 200 } 201 return new JarFileEntry(name, fn, rt, jarFile, je); 202 } 203 204 // must be invoked before extractBaseName 205 private boolean versionAcceptable(JarEntry je) { 206 String name = je.getName(); 207 if (name.startsWith(VERSIONS_DIR)) { 208 name = name.substring(VERSIONS_DIR_LEN); 209 offset = name.indexOf('/'); 210 if (offset == -1) 211 throw new RuntimeException("");// fixme, better message 212 if (Integer.parseInt(name.substring(0, offset)) <= version) { 213 return true; 214 } 215 return false; 216 } 217 return true; 218 } 219 220 @Override 221 public void close() throws IOException { 222 if (jarFile != null) { 223 jarFile.close(); 224 } 225 } 226 227 @Override 228 public void open() throws IOException { 229 if (jarFile != null) { 230 jarFile.close(); 231 } 232 // open this way to determine if it's a multi-release jar 233 jarFile = new JarFile(file.toFile(), true, ZipFile.OPEN_READ, JarFile.runtimeVersion()); 234 version = jarFile.getVersion().major(); 235 } 236 }