package org.ops4j.pax.construct.util; /* * Copyright 2007 Stuart McCulloch, Alin Dreghiciu * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; import java.util.Iterator; import java.util.List; import java.util.jar.Attributes; import java.util.jar.JarFile; import java.util.jar.Manifest; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import org.codehaus.plexus.util.FileUtils; import org.codehaus.plexus.util.IOUtil; import org.ops4j.pax.construct.util.PomUtils.Pom; /** * Various utility methods for managing and refactoring directories and paths */ public final class DirUtils { /** * Hide constructor for utility class */ private DirUtils() { /* * nothing to do */ } /** * Resolve a file to its unique canonical path - null resolves to the current directory * * @param file file, may be null * @param ignoreErrors ignore checked exceptions when true * @return file representing the canonical path */ public static File resolveFile( File file, boolean ignoreErrors ) { File candidate = file; if( null == file ) { candidate = new File( "." ); } try { candidate = candidate.getCanonicalFile(); } catch( IOException e ) { if( !ignoreErrors ) { throw new RuntimeException( e ); } } return candidate; } /** * Search the local project tree for a Maven POM with the given id * * @param baseDir directory in the project tree * @param pomId either artifactId or groupId:artifactId * @return a Maven POM with the given id, null if not found */ public static Pom findPom( File baseDir, String pomId ) { // no searching required if( PomUtils.isEmpty( pomId ) ) { return null; } // handle groupId:artifactId:other:stuff String[] fields = pomId.split( ":" ); String groupId; String artifactId; if( fields.length > 1 ) { groupId = fields[0]; artifactId = fields[1]; } else { groupId = null; artifactId = pomId; } for( Iterator i = new PomIterator( baseDir ); i.hasNext(); ) { Pom pom = (Pom) i.next(); if( sameProject( pom, groupId, artifactId ) ) { return pom; } } return null; } /** * @param pom Maven project model * @param groupId optional project group id * @param artifactId project artifact id or bundle symbolic name * @return true if the project has matching ids, otherwise false */ private static boolean sameProject( Pom pom, String groupId, String artifactId ) { if( ( artifactId.equals( pom.getArtifactId() ) || artifactId.equals( pom.getBundleSymbolicName() ) ) && ( null == groupId || groupId.equals( pom.getGroupId() ) ) ) { return true; } return false; } /** * Verify all Maven POMs from the base directory to the target, adding missing POMs as required * * @param baseDir base directory * @param targetDir target directory * @return the Maven project in the target directory * @throws IOException */ public static Pom createModuleTree( File baseDir, File targetDir ) throws IOException { // shortcut: target directory already has a POM File pomFile = new File( targetDir, "pom.xml" ); if( pomFile.exists() ) { return PomUtils.readPom( pomFile ); } String[] pivot = calculateRelativePath( baseDir, targetDir ); if( null == pivot ) { // unable to find common parent directory! return null; } File commonDir = new File( pivot[1] ); String descentPath = pivot[2]; Pom parentPom = null; Pom childPom = null; int i = 0; int j = -1; do { // check the next module pom... String pathSoFar = descentPath.substring( 0, j + 1 ); pomFile = new File( commonDir, pathSoFar + "pom.xml" ); if( pomFile.exists() ) { // existing pom, follow it along... childPom = PomUtils.readPom( pomFile ); } else if( parentPom != null && "pom".equals( parentPom.getPackaging() ) ) { // no such pom, need to create new module pom String module = descentPath.substring( i, j ); childPom = createMissingModulePom( parentPom, module, pomFile ); } else { return null; // bad project structure: cannot add interim module } // descend to next pom parentPom = childPom; i = j + 1; j = descentPath.indexOf( '/', i ); } while( j >= 0 ); // final pom in target directory return childPom; } /** * Add missing Maven project POM and attach to the parent project * * @param parentPom parent project * @param module new project module * @param pomFile new project file * @return the new Maven POM * @throws IOException */ private static Pom createMissingModulePom( Pom parentPom, String module, File pomFile ) throws IOException { // link parent to new module pom parentPom.addModule( module, true ); parentPom.write(); String groupId = PomUtils.getCompoundId( parentPom.getGroupId(), parentPom.getArtifactId() ); if( groupId.equals( parentPom.getGroupId() ) ) { groupId += '.' + module; } // create missing module pom and link back to parent Pom childPom = PomUtils.createModulePom( pomFile, groupId, module ); childPom.setParent( parentPom, null, true ); childPom.write(); return childPom; } /** * Calculate the relative path (and common directory) to get from a base directory to a target directory * * @param baseDir base directory * @param targetDir target directory * @return three strings: a dotted path, absolute location of the common directory, and a descent path */ public static String[] calculateRelativePath( File baseDir, File targetDir ) { // need canonical form for this to work File from = resolveFile( baseDir, true ); File to = resolveFile( targetDir, true ); StringBuffer dottedPath = new StringBuffer(); StringBuffer descentPath = new StringBuffer(); while( !from.equals( to ) ) { // "from" is above "to", so need to descend... if( from.getPath().length() < to.getPath().length() ) { descentPath.insert( 0, to.getName() + '/' ); to = to.getParentFile(); } // otherwise need to move up (ie "..") else { dottedPath.append( "../" ); from = from.getParentFile(); } // reached top of file-system! if( null == from || null == to ) { return null; } } return new String[] { // both "from" and "to" now hold the common directory dottedPath.toString(), to.getPath(), descentPath.toString() }; } /** * Set the logical parent for a given POM * * @param pomFile directory or file containing the pom to update * @param parentId either artifactId or groupId:artifactId * @return the relative path to the parent, null if it wasn't found * @throws IOException */ public static String updateLogicalParent( File pomFile, String parentId ) throws IOException { Pom pom = PomUtils.readPom( pomFile ); File baseDir = pom.getBasedir(); Pom parentPom = DirUtils.findPom( baseDir, parentId ); if( null == parentPom ) { return null; } String[] pivot = DirUtils.calculateRelativePath( baseDir, parentPom.getBasedir() ); if( null == pivot ) { return null; } String relativePath = pivot[0] + pivot[2]; // (re-)attach project to its logical parent pom.setParent( parentPom, relativePath, true ); pom.write(); return relativePath; } /** * Refactor path string, adding base directory to all entries * * @param path path to refactor * @param baseDir base directory to add * @param pathSeparator path separator * @return the refactored path */ public static String rebasePaths( String path, String baseDir, char pathSeparator ) { if( null == path ) { return baseDir; } String[] entries = path.split( Character.toString( pathSeparator ) ); StringBuffer rebasedPath = new StringBuffer(); for( int i = 0; i < entries.length; i++ ) { String pathEntry = entries[i].trim(); if( i > 0 ) { rebasedPath.append( pathSeparator ); } rebasedPath.append( baseDir ); if( !".".equals( pathEntry ) ) { rebasedPath.append( '/' ); rebasedPath.append( pathEntry ); } } return rebasedPath.toString(); } /** * Expand any bundle entries on the classpath to include embedded jars, etc. * * @param outputDir current output directory * @param path list of classpath elements * @param tempDir temporary directory for unpacking * @return expanded classpath */ public static List expandOSGiClassPath( File outputDir, List path, File tempDir ) { List expandedPath = new ArrayList(); for( Iterator i = path.iterator(); i.hasNext(); ) { File element = new File( (String) i.next() ); if( element.equals( outputDir ) ) { // don't expand the current project expandedPath.add( element.getPath() ); } else { expandedPath.addAll( expandBundleClassPath( element, tempDir ) ); } } return expandedPath; } /** * Expand compilatation classpath element to include extra entries for compiling against OSGi bundles * * @param element compilatation classpath element * @param tempDir temporary directory for unpacking * @return expanded classpath elements */ private static List expandBundleClassPath( File element, File tempDir ) { File bundle = locateBundle( element ); if( bundle != null && bundle.isFile() ) { String bundleClassPath = extractBundleClassPath( bundle ); File unpackDir = new File( tempDir, bundle.getName() ); return unpackEmbeddedEntries( bundle, unpackDir, bundleClassPath ); } return Collections.singletonList( element.getPath() ); } /** * Locate the actual bundle for the given classpath element * * @param classpathElement classpath element, may be myProject/target/classes * @return the final bundle, null if it hasn't been built yet */ private static File locateBundle( File classpathElement ) { // assume standard output directory, ie. target/classes String outputDir = "target" + File.separator + "classes"; String path = classpathElement.getPath(); if( path.endsWith( outputDir ) ) { // we need the final bundle, not the output directory, so load up the project POM File projectDir = new File( path.substring( 0, path.length() - outputDir.length() ) ); try { Pom reactorPom = PomUtils.readPom( projectDir ); String artifactId = reactorPom.getArtifactId(); String version = reactorPom.getVersion(); return new File( projectDir, "target/" + artifactId + '-' + version + ".jar" ); } catch( IOException e ) { return null; } } // otherwise just assume it's a bundle for now (will check for manifest later) return classpathElement; } /** * @param bundle jarfile * @return Bundle-ClassPath */ private static String extractBundleClassPath( File bundle ) { String bundleClassPath = null; try { Manifest manifest = new JarFile( bundle ).getManifest(); if( null != manifest ) { Attributes mainAttributes = manifest.getMainAttributes(); bundleClassPath = mainAttributes.getValue( "Bundle-ClassPath" ); } } catch( IOException e ) { System.err.println( "WARNING: unable to read jarfile " + bundle ); } if( bundleClassPath != null ) { return bundleClassPath; } return "."; } /** * Simple API to allow selected unpacking of content from bundles */ public interface EntryFilter { /** * @param entryName name of a file entry inside the bundle * @return true if it should be unpacked, otherwise false */ boolean accept( String entryName ); } /** * @param bundle jarfile * @param here unpack directory * @param filter selection filter * @return true if bundle was successfully unpacked */ public static boolean unpackBundle( File bundle, File here, EntryFilter filter ) { try { // improves unpacking performance FileUtils.deleteDirectory( here ); unpack( bundle, here, filter ); return true; } catch( IOException e ) { return false; } } /** * Simple Zip unpacking code, supports selected extraction of entries * * @param bundle zipfile * @param here unpack directory * @param filter selection filter * @throws IOException */ private static void unpack( File bundle, File here, EntryFilter filter ) throws IOException { ZipFile zipFile = new ZipFile( bundle ); try { for( Enumeration e = zipFile.entries(); e.hasMoreElements(); ) { ZipEntry entry = (ZipEntry) e.nextElement(); String name = entry.getName(); // don't bother with plain folders, as we always create them on-demand if( !entry.isDirectory() && ( null == filter || filter.accept( name ) ) ) { // place unpacked file underneath target folder File file = FileUtils.resolveFile( here, name ); file.getParentFile().mkdirs(); InputStream in = zipFile.getInputStream( entry ); OutputStream out = new FileOutputStream( file ); try { // unpack contents IOUtil.copy( in, out ); } finally { IOUtil.close( out ); IOUtil.close( in ); } } } } finally { zipFile.close(); } } /** * @param bundle jarfile * @param here unpack directory * @param bundleClassPath Bundle-ClassPath attribute * @return list of paths pointing to unpacked entries */ private static List unpackEmbeddedEntries( File bundle, File here, String bundleClassPath ) { try { // improves unpacking performance FileUtils.deleteDirectory( here ); } catch( IOException e ) { return Collections.singletonList( bundle.getPath() ); } List pathList = new ArrayList(); String pathPrefix = here.getPath(); String[] entries = bundleClassPath.split( "," ); for( int i = 0; i < entries.length; i++ ) { final String path = entries[i].trim(); if( path.length() == 0 ) { continue; } else if( ".".equals( path ) ) { // no need to unpack, just use jar pathList.add( bundle.getPath() ); } else { try { unpack( bundle, here, new EntryFilter() { // just unpack the embedded folder/jar public boolean accept( String entryName ) { return entryName.startsWith( path ); } } ); pathList.add( pathPrefix + '/' + path ); } catch( IOException e ) { continue; } } } return pathList; } /** * Recursively delete (prune) all empty directories underneath the base directory * * @param baseDir base directory */ public static void pruneEmptyFolders( File baseDir ) { List candidates = new ArrayList(); candidates.add( baseDir ); List prunable = new ArrayList(); while( !candidates.isEmpty() ) { File f = (File) candidates.remove( 0 ); if( f.isDirectory() ) { File[] files = f.listFiles(); candidates.addAll( Arrays.asList( files ) ); prunable.add( f ); } } // delete must go in reverse Collections.reverse( prunable ); for( Iterator i = prunable.iterator(); i.hasNext(); ) { // only deletes empty directories File directory = (File) i.next(); directory.delete(); } } }