Last active
August 29, 2015 13:56
-
-
Save renatoathaydes/9039643 to your computer and use it in GitHub Desktop.
OSGi Runtime Analyser - scans a directory recursively for bundles, finding if all packages are used/provided by the bundles found.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/groovy | |
/** | |
* OSGi Runtime Analyser | |
* Scans a directory recursively for bundles, finding if all packages are | |
* used/provided by the bundles found. | |
*/ | |
import groovy.io.FileType | |
import groovy.transform.EqualsAndHashCode | |
import groovy.transform.ToString | |
import java.nio.file.Path | |
import java.nio.file.Paths | |
import java.util.jar.JarEntry | |
import java.util.jar.JarFile | |
sep = File.separator | |
verbose = false | |
class VersionComparator { | |
static boolean compareVersions( String v1, String v2 ) { | |
def v1IsRange = isRange( v1 ) | |
def v2IsRange = isRange( v2 ) | |
if ( v1IsRange && !v2IsRange ) { | |
return isWithin( v1, v2 ) | |
} else if ( !v1IsRange && v2IsRange ) { | |
return isWithin( v2, v1 ) | |
} else { | |
return v1 == v2 | |
} | |
} | |
private static boolean isWithin( String range, String version ) { | |
def vParts = version.split( /[.-]/ ) | |
def rParts = range.split( /[\(\[\]\)\.\-]/ ).collect { it.trim() } | |
def separator = rParts.findIndexOf { it.contains( ',' ) } | |
def splitParts = rParts[ separator ].split( ',' ) | |
def lowerParts = rParts.tail().take( separator - 1 ) + [ splitParts[ 0 ] ] | |
def higherParts = [ splitParts[ 1 ] ] + | |
( rParts.size() > separator + 1 ? rParts[ ( separator + 1 )..-1 ] : [ ] ) | |
def nextPart = { rIter -> rIter.hasNext() ? rIter.next() : "0" } | |
def versionStr = { s -> s.isNumber() ? s.toInteger() : "-$s" } | |
def gtIncl = { a, b -> versionStr( a ) > versionStr( b ) } | |
def gtExcl = { a, b -> versionStr( a ) >= versionStr( b ) } | |
def ltIncl = { a, b -> versionStr( a ) < versionStr( b ) } | |
def ltExcl = { a, b -> versionStr( a ) <= versionStr( b ) } | |
def lt = range[ 0 ] == '[' ? ltIncl : ltExcl | |
def nextLow = nextPart.curry( lowerParts.iterator() ) | |
def decided = false | |
if ( vParts.size() > 1 ) { | |
for ( vPart in vParts[ 0..-2 ] ) { | |
def low = nextLow() | |
if ( vPart < low ) return false | |
if ( vPart > low ) { | |
decided = true; break | |
} | |
} | |
} | |
if ( !decided && lt( vParts[ -1 ], nextLow() ) ) return false | |
def gt = range[ -1 ] == ']' ? gtIncl : gtExcl | |
def nextHigh = nextPart.curry( higherParts.iterator() ) | |
if ( vParts.size() > 1 ) { | |
for ( vPart in vParts[ 0..-2 ] ) { | |
def high = nextHigh() | |
if ( vPart > high ) return false | |
if ( vPart < high ) return true | |
} | |
} | |
if ( gt( vParts[ -1 ], nextHigh() ) ) return false | |
return true | |
} | |
private static boolean isRange( String v ) { | |
( v.startsWith( '(' ) || v.startsWith( '[' ) ) && | |
( v.endsWith( ')' ) || v.endsWith( ']' ) && v.contains( ',' ) ) | |
} | |
static void test() { | |
assert isWithin( '(0,1]', '1' ) | |
assert isWithin( '(0.0,1.0)', '0.1' ) | |
assert isWithin( '[0.0,1.0)', '0.0' ) | |
assert isWithin( '(0.0,1.0)', '0.9' ) | |
assert isWithin( '(24.0,27.2)', '25.28.2' ) | |
assert isWithin( '(24.0,27.2]', '27.18.2' ) | |
assert isWithin( '(24.0,27.2)', '25.28.2-BETA' ) | |
assert isWithin( '(24.0,27.2)', '27.1-SNAPSHOT' ) | |
assert isWithin( '(24.0,27.2]', '27.2-SNAPSHOT' ) | |
assert isWithin( '(24.0,27.2.1)', '27.2.0-SNAPSHOT' ) | |
assert !isWithin( '(0,1)', '1' ) | |
assert !isWithin( '(0.0,1.0)', '0.0' ) | |
assert !isWithin( '(0.0,1.0]', '1.1.0' ) | |
assert !isWithin( '(0.0,1.0)', '1.0' ) | |
assert !isWithin( '(24.0,27.2)', '27.28.2' ) | |
assert !isWithin( '(24.0,27.2]', '27.28.2' ) | |
assert !isWithin( '(24.0,27.2]', '27.2.1-SNAPSHOT' ) | |
assert !isWithin( '(24.0,27.2.1)', '27.2.12-SNAPSHOT' ) | |
} | |
} | |
VersionComparator.test() | |
@ToString( includePackage = false, includeNames = true ) | |
class Package { | |
String name | |
String version | |
List<String> packageUses = [ ] | |
boolean equals( other ) { | |
if ( other instanceof Package && this.name == other.name ) { | |
return VersionComparator.compareVersions( this.version.trim(), other.version.trim() ) | |
} | |
false | |
} | |
} | |
@ToString( includePackage = false, includeNames = true ) | |
@EqualsAndHashCode( includes = [ 'symbolicName', 'version' ] ) | |
class Bundle { | |
List<Package> importPackages = [ ] | |
List<Package> exportPackages = [ ] | |
String version | |
String symbolicName | |
} | |
List<Bundle> scanPath( Path path ) { | |
def bundles = [ ] | |
path.toFile().eachFileRecurse( FileType.FILES ) { File jar -> | |
if ( jar.name.matches( ~/.*\.jar/ ) ) { | |
def bundle = bundleFrom jar | |
if ( bundle ) bundles << bundle | |
} | |
} | |
bundles | |
} | |
Bundle bundleFrom( File jar ) { | |
println( ( '*' * 10 ) + jar.name + ( '*' * 10 ) ) | |
def jarFile = new JarFile( jar ) | |
try { | |
JarEntry manifest = jarFile.entries().find { it.name == "META-INF${sep}MANIFEST.MF" } | |
if ( manifest?.name ) { | |
def manifestText = jarFile.getInputStream( jarFile.getEntry( manifest.name ) ).text | |
return readManifest( manifestText ) | |
} | |
} finally { | |
jarFile.close() | |
} | |
null | |
} | |
Bundle readManifest( String manifestText ) { | |
def lines = manifestText.split( "\n" ) | |
//lines.each { println it + '\n------------------' } | |
def bundle = new Bundle() | |
for ( iter = lines.iterator(); iter.hasNext(); ) { | |
def line = iter.next() | |
switch ( line.trim() ) { | |
case ~/Export\-Package:.*/: | |
bundle.exportPackages = asPackages( parsePackages( 'Export-Package:', line, iter ) ) | |
break | |
case ~/Import\-Package:.*/: | |
bundle.importPackages = asPackages( parsePackages( 'Import-Package:', line, iter ) ) | |
break | |
case ~/Bundle\-SymbolicName:.*/: | |
bundle.symbolicName = ( line - 'Bundle-SymbolicName:' ).trim() | |
break | |
case ~/Bundle\-Version:.*/: | |
bundle.version = ( line - 'Bundle-Version:' ).trim() | |
break | |
} | |
} | |
return bundle | |
} | |
def parsePackages( String instruction, String firstLine, Iterator<String> iter ) { | |
def packages = ( firstLine - instruction ).trim() | |
while ( iter.hasNext() ) { | |
def nextLine = iter.next() | |
if ( nextLine.startsWith( ' ' ) ) { | |
packages += nextLine.trim() | |
} else break | |
} | |
def parts = packages.split( ';' ) | |
def packageMaps = [ ] | |
for ( part in parts ) { | |
processPackagesData( part, packageMaps ) | |
} | |
packageMaps | |
} | |
private void processPackagesData( String part, List packageMaps ) { | |
if ( part.startsWith( 'uses:="' ) ) { | |
def uses = ( part - 'uses:="' ).takeWhile { it != '"' } | |
packageMaps[ -1 ][ 'uses' ] = uses.split( ',' ) | |
} else if ( part.startsWith( 'version="' ) ) { | |
def version = ( part - 'version="' ).takeWhile { it != '"' } | |
addVersionToPrevPackages( packageMaps, version ) | |
def rest = part - ( 'version="' + version + '"' ) | |
if ( rest && rest[ 0 ] == ',' ) rest = rest[ 1..-1 ] | |
if ( rest ) processPackagesData( rest, packageMaps ) | |
} else { | |
packageMaps << [ package: part ] | |
} | |
} | |
void addVersionToPrevPackages( List<Map> packageMaps, String version ) { | |
for ( i in 1..packageMaps.size() ) { | |
if ( !packageMaps[ -i ].version ) | |
packageMaps[ -i ][ 'version' ] = version | |
else break | |
} | |
} | |
List<Package> asPackages( List<Map> packageMaps ) { | |
def result = [ ] | |
for ( map in packageMaps ) { | |
for ( pkg in map.package.split( ',' ) ) { | |
result << new Package( name: pkg.trim(), version: map.version, | |
packageUses: map.uses ? map.uses as List : [ ] ) | |
} | |
} | |
result | |
} | |
void analyse( List<Bundle> bundles ) { | |
Set<Package> haves = [ ] as Set | |
Set<Package> donts = [ ] as Set | |
for ( bundle in bundles ) { | |
donts += bundle.importPackages | |
haves += bundle.exportPackages | |
} | |
def required = donts - haves | |
def notNeeded = haves - donts | |
if ( verbose ) { | |
println "Packages imported:\n${donts.collect { it.name + '-' + it.version }}" | |
println "Packages exported:\n${haves.collect { it.name + '-' + it.version }}" | |
} | |
def prettyPackages = { packages -> | |
packages.sort { it.name } | |
.collect { '\n ' + it.name + '-' + it.version }.toString()[ 1..-2 ] | |
} | |
if ( !required ) { | |
println "Your OSGi bundles seem to have everything they need!" | |
} else { | |
def requiredString = prettyPackages required | |
println "The following packages are not provided: ${requiredString}" | |
} | |
if ( notNeeded ) { | |
def notNeededString = prettyPackages notNeeded | |
println "The following packages are exported but never used: ${notNeededString}" | |
} | |
} | |
def usage() { | |
""" | |
---- OSGi Runtime Analyser ---- | |
Usage: groovy osgiRuntimeAnalyser [ options ] path | |
""" | |
} | |
Path resolvePath( String arg ) { | |
def path = arg.split( "\\${sep}" ) | |
if ( path[ 0 ] == '' ) path[ 0 ] = sep | |
def root = Paths.get( * path ) | |
assert root.toFile().exists(), "The path given does not exist ${root}" | |
assert root.toFile().isDirectory(), "The path given is not a directory ${root}" | |
root | |
} | |
def cli = new CliBuilder( usage: usage(), header: 'Options:' ) | |
cli.h( 'Show usage and quit' ) | |
cli.v( 'Show detailed information about bundles' ) | |
options = cli.parse( args ) | |
if ( options && options.h ) { | |
cli.usage() | |
return | |
} | |
if ( !options || !options.arguments() ) { | |
println "Error: Provide at least one path to scan" | |
return | |
} | |
verbose = options.v | |
bundles = scanPath resolvePath( options.arguments().first() ) | |
analyse bundles | |
println "Done!" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment