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
020package org.crsh.vfs.spi.url;
021
022import org.crsh.util.InputStreamFactory;
023import org.crsh.util.Utils;
024import org.crsh.util.ZipIterator;
025
026import java.io.File;
027import java.io.FileInputStream;
028import java.io.IOException;
029import java.io.InputStream;
030import java.net.URISyntaxException;
031import java.net.URL;
032import java.util.ArrayList;
033import java.util.Arrays;
034import java.util.Collections;
035import java.util.Enumeration;
036import java.util.HashMap;
037import java.util.Iterator;
038import java.util.LinkedList;
039import java.util.List;
040import java.util.zip.ZipEntry;
041
042/** @author <a href="mailto:julien.viet@exoplatform.com">Julien Viet</a> */
043public 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}