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.Safe;
024import org.crsh.util.ZipIterator;
025
026import java.io.FileInputStream;
027import java.io.IOException;
028import java.io.InputStream;
029import java.net.URISyntaxException;
030import java.net.URL;
031import java.util.ArrayList;
032import java.util.Collections;
033import java.util.Enumeration;
034import java.util.HashMap;
035import java.util.Iterator;
036import java.util.LinkedList;
037import java.util.zip.ZipEntry;
038
039/** @author <a href="mailto:julien.viet@exoplatform.com">Julien Viet</a> */
040public class Node implements Iterable<Resource> {
041
042  /** . */
043  public final String name;
044
045  /** . */
046  HashMap<String, Node> children = new HashMap<String, Node>();
047
048  /** . */
049  LinkedList<Resource> resources = new LinkedList<Resource>();
050
051  public Node() {
052    this.name = "";
053  }
054
055  private Node(String name) {
056    this.name = name;
057  }
058
059  void merge(ClassLoader loader) throws IOException, URISyntaxException {
060
061    // Get the root class path files
062    for (Enumeration<URL> i = loader.getResources("");i.hasMoreElements();) {
063      URL url = i.nextElement();
064      // In some case we can get null (Tomcat 8)
065      if (url != null) {
066        mergeEntries(url);
067      }
068    }
069    ArrayList<URL> items = Collections.list(loader.getResources("META-INF/MANIFEST.MF"));
070    for (URL item : items) {
071      if ("jar".equals(item.getProtocol())) {
072        String path = item.getPath();
073        int pos = path.lastIndexOf("!/");
074        URL url = new URL("jar:" + path.substring(0, pos + 2));
075        mergeEntries(url);
076      }
077      else {
078        //
079      }
080    }
081  }
082
083  /**
084   * Rewrite an URL by analysing the serie of trailing <code>!/</code>. The number of <code>jar:</code> prefixes
085   * does not have to be equals to the number of separators.
086   *
087   * @param url the url to rewrite
088   * @return the rewritten URL
089   */
090  String rewrite(String url) {
091    int end = url.lastIndexOf("!/");
092    if (end >= 0) {
093      String entry = url.substring(end + 2);
094      int start = url.indexOf(':');
095      String protocol = url.substring(0, start);
096      String nestedURL;
097      if (protocol.equals("jar")) {
098        nestedURL = rewrite(url.substring(start + 1, end));
099        return "jar:" + nestedURL + "!/" + entry;
100      } else {
101        nestedURL = rewrite(url.substring(0, end));
102      }
103      return "jar:" + nestedURL + "!/" + entry;
104    } else {
105      return url;
106    }
107  }
108
109  void mergeEntries(URL url) throws IOException, URISyntaxException {
110    // We handle a special case of spring-boot URLs here before diving in the recursive analysis
111    // see https://github.com/spring-projects/spring-boot/tree/master/spring-boot-tools/spring-boot-loader#urls
112    if (url.getProtocol().equals("jar")) {
113      url = new URL(rewrite(url.toString()));
114    }
115    _mergeEntries(url);
116  }
117
118  private void _mergeEntries(URL url) throws IOException, URISyntaxException {
119    if (url.getProtocol().equals("file")) {
120      try {
121        java.io.File f = new java.io.File(url.toURI());
122        if (f.isDirectory()) {
123          merge(f);
124        } else if (f.getName().endsWith(".jar")) {
125          mergeEntries(new URL("jar:" + url + "!/"));
126        } else {
127          // WTF ?
128        }
129      }
130      catch (URISyntaxException e) {
131        throw new IOException(e);
132      }
133    }
134    else if (url.getProtocol().equals("jar")) {
135      int pos = url.getPath().lastIndexOf("!/");
136      URL jarURL = new URL(url.getPath().substring(0, pos));
137      String path = url.getPath().substring(pos + 2);
138      ZipIterator i = ZipIterator.create(jarURL);
139      try {
140        while (i.hasNext()) {
141          ZipEntry entry = i.next();
142          if (entry.getName().startsWith(path)) {
143            addEntry(url, entry.getName().substring(path.length()), i.getStreamFactory());
144          }
145        }
146      }
147      finally {
148        Safe.close(i);
149      }
150    }
151    else {
152      if (url.getPath().endsWith(".jar")) {
153        mergeEntries(new URL("jar:" + url + "!/"));
154      } else {
155        // WTF ?
156      }
157    }
158  }
159
160  private void merge(java.io.File f) throws IOException {
161    java.io.File[] files = f.listFiles();
162    if (files != null) {
163      for (final java.io.File file : files) {
164        String name = file.getName();
165        Node child = children.get(name);
166        if (file.isDirectory()) {
167          if (child == null) {
168            Node dir = new Node(name);
169            dir.merge(file);
170            children.put(name, dir);
171          } else {
172            child.merge(file);
173          }
174        } else {
175          if (child == null) {
176            children.put(name, child = new Node(name));
177          }
178          child.resources.add(
179              new Resource(file.toURI().toURL(),
180                  new InputStreamFactory() {
181                    public InputStream open() throws IOException {
182                      return new FileInputStream(file);
183                    }
184                  }, file.lastModified()
185              )
186          );
187        }
188      }
189    }
190  }
191
192  private void addEntry(URL baseURL, String entryName, InputStreamFactory resolver) throws IOException {
193    if (entryName.length() > 0 && entryName.charAt(entryName.length() - 1) != '/') {
194      addEntry(baseURL, 0, entryName, 1, resolver);
195    }
196  }
197
198  private void addEntry(URL baseURL, int index, String entryName, long lastModified, InputStreamFactory resolver) throws IOException {
199    int next = entryName.indexOf('/', index);
200    if (next == -1) {
201      String name = entryName.substring(index);
202      Node child = children.get(name);
203      if (child == null) {
204        children.put(name, child = new Node(name));
205      }
206      child.resources.add(new Resource(new URL(baseURL + entryName), resolver, lastModified));
207    }
208    else {
209      String name = entryName.substring(index, next);
210      Node child = children.get(name);
211      if (child == null) {
212        children.put(name, child = new Node(name));
213      }
214      child.addEntry(baseURL, next + 1, entryName, lastModified, resolver);
215    }
216  }
217
218  @Override
219  public Iterator<Resource> iterator() {
220    return resources.iterator();
221  }
222}