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 }