Skip to content

Instantly share code, notes, and snippets.

@renatoathaydes
Last active August 29, 2015 13:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save renatoathaydes/9039643 to your computer and use it in GitHub Desktop.
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.
#!/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