001/** 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.xbean.finder.archive; 018 019import java.io.File; 020import java.io.FileNotFoundException; 021import java.io.IOException; 022import java.io.InputStream; 023import java.net.MalformedURLException; 024import java.net.URL; 025import java.util.ArrayList; 026import java.util.Collections; 027import java.util.Comparator; 028import java.util.Enumeration; 029import java.util.Iterator; 030import java.util.List; 031import java.util.NoSuchElementException; 032import java.util.jar.JarEntry; 033import java.util.jar.JarFile; 034import java.util.jar.Manifest; 035import java.util.zip.ZipEntry; 036 037/** 038 * @version $Rev$ $Date$ 039 */ 040public class JarArchive implements Archive, AutoCloseable { 041 042 private final ClassLoader loader; 043 private final URL url; 044 private final JarFile jar; 045 private final MJarSupport mjar = new MJarSupport(); 046 047 /* 048 * Supports only 'file:/...' or 'jar:file:/...!/' URLs 049 */ 050 public JarArchive(ClassLoader loader, URL url){ 051// if (!"jar".equals(url.getProtocol())) throw new IllegalArgumentException("not a jar url: " + url); 052 053 this.loader = loader; 054 this.url = url; 055 File jarFile = null; 056 String jarPath; 057 int idx; 058 059 // Wipe out 'jar:' prefix AND '!/{...}' suffix(if any) 060 if("jar".equalsIgnoreCase(url.getProtocol())){ 061 062 try{ 063 jarPath = url.getPath(); 064 url = new URL(jarPath.endsWith("!/") ? 065 jarPath.substring(0, jarPath.lastIndexOf("!/")) 066 : jarPath); 067 }catch(MalformedURLException ex){ 068 throw new IllegalArgumentException( 069 "Please provide 'file:/...' or 'jar:file:/...!/' URL" 070 + " instead of '" + FileArchive.decode(String.valueOf(url)) + "'"); 071 } 072 } 073 074 try{ 075 // handle 'file:/...' URL 076 if("file".equalsIgnoreCase(url.getProtocol())){ 077 078 // Testing if file DOEN't exists AND trying 079 // substrings up to every '!/{...}' as path 080 idx = 0; 081 jarPath = FileArchive.decode(url.getPath()); 082 for(String jp = jarPath; !(jarFile = new File(jp)).exists() 083 && (idx = jarPath.indexOf("!/", idx + 1)) > 0; 084 jp = jarPath.substring(0, idx)){} 085 086 // All substrings attempted, but referenced file wasn't discovered 087 if(!jarFile.exists()){ 088 089 // To be caught later and wrapped into IllegalStateEx - default behavior 090 throw new FileNotFoundException(FileArchive.decode(String.valueOf(url))); 091 } 092 093 }else{ 094 throw new IllegalArgumentException( 095 "Please provide 'file:/...' or 'jar:file:/...!/' URL" 096 + " instead of '" + FileArchive.decode(String.valueOf(url)) + "'"); 097 } 098 099 jar = new JarFile(jarFile); 100 101 }catch(IOException e){ 102 throw new IllegalStateException("Cannot open jar(zip) '" 103 + jarFile != null ? // why can it be null? but since compiler thinks so... 104 jarFile.getAbsolutePath() 105 : FileArchive.decode(String.valueOf(url)) + "'", e); 106 } 107 } 108 109 public URL getUrl() { 110 return url; 111 } 112 113 public InputStream getBytecode(String className) throws IOException, ClassNotFoundException { 114 int pos = className.indexOf("<"); 115 if (pos > -1) { 116 className = className.substring(0, pos); 117 } 118 pos = className.indexOf(">"); 119 if (pos > -1) { 120 className = className.substring(0, pos); 121 } 122 if (!className.endsWith(".class")) { 123 className = className.replace('.', '/') + ".class"; 124 } 125 126 ZipEntry entry = jar.getEntry(className); 127 if (entry == null) throw new ClassNotFoundException(className); 128 129 return jar.getInputStream(entry); 130 } 131 132 133 public Class<?> loadClass(String className) throws ClassNotFoundException { 134 // assume the loader knows how to handle mjar release if activated 135 return loader.loadClass(className); 136 } 137 138 public Iterator<Entry> iterator() { 139 return new JarIterator(); 140 } 141 142 @Override 143 public void close() throws Exception { 144 jar.close(); 145 } 146 147 private class JarIterator implements Iterator<Entry> { 148 149 private final Iterator<JarEntry> stream; 150 private Entry next; 151 152 private JarIterator() { 153 final Enumeration<JarEntry> entries = jar.entries(); 154 try { 155 final Manifest manifest = jar.getManifest(); 156 if (manifest != null) { 157 mjar.load(manifest); 158 } 159 } catch (IOException e) { 160 // no-op 161 } 162 if (mjar.isMjar()) { // sort it to ensure we browse META-INF/versions first 163 final List<JarEntry> list = new ArrayList<JarEntry>(Collections.list(entries)); 164 Collections.sort(list, new Comparator<JarEntry>() { 165 public int compare(JarEntry o1, JarEntry o2) { 166 final String n2 = o2.getName(); 167 final String n1 = o1.getName(); 168 final boolean n1v = n1.startsWith("META-INF/versions/"); 169 final boolean n2v = n2.startsWith("META-INF/versions/"); 170 if (n1v && n2v) { 171 return n1.compareTo(n2); 172 } 173 if (n1v) { 174 return -1; 175 } 176 if (n2v) { 177 return 1; 178 } 179 try { 180 return Integer.parseInt(n2) - Integer.parseInt(n1); 181 } catch (final NumberFormatException nfe) { 182 return n2.compareTo(n1); 183 } 184 } 185 }); 186 stream = list.iterator(); 187 } else { 188 stream = Collections.list(entries).iterator(); 189 } 190 } 191 192 private boolean advance() { 193 if (next != null) { 194 return true; 195 } 196 while (stream.hasNext()) { 197 final JarEntry entry = stream.next(); 198 final String entryName = entry.getName(); 199 if (entry.isDirectory() || !entryName.endsWith(".class") || entryName.endsWith("module-info.class")) { 200 continue; 201 } 202 203 String className = entryName; 204 if (entryName.endsWith(".class")) { 205 className = className.substring(0, className.length() - 6); 206 } 207 if (className.contains(".")) { 208 continue; 209 } 210 211 if (entryName.startsWith("META-INF/versions/")) { // JarFile will handle it for us 212 continue; 213 } 214 215 next = new ClassEntry(entry, className.replace('/', '.')); 216 return true; 217 } 218 return false; 219 } 220 221 public boolean hasNext() { 222 return advance(); 223 } 224 225 public Entry next() { 226 if (!hasNext()) throw new NoSuchElementException(); 227 Entry entry = next; 228 next = null; 229 return entry; 230 } 231 232 public void remove() { 233 throw new UnsupportedOperationException("remove"); 234 } 235 236 private class ClassEntry implements Entry { 237 private final String name; 238 private final JarEntry entry; 239 240 private ClassEntry(JarEntry entry, String name) { 241 this.name = name; 242 this.entry = entry; 243 } 244 245 public String getName() { 246 return name; 247 } 248 249 public InputStream getBytecode() throws IOException { 250 if (mjar.isMjar()) { 251 // JarFile handles it for us :) 252 final ZipEntry entry = jar.getJarEntry(this.entry.getName()); 253 if (entry != null) { 254 return jar.getInputStream(entry); 255 } 256 } 257 return jar.getInputStream(entry); 258 } 259 } 260 } 261} 262