/******************************************************************************* * Copyright (c) 2010-present 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 ); } } }