001 /* 002 * Copyright (C) 2012 eXo Platform SAS. 003 * 004 * This is free software; you can redistribute it and/or modify it 005 * under the terms of the GNU Lesser General Public License as 006 * published by the Free Software Foundation; either version 2.1 of 007 * the License, or (at your option) any later version. 008 * 009 * This software is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * You should have received a copy of the GNU Lesser General Public 015 * License along with this software; if not, write to the Free 016 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 017 * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 018 */ 019 020 package org.crsh.vfs.spi.url; 021 022 import org.crsh.util.InputStreamFactory; 023 import org.crsh.util.Utils; 024 import org.crsh.util.ZipIterator; 025 026 import java.io.File; 027 import java.io.FileInputStream; 028 import java.io.IOException; 029 import java.io.InputStream; 030 import java.net.URISyntaxException; 031 import java.net.URL; 032 import java.util.ArrayList; 033 import java.util.Arrays; 034 import java.util.Collections; 035 import java.util.Enumeration; 036 import java.util.HashMap; 037 import java.util.Iterator; 038 import java.util.LinkedList; 039 import java.util.List; 040 import java.util.zip.ZipEntry; 041 042 /** @author <a href="mailto:julien.viet@exoplatform.com">Julien Viet</a> */ 043 public class Node implements Iterable<Resource> { 044 045 /** . */ 046 private static final File[] EMPTY = new File[0]; 047 048 /** . */ 049 public final String name; 050 051 /** The lazy dires not yet processed. */ 052 File[] dirs = EMPTY; 053 054 /** . */ 055 HashMap<String, Node> children = new HashMap<String, Node>(); 056 057 /** . */ 058 LinkedList<Resource> resources = new LinkedList<Resource>(); 059 060 public Node() { 061 this.name = ""; 062 } 063 064 private Node(String name) { 065 this.name = name; 066 } 067 068 void merge(ClassLoader loader) throws IOException, URISyntaxException { 069 070 // Get the root class path files 071 for (Enumeration<URL> i = loader.getResources("");i.hasMoreElements();) { 072 URL url = i.nextElement(); 073 // In some case we can get null (Tomcat 8) 074 if (url != null) { 075 mergeEntries(url); 076 } 077 } 078 ArrayList<URL> items = Collections.list(loader.getResources("META-INF/MANIFEST.MF")); 079 for (URL item : items) { 080 if ("jar".equals(item.getProtocol())) { 081 String path = item.getPath(); 082 int pos = path.lastIndexOf("!/"); 083 URL url = new URL("jar:" + path.substring(0, pos + 2)); 084 mergeEntries(url); 085 } 086 else { 087 // 088 } 089 } 090 } 091 092 /** 093 * Rewrite an URL by analysing the serie of trailing <code>!/</code>. The number of <code>jar:</code> prefixes 094 * does not have to be equals to the number of separators. 095 * 096 * @param url the url to rewrite 097 * @return the rewritten URL 098 */ 099 String rewrite(String url) { 100 int end = url.lastIndexOf("!/"); 101 if (end >= 0) { 102 String entry = url.substring(end + 2); 103 int start = url.indexOf(':'); 104 String protocol = url.substring(0, start); 105 String nestedURL; 106 if (protocol.equals("jar")) { 107 nestedURL = rewrite(url.substring(start + 1, end)); 108 return "jar:" + nestedURL + "!/" + entry; 109 } else { 110 nestedURL = rewrite(url.substring(0, end)); 111 } 112 return "jar:" + nestedURL + "!/" + entry; 113 } else { 114 return url; 115 } 116 } 117 118 Iterable<Node> children() throws IOException { 119 // Lazy merge the dirs when accessing this node 120 // it is not only important for performance reason but in some case 121 // the classpath may contain an exploded dir that see the the whole file system 122 // and the full scan is an issue 123 while (true) { 124 int length = dirs.length; 125 if (length > 0) { 126 File dir = dirs[length - 1]; 127 dirs = Arrays.copyOf(dirs, length - 1); 128 merge(dir); 129 } else { 130 break; 131 } 132 } 133 return children.values(); 134 } 135 136 void mergeEntries(URL url) throws IOException, URISyntaxException { 137 // We handle a special case of spring-boot URLs here before diving in the recursive analysis 138 // see https://github.com/spring-projects/spring-boot/tree/master/spring-boot-tools/spring-boot-loader#urls 139 if (url.getProtocol().equals("jar")) { 140 url = new URL(rewrite(url.toString())); 141 } 142 _mergeEntries(url); 143 } 144 145 private void _mergeEntries(URL url) throws IOException, URISyntaxException { 146 if (url.getProtocol().equals("file")) { 147 try { 148 java.io.File f = Utils.toFile(url); 149 if (f.isDirectory()) { 150 merge(f); 151 } else if (f.getName().endsWith(".jar")) { 152 mergeEntries(new URL("jar:" + url + "!/")); 153 } else { 154 // WTF ? 155 } 156 } 157 catch (URISyntaxException e) { 158 throw new IOException(e); 159 } 160 } 161 else if (url.getProtocol().equals("jar")) { 162 int pos = url.getPath().lastIndexOf("!/"); 163 URL jarURL = new URL(url.getPath().substring(0, pos)); 164 String path = url.getPath().substring(pos + 2); 165 ZipIterator i = ZipIterator.create(jarURL); 166 try { 167 while (i.hasNext()) { 168 ZipEntry entry = i.next(); 169 if (entry.getName().startsWith(path)) { 170 addEntry(url, entry.getName().substring(path.length()), i.getStreamFactory()); 171 } 172 } 173 } 174 finally { 175 Utils.close(i); 176 } 177 } 178 else { 179 if (url.getPath().endsWith(".jar")) { 180 mergeEntries(new URL("jar:" + url + "!/")); 181 } else { 182 // WTF ? 183 } 184 } 185 } 186 187 private void merge(java.io.File f) throws IOException { 188 java.io.File[] files = f.listFiles(); 189 if (files != null) { 190 for (final java.io.File file : files) { 191 String name = file.getName(); 192 Node child = children.get(name); 193 if (file.isDirectory()) { 194 if (child == null) { 195 child = new Node(name); 196 children.put(name, child); 197 } 198 int length = child.dirs.length; 199 child.dirs = Arrays.copyOf(child.dirs, length + 1); 200 child.dirs[length] = file; 201 } else { 202 if (child == null) { 203 children.put(name, child = new Node(name)); 204 } 205 child.resources.add( 206 new Resource(file.toURI().toURL(), 207 new InputStreamFactory() { 208 public InputStream open() throws IOException { 209 return new FileInputStream(file); 210 } 211 }, file.lastModified() 212 ) 213 ); 214 } 215 } 216 } 217 } 218 219 private void addEntry(URL baseURL, String entryName, InputStreamFactory resolver) throws IOException { 220 if (entryName.length() > 0 && entryName.charAt(entryName.length() - 1) != '/') { 221 addEntry(baseURL, 0, entryName, 1, resolver); 222 } 223 } 224 225 private void addEntry(URL baseURL, int index, String entryName, long lastModified, InputStreamFactory resolver) throws IOException { 226 int next = entryName.indexOf('/', index); 227 if (next == -1) { 228 String name = entryName.substring(index); 229 Node child = children.get(name); 230 if (child == null) { 231 children.put(name, child = new Node(name)); 232 } 233 child.resources.add(new Resource(new URL(baseURL + entryName), resolver, lastModified)); 234 } 235 else { 236 String name = entryName.substring(index, next); 237 Node child = children.get(name); 238 if (child == null) { 239 children.put(name, child = new Node(name)); 240 } 241 child.addEntry(baseURL, next + 1, entryName, lastModified, resolver); 242 } 243 } 244 245 @Override 246 public Iterator<Resource> iterator() { 247 return resources.iterator(); 248 } 249 }