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    }