| /******************************************************************************* |
| * Copyright (c) 2010, 2015 Sonatype, Inc. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * Stuart McCulloch (Sonatype, Inc.) - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.sisu.space; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.net.URLConnection; |
| import java.net.URLStreamHandler; |
| import java.util.Collections; |
| import java.util.Enumeration; |
| import java.util.Iterator; |
| import java.util.NoSuchElementException; |
| import java.util.zip.ZipEntry; |
| import java.util.zip.ZipException; |
| import java.util.zip.ZipInputStream; |
| |
| /** |
| * {@link Enumeration} of resources found by scanning JARs and directories. |
| */ |
| final class ResourceEnumeration |
| implements Enumeration<URL> |
| { |
| // ---------------------------------------------------------------------- |
| // Constants |
| // ---------------------------------------------------------------------- |
| |
| private static final Iterator<String> NO_ENTRIES = Collections.<String> emptySet().iterator(); |
| |
| // ---------------------------------------------------------------------- |
| // Implementation fields |
| // ---------------------------------------------------------------------- |
| |
| private final URL[] urls; |
| |
| private final String subPath; |
| |
| private final GlobberStrategy globber; |
| |
| private final Object globPattern; |
| |
| private final boolean recurse; |
| |
| private int index; |
| |
| private URL currentURL; |
| |
| private boolean isFolder; |
| |
| private Iterator<String> entryNames = NO_ENTRIES; |
| |
| private String nextEntryName; |
| |
| // ---------------------------------------------------------------------- |
| // Constructors |
| // ---------------------------------------------------------------------- |
| |
| /** |
| * Creates an {@link Enumeration} that scans the given URLs for resources matching the globbed pattern. |
| * |
| * @param subPath An optional path to begin the search from |
| * @param glob The globbed basename pattern |
| * @param recurse When {@code true} search paths below the initial search point; otherwise don't |
| * @param urls The URLs containing resources |
| */ |
| ResourceEnumeration( final String subPath, final String glob, final boolean recurse, final URL[] urls ) // NOPMD |
| { |
| this.subPath = normalizeSearchPath( subPath ); |
| globber = GlobberStrategy.selectFor( glob ); |
| globPattern = globber.compile( glob ); |
| this.recurse = recurse; |
| this.urls = urls; |
| } |
| |
| // ---------------------------------------------------------------------- |
| // Public methods |
| // ---------------------------------------------------------------------- |
| |
| public boolean hasMoreElements() |
| { |
| while ( null == nextEntryName ) |
| { |
| if ( entryNames.hasNext() ) |
| { |
| final String name = entryNames.next(); |
| if ( matchesRequest( name ) ) |
| { |
| nextEntryName = name; |
| } |
| } |
| else if ( index < urls.length ) |
| { |
| currentURL = urls[index++]; |
| entryNames = scan( currentURL ); |
| } |
| else |
| { |
| return false; // no more URLs |
| } |
| } |
| return true; |
| } |
| |
| public URL nextElement() |
| { |
| if ( hasMoreElements() ) |
| { |
| // initialized by hasMoreElements() |
| final String name = nextEntryName; |
| nextEntryName = null; |
| |
| try |
| { |
| return findResource( name ); |
| } |
| catch ( final MalformedURLException e ) |
| { |
| // this shouldn't happen, hence illegal state |
| throw new IllegalStateException( e.toString() ); |
| } |
| } |
| throw new NoSuchElementException(); |
| } |
| |
| // ---------------------------------------------------------------------- |
| // Implementation methods |
| // ---------------------------------------------------------------------- |
| |
| /** |
| * Normalizes the initial search path by removing any duplicate or initial slashes. |
| * |
| * @param path The path to normalize |
| * @return Normalized search path |
| */ |
| static String normalizeSearchPath( final String path ) |
| { |
| if ( null == path || "/".equals( path ) ) |
| { |
| return ""; |
| } |
| boolean echoSlash = false; |
| final StringBuilder buf = new StringBuilder(); |
| for ( int i = 0, length = path.length(); i < length; i++ ) |
| { |
| // ignore any duplicate slashes |
| final char c = path.charAt( i ); |
| final boolean isNotSlash = '/' != c; |
| if ( echoSlash || isNotSlash ) |
| { |
| echoSlash = isNotSlash; |
| buf.append( c ); |
| } |
| } |
| if ( echoSlash ) |
| { |
| // add final slash |
| buf.append( '/' ); |
| } |
| return buf.toString(); |
| } |
| |
| /** |
| * Returns the appropriate {@link Iterator} to iterate over the contents of the given URL. |
| * |
| * @param url The containing URL |
| * @return Iterator that iterates over resources contained inside the given URL |
| */ |
| private Iterator<String> scan( final URL url ) |
| { |
| isFolder = url.getPath().endsWith( "/" ); |
| |
| if ( globber == GlobberStrategy.EXACT && !recurse ) |
| { |
| try |
| { |
| // short-cut the nextElement() process |
| nextEntryName = subPath + globPattern; |
| |
| // but still need to check resource actually exists! |
| Streams.open( findResource( nextEntryName ) ).close(); |
| } |
| catch ( final Exception e ) // IOException + SecurityException + etc... |
| { |
| nextEntryName = null; |
| } |
| return NO_ENTRIES; |
| } |
| |
| return isFolder ? new FileEntryIterator( url, subPath, recurse ) : new ZipEntryIterator( url ); |
| } |
| |
| /** |
| * Returns a {@link URL} pointing to the named resource underneath the current search URL. |
| * |
| * @param name The resource name |
| * @return URL for the resource |
| */ |
| private URL findResource( final String name ) |
| throws MalformedURLException |
| { |
| if ( isFolder ) |
| { |
| return new URL( currentURL, name ); |
| } |
| if ( "jar".equals( currentURL.getProtocol() ) ) |
| { |
| // workaround JDK limitation that doesn't allow nested "jar:" URLs |
| return new URL( currentURL, "#" + name, new NestedJarHandler() ); |
| } |
| return new URL( "jar:" + currentURL + "!/" + name ); |
| } |
| |
| /** |
| * Compares the given entry name against the normalized search path and compiled glob pattern. |
| * |
| * @param entryName The entry name |
| * @return {@code true} if the given name matches the search criteria; otherwise {@code false} |
| */ |
| private boolean matchesRequest( final String entryName ) |
| { |
| if ( entryName.endsWith( "/" ) || !entryName.startsWith( subPath ) ) |
| { |
| return false; // not inside the search scope |
| } |
| if ( !recurse && entryName.indexOf( '/', subPath.length() ) > 0 ) |
| { |
| return false; // inside a sub-directory |
| } |
| return globber.matches( globPattern, entryName ); |
| } |
| |
| // ---------------------------------------------------------------------- |
| // Implementation types |
| // ---------------------------------------------------------------------- |
| |
| /** |
| * Custom {@link URLStreamHandler} that can stream JARs nested inside an arbitrary resource. |
| */ |
| static final class NestedJarHandler |
| extends URLStreamHandler |
| { |
| @Override |
| protected URLConnection openConnection( final URL url ) |
| { |
| return new NestedJarConnection( url ); |
| } |
| } |
| |
| /** |
| * Custom {@link URLConnection} that can access JARs nested inside an arbitrary resource. |
| */ |
| static final class NestedJarConnection |
| extends URLConnection |
| { |
| NestedJarConnection( final URL url ) |
| { |
| super( url ); |
| } |
| |
| @Override |
| public void connect() |
| { |
| // postpone until someone actually requests an input stream |
| } |
| |
| @Override |
| public InputStream getInputStream() |
| throws IOException |
| { |
| final URL containingURL = new URL( "jar", null, -1, url.getFile() ); |
| final ZipInputStream is = new ZipInputStream( Streams.open( containingURL ) ); |
| final String entryName = url.getRef(); |
| |
| for ( ZipEntry entry = is.getNextEntry(); entry != null; entry = is.getNextEntry() ) |
| { |
| if ( entryName.equals( entry.getName() ) ) |
| { |
| return is; |
| } |
| } |
| throw new ZipException( "No such entry: " + entryName + " in: " + containingURL ); |
| } |
| } |
| } |