| /******************************************************************************* |
| * Copyright (c) 25.07.2011 Aaron Digulla. |
| * 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: |
| * Aaron Digulla - initial API and implementation and/or initial documentation |
| *******************************************************************************/ |
| |
| package m4e |
| |
| import groovy.xml.MarkupBuilder |
| import java.util.jar.Manifest |
| import java.util.zip.ZipEntry |
| import java.util.zip.ZipFile |
| import java.util.zip.ZipOutputStream |
| import org.codehaus.groovy.runtime.ProcessGroovyMethods; |
| import org.eclipse.osgi.util.ManifestElement; |
| import org.osgi.framework.Constants; |
| import org.slf4j.LoggerFactory; |
| import org.slf4j.Logger; |
| |
| class InstallCmd extends AbstractCommand { |
| |
| final static String DESCRIPTION = '''archives...\n- Extract the specified archives and convert the Eclipse plug-ins inside into Maven artifacts''' |
| |
| void doRun( String... args ) { |
| // args[1..-1] throws "IndexOutOfBoundsException: toIndex = 2" if array has only one element |
| if( args.size() > 1 ) { |
| for( String archive in args[1..-1] ) { |
| importArchive( archive ) |
| } |
| } |
| |
| log.info( "Import complete. Imported ${statistics.bundleCount} Eclipse bundles into ${statistics.pomCount} POMs. There are ${statistics.jarCount} new JARs of which ${statistics.sourceCount} have sources" ) |
| |
| if( m2repos.size() == 1 ) { |
| log.info( "The new Maven 2 repository is here: ${m2repos[0].absolutePath}" ) |
| } else { |
| log.info( "${m2repos.size()} Maven 2 repositories were created:" ) |
| m2repos.each { |
| log.info( " ${it.absolutePath}" ) |
| } |
| } |
| } |
| |
| List<File> m2repos = [] |
| ImportStatistics statistics = new ImportStatistics() |
| |
| void importArchive( String url ) { |
| |
| File archive = downloadArchive( url ) |
| File path = unpackArchive( archive ) |
| |
| ImportTool tool = importIntoTmpRepo( path ) |
| |
| File m2repo = tool.m2repo |
| m2repos << m2repo |
| } |
| |
| ImportTool importIntoTmpRepo( File path ) { |
| def tool = new ImportTool( installCmd: this ) |
| tool.run( path ) |
| return tool |
| } |
| |
| def archiveExtensions = [ '.tar', '.tar.gz', '.tar.bz2', '.zip' ] |
| |
| /** Unpack an archive for import */ |
| File unpackArchive( File archive ) { |
| |
| if( archive.isDirectory() ) { |
| log.debug( "Archive ${archive} is a directory; no need to unpack" ) |
| return archive |
| } |
| |
| String dirName = basename( archive ) |
| |
| for( String ext in archiveExtensions ) { |
| dirName = dirName.removeEnd( ext ) |
| } |
| |
| def path = new File( workDir, dirName ) |
| if( path.exists() ) { |
| log.debug( "Archive ${archive} is already unpacked at ${path}" ) |
| return path |
| } |
| |
| log.info( "Unpacking ${archive}" ) |
| |
| if( archive.name.endsWith( '.zip' ) ) { |
| archive.unzip( path ) |
| } else if( archive.name.endsWithOneOf( '.tar', '.tar.gz', '.tar.bz2' ) ) { |
| File log = new File( path.absolutePath + '.log' ) |
| |
| archive.untar( path, log ) |
| } else { |
| throw new RuntimeException( "Unsupported archive type ${archive}" ) |
| } |
| |
| log.info( 'OK' ) |
| return path |
| } |
| |
| /** Download an archive via HTTP. |
| * If the value of archive is not a URL, do nothing. |
| * |
| * This function returns the name of the downloaded file. |
| */ |
| File downloadArchive( String url ) { |
| if( !url.toString().startsWith('http://') ) { |
| log.debug( "Archive URL ${url} seems to be local" ) |
| return new File( url ) |
| } |
| |
| String basename = basename( archive ) |
| File path = new File( workDir, basename ) |
| |
| if( path.exists() ) { |
| log.debug('Archive ${path} has already been downloaded') |
| return path |
| } |
| |
| log.info( "Downloading ${url} to ${path}" ) |
| download( url, path ) |
| log.info( 'OK' ) |
| |
| return path |
| } |
| |
| String basename( def path ) { |
| if( path instanceof File ) { |
| return path.name |
| } |
| |
| String result = path.toString() |
| int pos = result.lastIndexOf( '/' ) |
| if( pos >= 0 ) { |
| path = result.substring( pos + 1 ) |
| } |
| |
| return path |
| } |
| |
| void download( String url, File path ) { |
| path.withOutputStream() { |
| def stream = new URL( url ).openStream() |
| it << stream |
| stream.close() |
| } |
| } |
| |
| } |
| |
| class FileFound extends Exception { |
| File file; |
| } |
| |
| class ImportStatistics { |
| int bundleCount = 0 |
| int pomCount = 0 |
| int jarCount = 0 |
| int sourceCount = 0 |
| } |
| |
| class ImportTool { |
| |
| static final Logger log = LoggerFactory.getLogger( ImportTool ) |
| |
| static boolean DEBUG = true |
| |
| static final String FAILURE_FILE_NAME = 'failure' |
| |
| InstallCmd installCmd |
| |
| File eclipseFolder |
| File tmpHome |
| File m2repo |
| File failure |
| File m2settings |
| |
| void run( File path ) { |
| assert path != null |
| assert installCmd != null |
| assert installCmd.workDir != null |
| |
| eclipseFolder = locate( path, 'plugins' ) |
| if( !eclipseFolder ) { |
| throw new RuntimeException( "Can't find plugins folder below ${path.absolutePath}" ) |
| } |
| eclipseFolder = eclipseFolder.parentFile |
| assert eclipseFolder != null |
| |
| tmpHome = new File( installCmd.workDir, path.name + '_home' ) |
| m2repo = new File( tmpHome, 'm2repo' ) |
| failure = new File( tmpHome, FAILURE_FILE_NAME ) |
| |
| installCmd.prepareErrorLog( m2repo, 'install' ) |
| |
| log.debug( "Importing plug-ins from ${eclipseFolder} into repo ${m2repo}" ) |
| clean() |
| |
| doImport() |
| |
| log.info( 'OK' ) |
| |
| failure.delete() |
| } |
| |
| void doImport() { |
| |
| // TODO convert features |
| |
| File pluginsFolder = new File( eclipseFolder, 'plugins' ) |
| doImport( pluginsFolder ) |
| } |
| |
| void doImport( File folder ) { |
| folder.eachFile { it -> |
| |
| try { |
| def tool = new BundleConverter( installCmd: installCmd, m2repo: m2repo, statistics: installCmd.statistics ) |
| |
| if( it.isDirectory() ) { |
| tool.importExplodedBundle( it ) |
| } else { |
| tool.importBundle( it ) |
| } |
| |
| tool.close() |
| } catch( Exception e ) { |
| installCmd.error( Error.IMPORT_ERROR, "Error processing ${it.absolutePath}: ${e}", e, [ file: it.absolutePath ] ) |
| } |
| } |
| } |
| |
| /** Make sure we don't have any leftovers from previous attempts. */ |
| void clean() { |
| if( tmpHome.exists() ) { |
| log.info('Cleaning up from last run...') |
| tmpHome.deleteDir() |
| } |
| |
| tmpHome.makedirs() |
| |
| // This file is deleted after the import succeeds |
| failure << 'Import failed' |
| } |
| |
| File locate( File root, String name ) { |
| try { |
| root.eachDirRecurse() { File dir -> |
| if( dir.name == name ) { |
| throw new FileFound( file: dir ) |
| } |
| } |
| } catch( FileFound e ) { |
| return e.file |
| } |
| |
| return null |
| } |
| } |
| |
| class BundleConverter { |
| |
| private final static Logger log = LoggerFactory.getLogger( BundleConverter ) |
| |
| InstallCmd installCmd |
| File m2repo |
| |
| Manifest manifest |
| String groupId |
| String artifactId |
| String version |
| |
| File bundle |
| |
| ImportStatistics statistics |
| |
| void importBundle( File bundleJar ) { |
| this.bundle = bundleJar |
| |
| manifest = loadManifestFromJar( bundleJar ) |
| if( ! manifest ) { |
| return |
| } |
| |
| statistics.bundleCount ++ |
| |
| log.debug( 'Importing {}', bundleJar ) |
| |
| examineManifest() |
| |
| if( isSourceBundle() ) { |
| return |
| } |
| |
| String key = "${groupId}:${artifactId}:${version}" |
| log.info( "Importing ${key}" ) |
| File jarFile = MavenRepositoryTools.buildPath( m2repo, key, 'jar' ) |
| |
| String classPath = manifest.attr.getValue( 'Bundle-ClassPath' ) |
| if( classPath && classPath != '.' ) { |
| unpackNestedJar( classPath, jarFile ) |
| } else { |
| bundleJar.copy( jarFile ) |
| } |
| |
| statistics.jarCount ++ |
| |
| File pomFile = MavenRepositoryTools.buildPath( m2repo, key, 'pom' ) |
| pomFile.withWriter( 'UTF-8' ) { |
| createPom( it ) |
| } |
| } |
| |
| boolean isSourceBundle() { |
| ManifestElement[] sourceBundleFor = parseAttribute( 'Eclipse-SourceBundle' ) |
| if( !sourceBundleFor ) { |
| return false |
| } |
| |
| importSourceBundle( manifest, sourceBundleFor ) |
| return true |
| } |
| |
| boolean isSingleton |
| String licenseURL |
| |
| Map licenseNameMap = [ |
| 'http://www.apache.org/licenses/LICENSE-2.0.txt': 'Apache 2' |
| ] |
| |
| void examineManifest() { |
| // println manifest.entries |
| |
| log.debug( "Attributes of ${bundle}" ) |
| manifest.attr.each { |
| log.debug( " ${it.key}: ${it.value}" ) |
| } |
| |
| def attrs = parseAttribute( Constants.BUNDLE_SYMBOLICNAME ) |
| assert attrs.size() == 1 |
| |
| artifactId = attrs[0].value |
| version = manifest.attr.getValue( Constants.BUNDLE_VERSION ) |
| groupId = artifactIdToGroupId( artifactId ) |
| |
| isSingleton = ( 'true' == attrs[0].getDirective( Constants.SINGLETON_DIRECTIVE ) ) |
| |
| attrs = parseAttribute( 'Bundle-License' ) |
| if( attrs && attrs.size() == 1 ) { |
| licenseURL = attrs[0].value |
| } |
| } |
| |
| void unpackNestedJar( String nestedJarPath, File jarFile ) { |
| |
| if( nestedJarPath.contains( ',' ) ) { |
| String msg = "Multiple nested JARs are not supported; just copying the original bundle" |
| Map xml = [ jar: jarFile.absolutePath, nestedJarPath: nestedJarPath ] |
| installCmd.warn( Warning.MULTIPLE_NESTED_JARS, msg, xml ) |
| bundle.copy( jarFile ) |
| return |
| } |
| |
| def entry = archive[ nestedJarPath ] |
| if( null == entry ) { |
| throw new RuntimeException( "Can't find [${nestedJarPath}] in ${bundle}" ) |
| } |
| |
| jarFile.parentFile?.makedirs() |
| |
| archive.withInputStream( entry ) { |
| jarFile << it |
| } |
| } |
| |
| String artifactIdToGroupId( String artifactId ) { |
| def parts = artifactId.split( '\\.', -1 ) |
| def n = Math.min( parts.size()-1, 2 ) |
| return parts[0..n].join( '.' ) |
| } |
| |
| String indent = ' ' |
| |
| void createPom( Writer writer ) { |
| |
| statistics.pomCount ++ |
| |
| writer << '<?xml version="1.0" encoding="UTF-8"?>\n' |
| writer << '<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"\n' |
| writer << indent*2 << 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">\n' |
| writer << indent << '<modelVersion>4.0.0</modelVersion>\n' |
| |
| writer << indent << '<groupId>' << groupId << '</groupId>\n' |
| writer << indent << '<artifactId>' << artifactId << '</artifactId>\n' |
| writer << indent << '<version>' << version << '</version>\n' |
| |
| String name = manifest.attr.getValue( Constants.BUNDLE_NAME ) |
| if( name ) { |
| if( name.contains( '%' ) ) { |
| name = expand( name ) |
| } |
| |
| writer << indent << '<name>' << escape( name ) << '</name>\n' |
| } |
| |
| String description = manifest.attr.getValue( Constants.BUNDLE_DESCRIPTION ) |
| if( description ) { |
| if( description.contains( '%' ) ) { |
| description = expand( description ) |
| } |
| } else { |
| description = '' |
| } |
| |
| writer << indent << '<description>' << escape( description ) << ( description ? "\n " : "" ) << 'Converted with MT4E ' << Tool.VERSION << '</description>\n' |
| |
| String url = manifest.attr.getValue( Constants.BUNDLE_DOCURL ) |
| if( url ) { |
| writer << indent << '<url>' << escape( url ) << '</url>\n' |
| } |
| |
| if( licenseURL ) { |
| writer << indent << '<licenses>\n' |
| writer << indent*2 << '<license>\n' |
| |
| String licName = licenseNameMap[ licenseURL ] |
| if( licName ) { |
| writer << indent*3 << '<name>' << escape( licName ) << '</name>\n' |
| } |
| |
| writer << indent*3 << '<url>' << escape( licenseURL ) << '</url>\n' |
| |
| writer << indent*2 << '</license>\n' |
| writer << indent << '</licenses>\n' |
| } |
| |
| addProperties( writer ) |
| |
| def requiredBundles = parseAttribute( Constants.REQUIRE_BUNDLE ) |
| // println "requiredBundles=${requiredBundles}" |
| if( requiredBundles ) { |
| addDependencies( writer, requiredBundles ) |
| } |
| |
| writer << '</project>\n' |
| } |
| |
| void addProperties( Writer writer, ManifestElement[] requiredBundles ) { |
| |
| def imports = parseAttribute( Constants.IMPORT_PACKAGE ) |
| def exports = parseAttribute( Constants.EXPORT_PACKAGE ) |
| // println "imports=${imports}" |
| // println "exports=${exports}" |
| if( !imports && !exports && !isSingleton ) { |
| return |
| } |
| |
| writer << indent << '<properties>\n' |
| |
| if( imports ) { |
| String value = imports.join( ',' ) |
| addProperty( writer, Pom.IMPORT_PACKAGE_PROPERTY, value ) |
| } |
| |
| if( exports ) { |
| String value = exports.join( ',' ) |
| addProperty( writer, Pom.EXPORT_PACKAGE_PROPERTY, value ) |
| } |
| |
| if( isSingleton ) { |
| addProperty( writer, Pom.IS_SINGLETON_PROPERTY, 'true' ) |
| } |
| |
| writer << indent << '</properties>\n' |
| } |
| |
| void addProperty( Writer writer, String name, String value ) { |
| writer << indent*2 << '<' << name << '>' << escape( value ) << '</' << name << '>\n' |
| } |
| |
| void addDependencies( Writer writer, ManifestElement[] requiredBundles ) { |
| |
| writer << indent << '<dependencies>\n' |
| |
| requiredBundles.each { |
| addDependency( writer, it ) |
| } |
| |
| writer << indent << '</dependencies>\n' |
| } |
| |
| void addDependency( Writer writer, ManifestElement dep ) { |
| // println dep |
| |
| String artifactId = dep.value |
| String version = dep.getAttribute( Constants.BUNDLE_VERSION_ATTRIBUTE ) |
| if( !version ) { |
| version = '[0,)' |
| } else if ( !version.startsWith( '[' ) && !version.startsWith( '(' ) ) { |
| version = "[${version},)" |
| } |
| |
| String groupId = artifactIdToGroupId( artifactId ) |
| |
| writer << indent*2 << '<dependency>\n' |
| writer << indent*3 << '<groupId>' << groupId << '</groupId>\n' |
| writer << indent*3 << '<artifactId>' << artifactId << '</artifactId>\n' |
| writer << indent*3 << '<version>' << version << '</version>\n' |
| |
| String resolution = dep.getDirective( Constants.RESOLUTION_DIRECTIVE ) |
| if( Constants.RESOLUTION_OPTIONAL == resolution ) { |
| writer << indent*3 << '<optional>true</optional>\n' |
| } |
| |
| writer << indent*2 << '</dependency>\n' |
| } |
| |
| Properties pluginProperties |
| |
| String expand( String variable ) { |
| loadPluginProperties() |
| |
| if( ! variable.startsWith( '%' ) ) { |
| throw new RuntimeException( 'Expected "%" as the first character in ' + variable ) |
| } |
| |
| String value = pluginProperties.getProperty( variable.substring( 1 ) ) |
| return value ? value : variable |
| } |
| |
| void loadPluginProperties() { |
| |
| if( null != pluginProperties ) { |
| return |
| } |
| |
| pluginProperties = new Properties() |
| |
| if( archive ) { |
| def entry = archive[ 'plugin.properties' ] |
| if( entry ) { |
| archive.withInputStream( entry ) { |
| pluginProperties.load( it ) |
| } |
| } |
| |
| entry = archive[ 'OSGI-INF/l10n/bundle.properties' ] |
| if( entry ) { |
| archive.withInputStream( entry ) { |
| pluginProperties.load( it ) |
| } |
| } |
| |
| } else { |
| File file = new File( bundle, 'plugin.properties' ) |
| if( file.exists() ) { |
| file.withInputStream { stream -> |
| pluginProperties.load( stream ) |
| } |
| } |
| } |
| } |
| |
| String escape( String text ) { |
| return text.replace( '&', '&' ).replace( '<', '<' ).replace( '>', '>' ) |
| } |
| |
| void importSourceBundle( Manifest manifest, ManifestElement[] sourceBundleFor ) { |
| |
| statistics.sourceCount ++ |
| |
| if( sourceBundleFor.size() != 1 ) { |
| throw new RuntimeException( "Expected exactly one element in ${sourceBundleFor}" ) |
| } |
| def attr = sourceBundleFor[0] |
| |
| log.debug( "Found source bundle for ${attr.value}" ) |
| |
| artifactId = attr.value |
| version = attr.getAttribute( Constants.VERSION_ATTRIBUTE ) |
| groupId = artifactIdToGroupId( artifactId ) |
| |
| String key = "${groupId}:${artifactId}:${version}" |
| File mavenSourceJar = MavenRepositoryTools.buildPath( m2repo, key, "jar", "sources" ) |
| |
| String roots = attr.getDirective( 'roots' ) |
| if( !roots ) { |
| assert bundle.isFile() |
| |
| bundle.copy( mavenSourceJar ) |
| return |
| } |
| |
| mavenSourceJar.parentFile?.makedirs() |
| |
| manifest.entries.clear() |
| |
| roots += '/' |
| mavenSourceJar.withOutputStream { it -> |
| def out = new ZipOutputStream( it ) |
| |
| filterSourceBundle( out, roots ) |
| |
| out.close() |
| } |
| } |
| |
| void filterSourceBundle( ZipOutputStream out, String roots ) { |
| assert archive != null |
| |
| archive.eachEntry { ZipEntry entry -> |
| String name = entry.name |
| |
| if( 'META-INF/MANIFEST.MF' == name ) { |
| writeManifest( out ) |
| return |
| } |
| |
| if( name.startsWith( roots ) ) { |
| name = name.substring( roots.size() ) |
| } |
| |
| ZipEntry clone = new ZipEntry( name ) |
| clone.time = entry.time |
| clone.comment = entry.comment |
| clone.extra = entry.extra |
| |
| out.putNextEntry( clone ) |
| |
| archive.withInputStream( entry ) { |
| out << it |
| } |
| |
| out.closeEntry() |
| } |
| } |
| |
| void writeManifest( ZipOutputStream out ) { |
| def entry = new ZipEntry( 'META-INF/MANIFEST.MF' ) |
| out.putNextEntry( entry ) |
| manifest.write( out ) |
| out.closeEntry() |
| } |
| |
| void close() { |
| if( archive ) { |
| archive.close() |
| } |
| } |
| |
| ZipFile archive |
| |
| Manifest loadManifestFromJar( File file ) { |
| archive = new ZipFile( file ) |
| |
| def entry = archive[ 'META-INF/MANIFEST.MF' ] |
| if( !entry ) { |
| String msg = "Can't find manifest in ${file.absolutePath}" |
| installCmd.error( Error.MISSING_MANIFEST, msg, [ jar: file.absolutePath ] ) |
| return null |
| } |
| |
| def m |
| archive.withInputStream( entry ) { |
| m = new Manifest( it ) |
| } |
| return m |
| } |
| |
| void importExplodedBundle( File bundleFolder ) { |
| manifest = loadManifestFromFile( new File( bundleFolder, 'META-INF/MANIFEST.MF' ) ) |
| if( !manifest ) { |
| return |
| } |
| |
| statistics.bundleCount ++ |
| |
| log.debug( 'Importing {}', bundleFolder ) |
| bundle = bundleFolder |
| |
| examineManifest() |
| |
| if( isSourceBundle() ) { |
| return |
| } |
| |
| String key = "${groupId}:${artifactId}:${version}" |
| log.info( "Importing ${key}" ) |
| File jarFile = MavenRepositoryTools.buildPath( m2repo, key, 'jar' ) |
| |
| String classPath = manifest.attr.getValue( 'Bundle-ClassPath' ) |
| if( classPath ) { |
| File nestedJar = new File( bundleFolder, classPath ) |
| |
| nestedJar.copy( jarFile ) |
| } else { |
| packBundle( bundleFolder, jarFile ) |
| } |
| |
| statistics.jarCount ++ |
| |
| File pomFile = MavenRepositoryTools.buildPath( m2repo, key, 'pom' ) |
| pomFile.withWriter( 'UTF-8' ) { |
| createPom( it ) |
| } |
| } |
| |
| void packBundle( File bundleFolder, File jarFile ) { |
| |
| jarFile.parentFile?.makedirs() |
| |
| jarFile.withOutputStream { |
| def out = new ZipOutputStream( it ) |
| |
| bundleFolder.eachFileRecurse { File file -> |
| String name = file.pathRelativeTo( bundleFolder ) |
| |
| def entry = new ZipEntry( name ) |
| entry.setTime( file.lastModified() ) |
| |
| out.putNextEntry( entry ) |
| |
| out << file |
| |
| out.closeEntry() |
| } |
| |
| out.close() |
| } |
| } |
| |
| Manifest loadManifestFromFile( File file ) { |
| if( !file.exists() ) { |
| String msg = "Can't find manifest ${file.absolutePath}" |
| installCmd.error( Error.MISSING_MANIFEST, msg, [ jar: file.absolutePath ] ) |
| return null |
| } |
| |
| def m |
| file.withInputStream { |
| m = new Manifest( it ) |
| } |
| |
| return m |
| } |
| |
| ManifestElement[] parseAttribute( String name ) { |
| String text = manifest.attr.getValue( name ) |
| if( !text ) { |
| return null |
| } |
| |
| return ManifestElement.parseHeader( name, text ) |
| } |
| } |