summaryrefslogtreecommitdiffstats
path: root/bundle-plugin/src/main/scala/com/yahoo/container
diff options
context:
space:
mode:
Diffstat (limited to 'bundle-plugin/src/main/scala/com/yahoo/container')
-rw-r--r--bundle-plugin/src/main/scala/com/yahoo/container/plugin/bundle/AnalyzeBundle.scala72
-rw-r--r--bundle-plugin/src/main/scala/com/yahoo/container/plugin/bundle/TransformExportPackages.scala54
-rw-r--r--bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/Analyze.scala28
-rw-r--r--bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AnalyzeClassVisitor.scala102
-rw-r--r--bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AnalyzeMethodVisitor.scala88
-rw-r--r--bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AnalyzeSignatureVisitor.scala68
-rw-r--r--bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AnnotationVisitorTrait.scala39
-rw-r--r--bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AttributeVisitorTrait.scala15
-rw-r--r--bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/ClassFileMetaData.scala10
-rw-r--r--bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/ExportPackageAnnotation.scala24
-rw-r--r--bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/PackageTally.scala46
-rw-r--r--bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/Packages.scala27
-rw-r--r--bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/SubVisitorTrait.scala19
-rw-r--r--bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/package.scala24
-rw-r--r--bundle-plugin/src/main/scala/com/yahoo/container/plugin/mojo/Artifacts.scala30
-rw-r--r--bundle-plugin/src/main/scala/com/yahoo/container/plugin/mojo/AssembleContainerPluginMojo.scala113
-rw-r--r--bundle-plugin/src/main/scala/com/yahoo/container/plugin/mojo/GenerateBundleClassPathMappingsMojo.scala96
-rw-r--r--bundle-plugin/src/main/scala/com/yahoo/container/plugin/mojo/GenerateOsgiManifestMojo.scala283
-rw-r--r--bundle-plugin/src/main/scala/com/yahoo/container/plugin/osgi/ExportPackageParser.scala89
-rw-r--r--bundle-plugin/src/main/scala/com/yahoo/container/plugin/osgi/ExportPackages.scala27
-rw-r--r--bundle-plugin/src/main/scala/com/yahoo/container/plugin/osgi/ImportPackages.scala51
-rw-r--r--bundle-plugin/src/main/scala/com/yahoo/container/plugin/util/Extractors.scala17
-rw-r--r--bundle-plugin/src/main/scala/com/yahoo/container/plugin/util/Files.scala23
-rw-r--r--bundle-plugin/src/main/scala/com/yahoo/container/plugin/util/IO.scala46
-rw-r--r--bundle-plugin/src/main/scala/com/yahoo/container/plugin/util/Iteration.scala14
-rw-r--r--bundle-plugin/src/main/scala/com/yahoo/container/plugin/util/JarFiles.scala24
-rw-r--r--bundle-plugin/src/main/scala/com/yahoo/container/plugin/util/Maps.scala19
-rw-r--r--bundle-plugin/src/main/scala/com/yahoo/container/plugin/util/Strings.scala14
28 files changed, 1462 insertions, 0 deletions
diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/bundle/AnalyzeBundle.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/bundle/AnalyzeBundle.scala
new file mode 100644
index 00000000000..b9137a3c6bc
--- /dev/null
+++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/bundle/AnalyzeBundle.scala
@@ -0,0 +1,72 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.bundle
+
+import java.util.jar.{Manifest => JarManifest}
+import java.io.File
+import com.yahoo.container.plugin.osgi.{ExportPackages, ExportPackageParser}
+import ExportPackages.Export
+import collection.immutable.LinearSeq
+import com.yahoo.container.plugin.util.JarFiles
+
+
+/**
+ * @author tonytv
+ */
+object AnalyzeBundle {
+ case class PublicPackages(exports : List[Export], globals : List[String])
+
+ def publicPackagesAggregated(jarFiles : Iterable[File]) = aggregate(jarFiles map {publicPackages(_)})
+
+ def aggregate(publicPackagesList : Iterable[PublicPackages]) =
+ (PublicPackages(List(), List()) /: publicPackagesList) { (a,b) =>
+ PublicPackages(a.exports ++ b.exports, a.globals ++ b.globals)
+ }
+
+ def publicPackages(jarFile: File): PublicPackages = {
+ try {
+
+ (for {
+ manifest <- JarFiles.getManifest(jarFile)
+ if isOsgiManifest(manifest)
+ } yield PublicPackages(parseExports(manifest), parseGlobals(manifest))).
+ getOrElse(PublicPackages(List(), List()))
+
+ } catch {
+ case e : Exception => throw new RuntimeException("Invalid manifest in bundle '%s'".format(jarFile.getPath), e)
+ }
+ }
+
+ def bundleSymbolicName(jarFile: File): Option[String] = {
+ JarFiles.getManifest(jarFile).flatMap(getBundleSymbolicName)
+ }
+
+ private def parseExportsFromAttribute(manifest : JarManifest, attributeName : String) = {
+ (for (export <- getMainAttributeValue(manifest, attributeName)) yield
+ ExportPackageParser.parseAll(export) match {
+ case noSuccess: ExportPackageParser.NoSuccess => throw new RuntimeException(
+ "Failed parsing %s: %s".format(attributeName, noSuccess))
+ case success => success.get
+ }).
+ getOrElse(List())
+ }
+
+ private def parseExports = parseExportsFromAttribute(_ : JarManifest, "Export-Package")
+
+ private def parseGlobals(manifest : JarManifest) = {
+ //TODO: Use separate parser for global packages.
+ val globals = parseExportsFromAttribute(manifest, "Global-Package")
+
+ if (globals map {_.parameters} exists {!_.isEmpty}) {
+ throw new RuntimeException("Parameters not valid for Global-Package.")
+ }
+
+ globals flatMap {_.packageNames}
+ }
+
+ private def getMainAttributeValue(manifest: JarManifest, name: String): Option[String] =
+ Option(manifest.getMainAttributes.getValue(name))
+
+ private def isOsgiManifest = getBundleSymbolicName(_: JarManifest).isDefined
+
+ private def getBundleSymbolicName = getMainAttributeValue(_: JarManifest, "Bundle-SymbolicName")
+}
diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/bundle/TransformExportPackages.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/bundle/TransformExportPackages.scala
new file mode 100644
index 00000000000..baaef756911
--- /dev/null
+++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/bundle/TransformExportPackages.scala
@@ -0,0 +1,54 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.bundle
+
+import java.io.File
+import com.yahoo.container.plugin.osgi.ExportPackages.{Export, Parameter}
+import com.yahoo.container.plugin.osgi.ExportPackages.Export
+
+/**
+ * @author tonytv
+ */
+object TransformExportPackages extends App {
+ def replaceVersions(exports: List[Export], newVersion: String): List[Export] = {
+ mapParameters(exports) { parameters =>
+ parameters map replaceVersion(newVersion)
+ }
+ }
+
+ def removeUses(exports: List[Export]): List[Export] = {
+ mapParameters(exports) { parameters =>
+ parameters filter {_.name != "uses"}
+ }
+ }
+
+ def mapParameters(exports: List[Export])(f: List[Parameter] => List[Parameter]): List[Export] = {
+ exports map { case Export(packageNames: List[String], parameters: List[Parameter]) =>
+ Export(packageNames, f(parameters))
+ }
+ }
+
+ private def replaceVersion(newVersion: String)(parameter: Parameter) = {
+ parameter match {
+ case Parameter("version", _) => Parameter("version", newVersion)
+ case other => other
+ }
+ }
+
+ def toExportPackageProperty(exports: List[Export]): String = {
+ val exportPackages =
+ exports map { case Export(packageNames: List[String], parameters: List[Parameter]) =>
+ val parameterString = nameEqualsValue(parameters)
+ (packageNames ++ parameterString) mkString ";"
+ }
+
+ exportPackages mkString ","
+ }
+
+ private def nameEqualsValue(parameters: List[Parameter]) = {
+ parameters map { case Parameter(name, value) =>
+ s"$name=${quote(value)}"
+ }
+ }
+
+ def quote(s: String) = '"' + s + '"'
+}
diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/Analyze.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/Analyze.scala
new file mode 100644
index 00000000000..ed15c1bcc74
--- /dev/null
+++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/Analyze.scala
@@ -0,0 +1,28 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.classanalysis
+
+import org.objectweb.asm._
+import java.io.{InputStream, File}
+import com.yahoo.container.plugin.util.IO.withFileInputStream
+
+/**
+ * Main entry point for class analysis
+ * @author tonytv
+ */
+object Analyze {
+ def analyzeClass(classFile : File) : ClassFileMetaData = {
+ try {
+ withFileInputStream(classFile) { fileInputStream =>
+ analyzeClass(fileInputStream)
+ }
+ } catch {
+ case e : RuntimeException => throw new RuntimeException("An error occurred when analyzing " + classFile.getPath, e)
+ }
+ }
+
+ def analyzeClass(inputStream : InputStream) : ClassFileMetaData = {
+ val visitor = new AnalyzeClassVisitor()
+ new ClassReader(inputStream).accept(visitor, ClassReader.SKIP_DEBUG)
+ visitor.result
+ }
+}
diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AnalyzeClassVisitor.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AnalyzeClassVisitor.scala
new file mode 100644
index 00000000000..b57a07d5c30
--- /dev/null
+++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AnalyzeClassVisitor.scala
@@ -0,0 +1,102 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.classanalysis
+
+import org.objectweb.asm._
+import com.yahoo.osgi.annotation.{ExportPackage, Version}
+import collection.mutable
+
+/**
+ * Picks up classes used in class files.
+ * @author tonytv
+ */
+private class AnalyzeClassVisitor extends ClassVisitor(Opcodes.ASM5) with AnnotationVisitorTrait with AttributeVisitorTrait {
+ private var name : String = null
+ protected val imports : ImportsSet = mutable.Set()
+ protected var exportPackageAnnotation: Option[ExportPackageAnnotation] = None
+
+
+ override def visitAttribute(attribute: Attribute): Unit = super.visitAttribute(attribute)
+
+ override def visitMethod(access: Int, name: String, desc: String, signature: String,
+ exceptions: Array[String]): MethodVisitor = {
+
+ imports ++= (Type.getReturnType(desc) +: Type.getArgumentTypes(desc)).flatMap(getClassName)
+
+ imports ++= Option(exceptions) getOrElse(Array()) flatMap internalNameToClassName
+
+ AnalyzeSignatureVisitor.analyzeMethod(signature, this)
+ new AnalyzeMethodVisitor(this)
+ }
+
+ override def visitField(access: Int, name: String, desc: String, signature: String, value: AnyRef): FieldVisitor = {
+ imports ++= getClassName(Type.getType(desc)).toList
+
+ AnalyzeSignatureVisitor.analyzeField(signature, this)
+ new FieldVisitor(Opcodes.ASM5) with SubVisitorTrait with AttributeVisitorTrait with AnnotationVisitorTrait {
+ val analyzeClassVisitor = AnalyzeClassVisitor.this
+
+ override def visitAnnotation(desc: String, visible: Boolean): AnnotationVisitor = super.visitAnnotation(desc, visible)
+ override def visitAttribute(attribute: Attribute): Unit = super.visitAttribute(attribute)
+ override def visitEnd(): Unit = super.visitEnd()
+ }
+ }
+
+ override def visit(version: Int, access: Int, name: String, signature: String, superName: String, interfaces: Array[String]) {
+ this.name = internalNameToClassName(name).get
+
+ imports ++= (superName +: interfaces) flatMap internalNameToClassName
+ AnalyzeSignatureVisitor.analyzeClass(signature, this)
+ }
+
+ override def visitInnerClass(name: String, outerName: String, innerName: String, access: Int) {}
+ override def visitOuterClass(owner: String, name: String, desc: String) {}
+ override def visitSource(source: String, debug: String) {}
+ override def visitEnd() {}
+
+ def addImports(imports: TraversableOnce[String]) {
+ this.imports ++= imports
+ }
+
+ override def visitAnnotation(desc: String, visible: Boolean): AnnotationVisitor = {
+ if (Type.getType(desc).getClassName == classOf[ExportPackage].getName) {
+ visitExportPackage()
+ } else {
+ super.visitAnnotation(desc, visible)
+ }
+ }
+
+ def visitExportPackage(): AnnotationVisitor = {
+ def defaultVersionValue[T](name: String) = classOf[Version].getMethod(name).getDefaultValue().asInstanceOf[T]
+
+ new AnnotationVisitor(Opcodes.ASM5) {
+ var major: Int = defaultVersionValue("major")
+ var minor: Int = defaultVersionValue("minor")
+ var micro: Int = defaultVersionValue("micro")
+ var qualifier: String = defaultVersionValue("qualifier")
+
+ override def visit(name: String, value: AnyRef) {
+ def valueAsInt = value.asInstanceOf[Int]
+
+ name match {
+ case "major" => major = valueAsInt
+ case "minor" => minor = valueAsInt
+ case "micro" => micro = valueAsInt
+ case "qualifier" => qualifier = value.asInstanceOf[String]
+ }
+ }
+
+ override def visitEnd() {
+ exportPackageAnnotation = Some(ExportPackageAnnotation(major, minor, micro, qualifier))
+ }
+
+ override def visitEnum(name: String, desc: String, value: String) {}
+ override def visitArray(name: String): AnnotationVisitor = this
+ override def visitAnnotation(name: String, desc: String): AnnotationVisitor = this
+ }
+ }
+
+ def result = {
+ assert(!imports.contains("int"))
+ new ClassFileMetaData(name, imports.toSet, exportPackageAnnotation)
+ }
+}
diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AnalyzeMethodVisitor.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AnalyzeMethodVisitor.scala
new file mode 100644
index 00000000000..5d65b3972c0
--- /dev/null
+++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AnalyzeMethodVisitor.scala
@@ -0,0 +1,88 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.classanalysis
+
+import org.objectweb.asm._
+
+/**
+ * Picks up classes used in method bodies.
+ * @author tonytv
+ */
+private class AnalyzeMethodVisitor(val analyzeClassVisitor : AnalyzeClassVisitor)
+ extends MethodVisitor(Opcodes.ASM5) with AnnotationVisitorTrait with AttributeVisitorTrait with SubVisitorTrait {
+
+
+ override def visitParameterAnnotation(parameter: Int, desc: String, visible: Boolean): AnnotationVisitor = super.visitParameterAnnotation(parameter, desc, visible)
+ override def visitAnnotationDefault(): AnnotationVisitor = super.visitAnnotationDefault()
+ override def visitAttribute(attribute: Attribute): Unit = super.visitAttribute(attribute)
+ override def visitAnnotation(desc: String, visible: Boolean): AnnotationVisitor = super.visitAnnotation(desc, visible)
+ override def visitEnd(): Unit = super.visitEnd()
+
+ override def visitMultiANewArrayInsn(desc: String, dims: Int) {
+ imports ++= getClassName(Type.getType(desc)).toList
+ }
+
+
+ override def visitMethodInsn(opcode: Int, owner: String, name: String, desc: String, itf: Boolean) {
+ imports ++= internalNameToClassName(owner)
+ imports ++= Type.getArgumentTypes(desc).flatMap(getClassName)
+ imports ++= getClassName(Type.getReturnType(desc))
+ }
+
+ override def visitFieldInsn(opcode: Int, owner: String, name: String, desc: String) {
+ imports ++= internalNameToClassName(owner) ++ getClassName(Type.getType(desc)).toList
+
+ }
+
+ override def visitTypeInsn(opcode: Int, `type` : String) {
+ imports ++= internalNameToClassName(`type`)
+ }
+
+ override def visitTryCatchBlock(start: Label, end: Label, handler: Label, `type` : String) {
+ if (`type` != null) //null means finally block
+ imports ++= internalNameToClassName(`type`)
+ }
+
+ override def visitLocalVariable(name: String, desc: String, signature: String, start: Label, end: Label, index: Int) {
+ imports += Type.getType(desc).getClassName
+ }
+
+ override def visitLdcInsn(constant: AnyRef) {
+ constant match {
+ case typeConstant: Type => imports ++= getClassName(typeConstant)
+ case _ =>
+ }
+ }
+
+ override def visitInvokeDynamicInsn(name: String, desc: String, bootstrapMethod: Handle, bootstrapMethodArgs: AnyRef*) {
+ bootstrapMethodArgs.foreach {
+ case typeConstant: Type =>
+ imports ++= getClassName(typeConstant)
+ case handle: Handle =>
+ imports ++= internalNameToClassName(handle.getOwner)
+ imports ++= Type.getArgumentTypes(desc).flatMap(getClassName)
+ case _ : Number =>
+ case _ : String =>
+ case other => throw new AssertionError(s"Unexpected type ${other.getClass} with value '$other'")
+ }
+ }
+
+ override def visitMaxs(maxStack: Int, maxLocals: Int) {}
+ override def visitLineNumber(line: Int, start: Label) {}
+ //only for debugging
+ override def visitLookupSwitchInsn(dflt: Label, keys: Array[Int], labels: Array[Label]) {}
+
+
+ override def visitTableSwitchInsn(min: Int, max: Int, dflt: Label, labels: Label*): Unit = super.visitTableSwitchInsn(min, max, dflt, labels: _*)
+ override def visitIincInsn(`var` : Int, increment: Int) {}
+ override def visitLabel(label: Label) {}
+ override def visitJumpInsn(opcode: Int, label: Label) {}
+ override def visitVarInsn(opcode: Int, `var` : Int) {}
+ override def visitIntInsn(opcode: Int, operand: Int) {}
+ override def visitInsn(opcode: Int) {}
+ override def visitFrame(`type` : Int, nLocal: Int, local: Array[AnyRef], nStack: Int, stack: Array[AnyRef]) {}
+ override def visitCode() {}
+}
+
+
+
+
diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AnalyzeSignatureVisitor.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AnalyzeSignatureVisitor.scala
new file mode 100644
index 00000000000..693e0e17482
--- /dev/null
+++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AnalyzeSignatureVisitor.scala
@@ -0,0 +1,68 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.classanalysis
+
+import org.objectweb.asm.Opcodes
+import org.objectweb.asm.signature.{SignatureReader, SignatureVisitor}
+
+
+/**
+ * @author tonytv
+ */
+
+private class AnalyzeSignatureVisitor(val analyzeClassVisitor: AnalyzeClassVisitor)
+ extends SignatureVisitor(Opcodes.ASM5)
+ with SubVisitorTrait {
+
+
+ override def visitEnd(): Unit = super.visitEnd()
+
+ override def visitClassType(className: String) {
+ imports ++= internalNameToClassName(className)
+ }
+
+ override def visitFormalTypeParameter(name: String) {}
+
+ override def visitClassBound() = this
+
+ override def visitInterfaceBound() = this
+
+ override def visitSuperclass() = this
+
+ override def visitInterface() = this
+
+ override def visitParameterType() = this
+
+ override def visitReturnType() = this
+
+ override def visitExceptionType() = this
+
+ override def visitBaseType(descriptor: Char) {}
+
+ override def visitTypeVariable(name: String) {}
+
+ override def visitArrayType() = this
+
+ override def visitInnerClassType(name: String) {}
+
+ override def visitTypeArgument() {}
+
+ override def visitTypeArgument(wildcard: Char) = this
+}
+
+
+object AnalyzeSignatureVisitor {
+ def analyzeClass(signature: String, analyzeClassVisitor: AnalyzeClassVisitor) {
+ if (signature != null) {
+ new SignatureReader(signature).accept(new AnalyzeSignatureVisitor(analyzeClassVisitor))
+ }
+ }
+
+ def analyzeMethod(signature: String, analyzeClassVisitor: AnalyzeClassVisitor) {
+ analyzeClass(signature, analyzeClassVisitor)
+ }
+
+ def analyzeField(signature: String, analyzeClassVisitor: AnalyzeClassVisitor) {
+ if (signature != null)
+ new SignatureReader(signature).acceptType(new AnalyzeSignatureVisitor(analyzeClassVisitor))
+ }
+}
diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AnnotationVisitorTrait.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AnnotationVisitorTrait.scala
new file mode 100644
index 00000000000..8beb47c765f
--- /dev/null
+++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AnnotationVisitorTrait.scala
@@ -0,0 +1,39 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.classanalysis
+
+import org.objectweb.asm.{Opcodes, AnnotationVisitor, Type}
+
+/**
+ * Picks up classes used in annotations.
+ * @author tonytv
+ */
+private trait AnnotationVisitorTrait {
+ protected val imports: ImportsSet
+
+ def visitAnnotation(desc: String, visible: Boolean): AnnotationVisitor = {
+ imports ++= getClassName(Type.getType(desc)).toList
+
+ visitAnnotationDefault()
+ }
+
+ def visitAnnotationDefault(): AnnotationVisitor =
+ new AnnotationVisitor(Opcodes.ASM5) {
+ override def visit(name: String, value: AnyRef) {}
+
+ override def visitEnum(name: String, desc: String, value: String) {
+ imports ++= getClassName(Type.getType(desc)).toList
+ }
+
+ override def visitArray(name: String): AnnotationVisitor = this
+
+ override def visitAnnotation(name: String, desc: String): AnnotationVisitor = {
+ imports ++= getClassName(Type.getType(desc)).toList
+ this
+ }
+
+ override def visitEnd() {}
+ }
+
+ def visitParameterAnnotation(parameter: Int, desc: String, visible: Boolean): AnnotationVisitor =
+ visitAnnotation(desc, visible)
+}
diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AttributeVisitorTrait.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AttributeVisitorTrait.scala
new file mode 100644
index 00000000000..0603cebf5af
--- /dev/null
+++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AttributeVisitorTrait.scala
@@ -0,0 +1,15 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.classanalysis
+
+import org.objectweb.asm.{Type, Attribute}
+
+/**
+ * @author tonytv
+ */
+private trait AttributeVisitorTrait {
+ protected val imports: ImportsSet
+
+ def visitAttribute(attribute: Attribute) {
+ imports ++= getClassName(Type.getObjectType(attribute.`type`)).toList
+ }
+}
diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/ClassFileMetaData.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/ClassFileMetaData.scala
new file mode 100644
index 00000000000..00ed9efb360
--- /dev/null
+++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/ClassFileMetaData.scala
@@ -0,0 +1,10 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.classanalysis
+
+/**
+ * The result of analyzing a .class file.
+ * @author tonytv
+ */
+sealed case class ClassFileMetaData(name:String,
+ referencedClasses : Set[String],
+ exportPackage : Option[ExportPackageAnnotation])
diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/ExportPackageAnnotation.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/ExportPackageAnnotation.scala
new file mode 100644
index 00000000000..00265b80761
--- /dev/null
+++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/ExportPackageAnnotation.scala
@@ -0,0 +1,24 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.classanalysis
+
+import com.yahoo.container.plugin.util.Strings
+
+/**
+ * @author tonytv
+ */
+case class ExportPackageAnnotation(major: Int, minor: Int, micro: Int, qualifier: String) {
+ requireNonNegative(major, "major")
+ requireNonNegative(minor, "minor")
+ requireNonNegative(micro, "micro")
+ require(qualifier.matches("""(\p{Alpha}|\p{Digit}|_|-)*"""),
+ exportPackageError("qualifier must follow the format (alpha|digit|'_'|'-')* but was '%s'.".format(qualifier)))
+
+
+ private def requireNonNegative(i: Int, fieldName: String) {
+ require(i >= 0, exportPackageError("%s must be non-negative but was %d.".format(fieldName, i)))
+ }
+
+ private def exportPackageError(s: String) = "ExportPackage anntotation: " + s
+
+ def osgiVersion : String = (List(major, minor, micro) ++ Strings.noneIfEmpty(qualifier)).mkString(".")
+}
diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/PackageTally.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/PackageTally.scala
new file mode 100644
index 00000000000..ecbf9448dfd
--- /dev/null
+++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/PackageTally.scala
@@ -0,0 +1,46 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.classanalysis
+
+import com.yahoo.container.plugin.util.Maps
+
+/**
+ *
+ * @author tonytv
+ */
+final class PackageTally (private val definedPackagesMap : Map[String, Option[ExportPackageAnnotation]],
+ referencedPackagesUnfiltered : Set[String]) {
+
+ val referencedPackages = referencedPackagesUnfiltered diff definedPackages
+
+ def definedPackages = definedPackagesMap.keySet
+
+ def exportedPackages = definedPackagesMap collect { case (name, Some(export)) => (name, export) }
+
+ /**
+ * Represents the classes for two package tallies that are deployed as a single unit.
+ *
+ * ExportPackageAnnotations from this has precedence over the other.
+ */
+ def combine(other: PackageTally): PackageTally = {
+ new PackageTally(
+ Maps.combine(definedPackagesMap, other.definedPackagesMap)(_ orElse _),
+ referencedPackages ++ other.referencedPackages)
+ }
+}
+
+
+object PackageTally {
+ def fromAnalyzedClassFiles(analyzedClassFiles : Seq[ClassFileMetaData]) : PackageTally = {
+ combine(
+ for (metaData <- analyzedClassFiles)
+ yield {
+ new PackageTally(
+ Map(Packages.packageName(metaData.name) -> metaData.exportPackage),
+ metaData.referencedClasses.map(Packages.packageName))
+ })
+ }
+
+ def combine(packageTallies : Iterable[PackageTally]) : PackageTally = (empty /: packageTallies)(_.combine(_))
+
+ val empty : PackageTally = new PackageTally(Map(), Set())
+}
diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/Packages.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/Packages.scala
new file mode 100644
index 00000000000..2900d3f1551
--- /dev/null
+++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/Packages.scala
@@ -0,0 +1,27 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.classanalysis
+
+/**
+ * Utility methods related to packages.
+ * @author tonytv
+ */
+object Packages {
+ case class PackageMetaData(definedPackages: Set[String], referencedExternalPackages: Set[String])
+
+ def packageName(fullClassName: String) = {
+ def nullIfNotFound(index : Int) = if (index == -1) 0 else index
+
+ fullClassName.substring(0, nullIfNotFound(fullClassName.lastIndexOf(".")))
+ }
+
+
+
+ def analyzePackages(allClasses: Seq[ClassFileMetaData]): PackageMetaData = {
+ val (definedPackages, referencedClasses) =
+ (for (classMetaData <- allClasses)
+ yield (packageName(classMetaData.name), classMetaData.referencedClasses.map(packageName))).
+ unzip
+
+ PackageMetaData(definedPackages.toSet, referencedClasses.flatten.toSet diff definedPackages.toSet)
+ }
+}
diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/SubVisitorTrait.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/SubVisitorTrait.scala
new file mode 100644
index 00000000000..d4e7f4fb028
--- /dev/null
+++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/SubVisitorTrait.scala
@@ -0,0 +1,19 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.classanalysis
+
+import collection.mutable
+
+/**
+ * A visitor that's run for sub construct of a class
+ * and forwards all its imports the the owning ClassVisitor at the end.
+ * @author tonytv
+ */
+private trait SubVisitorTrait {
+ val analyzeClassVisitor : AnalyzeClassVisitor
+
+ val imports : ImportsSet = mutable.Set()
+
+ def visitEnd() {
+ analyzeClassVisitor.addImports(imports)
+ }
+}
diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/package.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/package.scala
new file mode 100644
index 00000000000..a94bc7710d2
--- /dev/null
+++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/package.scala
@@ -0,0 +1,24 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin
+
+import org.objectweb.asm.Type
+import collection.mutable
+
+package object classanalysis {
+ type ImportsSet = mutable.Set[String]
+
+ def internalNameToClassName(internalClassName: String) : Option[String] = {
+ getClassName(Type.getObjectType(internalClassName))
+ }
+
+ def getClassName(aType: Type): Option[String] = {
+ import Type._
+
+ aType.getSort match {
+ case ARRAY => getClassName(aType.getElementType)
+ case OBJECT => Some(aType.getClassName)
+ case _ => None
+ }
+ }
+}
+
diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/mojo/Artifacts.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/mojo/Artifacts.scala
new file mode 100644
index 00000000000..3b2e52be8c4
--- /dev/null
+++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/mojo/Artifacts.scala
@@ -0,0 +1,30 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.mojo
+
+
+import scala.collection.JavaConversions._
+import org.apache.maven.project.MavenProject
+import org.apache.maven.artifact.Artifact
+
+
+/**
+ * @author tonytv
+ */
+object Artifacts {
+ def getArtifacts(project : MavenProject) = {
+ type artifactSet = java.util.Set[Artifact]
+ val artifacts = project.getArtifacts.asInstanceOf[artifactSet].groupBy(_.getScope)
+
+ def isTypeJar(artifact : Artifact) = artifact.getType == "jar"
+ def getByScope(scope: String) =
+ artifacts.getOrElse(scope, Iterable.empty).partition(isTypeJar)
+
+
+ val (jarArtifactsToInclude, nonJarArtifactsToInclude) = getByScope(Artifact.SCOPE_COMPILE)
+ val (jarArtifactsProvided, nonJarArtifactsProvided) = getByScope(Artifact.SCOPE_PROVIDED)
+
+ (jarArtifactsToInclude, jarArtifactsProvided, nonJarArtifactsToInclude ++ nonJarArtifactsProvided)
+ }
+
+ def getArtifactsToInclude(project: MavenProject) = getArtifacts(project)._1
+}
diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/mojo/AssembleContainerPluginMojo.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/mojo/AssembleContainerPluginMojo.scala
new file mode 100644
index 00000000000..50379cee858
--- /dev/null
+++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/mojo/AssembleContainerPluginMojo.scala
@@ -0,0 +1,113 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.mojo
+
+import java.io.File
+import java.nio.channels.Channels
+import java.util.jar.JarFile
+import java.util.zip.ZipEntry
+
+import com.yahoo.container.plugin.util.{Files, JarFiles}
+import org.apache.maven.archiver.{MavenArchiveConfiguration, MavenArchiver}
+import org.apache.maven.plugin.AbstractMojo
+import org.apache.maven.plugins.annotations.{Component, Mojo, Parameter, ResolutionScope}
+import org.apache.maven.project.MavenProject
+import org.codehaus.plexus.archiver.Archiver
+import org.codehaus.plexus.archiver.jar.JarArchiver
+
+import scala.collection.convert.wrapAsScala._
+
+/**
+ * @author tonytv
+ */
+@Mojo(name = "assemble-container-plugin", requiresDependencyResolution = ResolutionScope.COMPILE)
+class AssembleContainerPluginMojo extends AbstractMojo {
+ object withDependencies
+ object withoutDependencies
+
+ @Parameter(defaultValue = "${project}")
+ var project: MavenProject = null
+
+ @Component(role = classOf[Archiver], hint = "jar")
+ var jarArchiver: JarArchiver = null
+
+ @Parameter
+ var archiveConfiguration: MavenArchiveConfiguration = new MavenArchiveConfiguration
+
+ @Parameter(alias = "UseCommonAssemblyIds", defaultValue = "false")
+ var useCommonAssemblyIds: Boolean = false
+
+
+ def execute() {
+ val jarSuffixes =
+ if (useCommonAssemblyIds) Map(withoutDependencies -> ".jar", withDependencies -> "-jar-with-dependencies.jar")
+ else Map(withoutDependencies -> "-without-dependencies.jar", withDependencies -> "-deploy.jar")
+
+ val jarFiles = jarSuffixes mapValues jarFileInBuildDirectory(build.getFinalName)
+
+ //force recreating the archive
+ archiveConfiguration.setForced(true)
+ archiveConfiguration.setManifestFile(new File(new File(project.getBuild.getOutputDirectory), JarFile.MANIFEST_NAME))
+
+ addClassesDirectory()
+ createArchive(jarFiles(withoutDependencies))
+ project.getArtifact.setFile(jarFiles(withoutDependencies))
+
+ addDependencies()
+ createArchive(jarFiles(withDependencies))
+ }
+
+ private def jarFileInBuildDirectory(name: String)(jarSuffix: String) = {
+ new File(build.getDirectory, name + jarSuffix)
+ }
+
+ private def addClassesDirectory() {
+ val classesDirectory = new File(build.getOutputDirectory)
+ if (classesDirectory.isDirectory) {
+ jarArchiver.addDirectory(classesDirectory)
+ }
+ }
+
+ private def createArchive(jarFile: File) {
+ val mavenArchiver = new MavenArchiver
+ mavenArchiver.setArchiver(jarArchiver)
+ mavenArchiver.setOutputFile(jarFile)
+ mavenArchiver.createArchive(project, archiveConfiguration)
+ }
+
+ private def addDependencies() {
+ Artifacts.getArtifactsToInclude(project).foreach { artifact =>
+ if (artifact.getType == "jar") {
+ jarArchiver.addFile(artifact.getFile, "dependencies/" + artifact.getFile.getName)
+ copyConfigDefinitions(artifact.getFile)
+ }
+ else
+ getLog.warn("Unkown artifact type " + artifact.getType)
+ }
+ }
+
+ private def copyConfigDefinitions(file: File) {
+ JarFiles.withJarFile(file) { jarFile =>
+ for {
+ entry <- jarFile.entries()
+ name = entry.getName
+ if name.startsWith("configdefinitions/") && name.endsWith(".def")
+
+ } copyConfigDefinition(jarFile, entry)
+ }
+ }
+
+ private def copyConfigDefinition(jarFile: JarFile, entry: ZipEntry) {
+ JarFiles.withInputStream(jarFile, entry) { input =>
+ val defPath = entry.getName.replace("/", File.separator)
+ val destinationFile = new File(project.getBuild.getOutputDirectory, defPath)
+ destinationFile.getParentFile.mkdirs()
+
+ Files.withFileOutputStream(destinationFile) { output =>
+ output.getChannel.transferFrom(Channels.newChannel(input), 0, Long.MaxValue)
+ }
+ jarArchiver.addFile(destinationFile, entry.getName)
+ }
+ }
+
+ private def build = project.getBuild
+}
diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/mojo/GenerateBundleClassPathMappingsMojo.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/mojo/GenerateBundleClassPathMappingsMojo.scala
new file mode 100644
index 00000000000..0700a2cf3b2
--- /dev/null
+++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/mojo/GenerateBundleClassPathMappingsMojo.scala
@@ -0,0 +1,96 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.mojo
+
+import java.io.File
+import java.nio.file.Paths
+
+import com.google.common.base.Preconditions
+import com.yahoo.container.plugin.bundle.AnalyzeBundle
+import com.yahoo.vespa.scalalib.osgi.maven.ProjectBundleClassPaths
+import ProjectBundleClassPaths.BundleClasspathMapping
+import com.yahoo.vespa.scalalib.osgi.maven.ProjectBundleClassPaths
+import org.apache.maven.artifact.Artifact
+import org.apache.maven.plugin.AbstractMojo
+import org.apache.maven.plugins.annotations.{ResolutionScope, Mojo, Parameter}
+import org.apache.maven.project.MavenProject
+
+
+
+/**
+ * Generates mapping from Bundle-SymbolicName to classpath elements, e.g
+ * myBundle -> List(.m2/repository/com/mylib/Mylib.jar, myBundleProject/target/classes)
+ * The mapping in stored in a json file.
+ * @author tonytv
+ */
+@Mojo(name = "generate-bundle-classpath-mappings", requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME)
+class GenerateBundleClassPathMappingsMojo extends AbstractMojo {
+ @Parameter(defaultValue = "${project}")
+ private var project: MavenProject = null
+
+ //TODO: Combine with com.yahoo.container.plugin.mojo.GenerateOsgiManifestMojo.bundleSymbolicName
+ @Parameter(alias = "Bundle-SymbolicName", defaultValue = "${project.artifactId}")
+ private var bundleSymbolicName: String = null
+
+
+ /* Sample output -- target/test-classes/bundle-plugin.bundle-classpath-mappings.json
+ {
+ "mainBundle": {
+ "bundleSymbolicName": "bundle-plugin-test",
+ "classPathElements": [
+ "/Users/tonyv/Repos/vespa/bundle-plugin-test/target/classes",
+ "/Users/tonyv/.m2/repository/com/yahoo/vespa/jrt/6-SNAPSHOT/jrt-6-SNAPSHOT.jar",
+ "/Users/tonyv/.m2/repository/com/yahoo/vespa/annotations/6-SNAPSHOT/annotations-6-SNAPSHOT.jar"
+ ]
+ },
+ "providedDependencies": [
+ {
+ "bundleSymbolicName": "jrt",
+ "classPathElements": [
+ "/Users/tonyv/.m2/repository/com/yahoo/vespa/jrt/6-SNAPSHOT/jrt-6-SNAPSHOT.jar"
+ ]
+ }
+ ]
+ }
+ */
+ override def execute(): Unit = {
+ Preconditions.checkNotNull(bundleSymbolicName)
+
+ val (embeddedArtifacts, providedJarArtifacts, _) = asLists(Artifacts.getArtifacts(project))
+
+ val embeddedArtifactsFiles = embeddedArtifacts.map(_.getFile)
+
+ val classPathElements = (outputDirectory +: embeddedArtifactsFiles).map(_.getAbsolutePath)
+
+ val classPathMappings = ProjectBundleClassPaths(
+ mainBundle = BundleClasspathMapping(bundleSymbolicName, classPathElements),
+ providedDependencies = providedJarArtifacts flatMap createDependencyClasspathMapping
+ )
+
+ ProjectBundleClassPaths.save(
+ testOutputPath.resolve(ProjectBundleClassPaths.classPathMappingsFileName),
+ classPathMappings)
+ }
+
+ private def outputDirectory = new File(project.getBuild.getOutputDirectory)
+ private def testOutputPath = Paths.get(project.getBuild.getTestOutputDirectory)
+
+ /* TODO:
+ * 1) add the dependencies of the artifact in the future(i.e. dependencies of dependencies)
+ * or
+ * 2) obtain bundles with embedded dependencies from the maven repository,
+ * and support loading classes from the nested jar files in those bundles.
+ */
+ def createDependencyClasspathMapping(artifact: Artifact): Option[BundleClasspathMapping] = {
+ for (bundleSymbolicName <- bundleSymbolicNameForArtifact(artifact))
+ yield BundleClasspathMapping(bundleSymbolicName, classPathElements = List(artifact.getFile.getAbsolutePath))
+ }
+
+ def bundleSymbolicNameForArtifact(artifact: Artifact): Option[String] = {
+ if (artifact.getFile.getName.endsWith(".jar")) AnalyzeBundle.bundleSymbolicName(artifact.getFile)
+ else Some(artifact.getArtifactId) //Not the best heuristic. The other alternatives are parsing the pom file or
+ //storing information in target/classes when building the provided bundles.
+ }
+
+ def asLists[A](tuple: (Iterable[A], Iterable[A], Iterable[A])) =
+ (tuple._1.toList, tuple._2.toList, tuple._3.toList)
+}
diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/mojo/GenerateOsgiManifestMojo.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/mojo/GenerateOsgiManifestMojo.scala
new file mode 100644
index 00000000000..5e1ea589f36
--- /dev/null
+++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/mojo/GenerateOsgiManifestMojo.scala
@@ -0,0 +1,283 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.mojo
+
+import java.io.File
+import java.util.jar.{Attributes, JarEntry, JarFile}
+import java.util.regex.Pattern
+
+import com.yahoo.container.plugin.bundle.AnalyzeBundle
+import com.yahoo.container.plugin.classanalysis.{Analyze, ExportPackageAnnotation, PackageTally}
+import com.yahoo.container.plugin.mojo.GenerateOsgiManifestMojo._
+import com.yahoo.container.plugin.osgi.ExportPackages
+import com.yahoo.container.plugin.osgi.ExportPackages.Export
+import com.yahoo.container.plugin.osgi.ImportPackages.Import
+import com.yahoo.container.plugin.osgi.{ExportPackageParser, ExportPackages, ImportPackages}
+import com.yahoo.container.plugin.util.Files.allDescendantFiles
+import com.yahoo.container.plugin.util.IO.withFileOutputStream
+import com.yahoo.container.plugin.util.Iteration.toStream
+import com.yahoo.container.plugin.util.JarFiles.{withInputStream, withJarFile}
+import com.yahoo.container.plugin.util.Strings
+import org.apache.maven.artifact.Artifact
+import org.apache.maven.plugin.{AbstractMojo, MojoExecutionException, MojoFailureException}
+import org.apache.maven.plugins.annotations.{Mojo, Parameter, ResolutionScope}
+import org.apache.maven.project.MavenProject
+
+import scala.collection.immutable.Map
+
+
+/**
+ * @author tonytv
+ */
+@Mojo(name = "generate-osgi-manifest", requiresDependencyResolution = ResolutionScope.TEST)
+class GenerateOsgiManifestMojo extends AbstractMojo {
+
+ @Parameter(defaultValue = "${project}")
+ var project: MavenProject = null
+
+ @Parameter
+ var discApplicationClass: String = null
+
+ @Parameter
+ var discPreInstallBundle: String = null
+
+ @Parameter(alias = "Bundle-Version", defaultValue = "${project.version}")
+ var bundleVersion: String = null
+
+ @Parameter(alias = "Bundle-SymbolicName", defaultValue = "${project.artifactId}")
+ var bundleSymbolicName: String = null
+
+ @Parameter(alias = "Bundle-Activator")
+ var bundleActivator: String = null
+
+ @Parameter(alias = "X-JDisc-Privileged-Activator")
+ var jdiscPrivilegedActivator: String = null
+
+ @Parameter(alias = "X-Config-Models")
+ var configModels: String = null
+
+ @Parameter(alias = "Import-Package")
+ var importPackage: String = null
+
+ @Parameter(alias = "WebInfUrl")
+ var webInfUrl: String = null
+
+ @Parameter(alias = "Main-Class")
+ var mainClass: String = null
+
+ @Parameter(alias = "X-Jersey-Binding")
+ var jerseyBinding: String = null
+
+ case class PackageInfo(name : String, exportAnnotation : Option[ExportPackageAnnotation])
+
+ def execute() {
+ try {
+ val (jarArtifactsToInclude, jarArtifactsProvided, nonJarArtifacts) = Artifacts.getArtifacts(project)
+ warnOnUnsupportedArtifacts(nonJarArtifacts)
+
+ val publicPackagesFromProvidedJars = AnalyzeBundle.publicPackagesAggregated(jarArtifactsProvided.map(_.getFile))
+ val includedJarPackageTally = definedPackages(jarArtifactsToInclude)
+
+ val projectPackageTally = analyzeProjectClasses()
+
+ val pluginPackageTally = projectPackageTally.combine(includedJarPackageTally)
+
+ warnIfPackagesDefinedOverlapsGlobalPackages(projectPackageTally.definedPackages ++ includedJarPackageTally.definedPackages,
+ publicPackagesFromProvidedJars.globals)
+
+ if (getLog.isDebugEnabled) {
+ getLog.debug("Referenced packages = " + pluginPackageTally.referencedPackages)
+ getLog.debug("Defined packages = " + pluginPackageTally.definedPackages)
+ getLog.debug("Exported packages of dependencies = " +
+ publicPackagesFromProvidedJars.exports.map(e => (e.packageNames, e.version getOrElse "")).toSet )
+ }
+
+ val calculatedImports = ImportPackages.calculateImports(
+ pluginPackageTally.referencedPackages,
+ pluginPackageTally.definedPackages,
+ ExportPackages.exportsByPackageName(publicPackagesFromProvidedJars.exports))
+
+ val manualImports = emptyToNone(importPackage) map getManualImports getOrElse Map()
+
+ createManifestFile(new File(project.getBuild.getOutputDirectory),
+ manifestContent(
+ project,
+ jarArtifactsToInclude,
+ manualImports,
+ (calculatedImports -- manualImports.keys).values.toSet,
+ pluginPackageTally))
+
+ } catch {
+ case e: MojoFailureException => throw e
+ case e: MojoExecutionException => throw e
+ case e: Exception => throw new MojoExecutionException("Failed generating osgi manifest.", e)
+ }
+ }
+
+ //TODO: Tell which dependency overlaps
+ private def warnIfPackagesDefinedOverlapsGlobalPackages(internalPackages: Set[String], globalPackages: List[String]) {
+ val overlap = internalPackages intersect globalPackages.toSet
+ if (overlap.nonEmpty)
+ throw new MojoExecutionException(
+ "The following packages are both global and included in the bundle:\n%s".format(overlap map (" " + _) mkString ("\n")))
+ }
+
+
+ def osgiExportPackages(exportedPackages: Map[String, ExportPackageAnnotation]): Iterable[String] = {
+ for ((name, annotation) <- exportedPackages)
+ yield name + ";version=" + annotation.osgiVersion
+ }
+
+ def trimWhitespace(lines: Option[String]): String = {
+ lines.getOrElse("").split(",").map(_.trim).mkString(",")
+ }
+
+ def manifestContent(project: MavenProject, jarArtifactsToInclude: Traversable[Artifact],
+ manualImports: Map[String, Option[String]], imports : Set[Import],
+ pluginPackageTally : PackageTally) = {
+ Map[String, String](
+ "Created-By" -> "vespa container maven plugin",
+ "Bundle-ManifestVersion" -> "2",
+ "Bundle-Name" -> project.getName,
+ "Bundle-SymbolicName" -> bundleSymbolicName,
+ "Bundle-Version" -> asBundleVersion(bundleVersion),
+ "Bundle-Vendor" -> "Yahoo!",
+ "Bundle-ClassPath" -> bundleClassPath(jarArtifactsToInclude),
+ "Bundle-Activator" -> bundleActivator,
+ "X-JDisc-Privileged-Activator" -> jdiscPrivilegedActivator,
+ "Main-Class" -> mainClass,
+ "X-JDisc-Application" -> discApplicationClass,
+ "X-JDisc-Preinstall-Bundle" -> trimWhitespace(Option(discPreInstallBundle)),
+ "X-Config-Models" -> configModels,
+ "X-Jersey-Binding" -> jerseyBinding,
+ "WebInfUrl" -> webInfUrl,
+ "Import-Package" -> ((manualImports map asOsgiImport) ++ (imports map {_.asOsgiImport})).toList.sorted.mkString(","),
+ "Export-Package" -> osgiExportPackages(pluginPackageTally.exportedPackages).toList.sorted.mkString(","))
+ .filterNot { case (key, value) => value == null || value.isEmpty }
+
+ }
+
+ def asOsgiImport(importSpec: (String, Option[String])) = importSpec match {
+ case (packageName, Some(version)) => packageName + ";version=" + quote(version)
+ case (packageName, None) => packageName
+ }
+
+ def quote(s: String) = '"' + s + '"'
+
+ def createManifestFile(outputDirectory: File, manifestContent: Map[String, String]) {
+ val manifest = toManifest(manifestContent)
+
+ withFileOutputStream(new File(outputDirectory, JarFile.MANIFEST_NAME)) {
+ outputStream =>
+ manifest.write(outputStream)
+ }
+ }
+
+ def toManifest(manifestContent: Map[String, String]) = {
+ val manifest = new java.util.jar.Manifest
+ val mainAttributes = manifest.getMainAttributes
+
+ mainAttributes.put(Attributes.Name.MANIFEST_VERSION, "1.0")
+ for ((key, value) <- manifestContent)
+ mainAttributes.putValue(key, value)
+
+ manifest
+ }
+
+ private def bundleClassPath(artifactsToInclude : Traversable[Artifact]) =
+ ("." +: artifactsToInclude.map(dependencyPath).toList).mkString(",")
+
+ private def dependencyPath(artifact : Artifact) =
+ "dependencies/" + artifact.getFile.getName
+
+ private def asBundleVersion(projectVersion: String) = {
+ require(projectVersion != null, "Missing project version.")
+
+ val parts = projectVersion.split(Pattern.quote("-"), 2)
+ val numericPart = parts.head.split('.').map(Strings.emptyStringTo("0")).padTo(3, "0").toList
+
+ val majorMinorMicro = numericPart take 3
+ majorMinorMicro.mkString(".")
+ }
+
+ private def warnOnUnsupportedArtifacts(nonJarArtifacts: Traversable[Artifact]) {
+ val unsupportedArtifacts = nonJarArtifacts.toSet.filter(_.getType != "pom")
+
+ for (artifact <- unsupportedArtifacts) {
+ getLog.warn(s"Unsupported artifact '${artifact.getId}': Type '${artifact.getType}' is not supported. Please file a feature request.")
+ }
+ }
+
+ private def analyzeProjectClasses() : PackageTally = {
+ val outputDirectory = new File(project.getBuild.getOutputDirectory)
+
+ val analyzedClasses = allDescendantFiles(outputDirectory).filter(_.getName.endsWith(".class")).
+ map(Analyze.analyzeClass)
+
+ PackageTally.fromAnalyzedClassFiles(analyzedClasses)
+ }
+
+ def definedPackages(jarArtifacts: Iterable[Artifact]) : PackageTally = {
+ PackageTally.combine(
+ for (jarArtifact <- jarArtifacts) yield {
+ withJarFile(jarArtifact.getFile) { jarFile =>
+ definedPackages(jarFile)
+ }
+ })
+ }
+
+ def definedPackages(jarFile: JarFile) = {
+ val analyzedClasses =
+ for {
+ entry <- toStream(jarFile.entries())
+ if !entry.isDirectory
+ if entry.getName.endsWith(".class")
+ metaData = analyzeClass(jarFile, entry)
+ } yield metaData
+
+ PackageTally.fromAnalyzedClassFiles(analyzedClasses)
+ }
+
+ def analyzeClass(jarFile : JarFile, entry : JarEntry) = {
+ try {
+ withInputStream(jarFile, entry)(Analyze.analyzeClass)
+ } catch {
+ case e : Exception =>
+ throw new MojoExecutionException(
+ "While analyzing the class '%s' in jar file '%s'".format(entry.getName, jarFile.getName),
+ e)
+ }
+ }
+}
+
+object GenerateOsgiManifestMojo {
+ def getManualImports(importPackage: String): Map[String, Option[String]] = {
+ try {
+ (for {
+ importDirective <- parseImportPackages(importPackage)
+ packageName <- importDirective.packageNames
+ } yield packageName -> getVersionThrowOthers(importDirective.parameters)).
+ toMap
+
+ } catch {
+ case e: Exception => throw new RuntimeException("Error in Import-Package:" + importPackage, e)
+ }
+ }
+
+ def getVersionThrowOthers(parameters: List[ExportPackages.Parameter]): Option[String] = {
+ parameters match {
+ case List() => None
+ case List(ExportPackages.Parameter("version", v)) => Some(v)
+ case default => throw new RuntimeException("A single, optional version parameter expected, but got " + default)
+ }
+ }
+
+ def parseImportPackages(importPackages: String): List[Export] = {
+ ExportPackageParser.parseAll(importPackages) match {
+ case ExportPackageParser.NoSuccess(msg, _) => throw new RuntimeException(msg)
+ case ExportPackageParser.Success(packages, _) => packages
+ }
+ }
+
+ def emptyToNone(str: String) =
+ Option(str) map {_.trim} filterNot {_.isEmpty}
+}
diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/osgi/ExportPackageParser.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/osgi/ExportPackageParser.scala
new file mode 100644
index 00000000000..6c84a39b975
--- /dev/null
+++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/osgi/ExportPackageParser.scala
@@ -0,0 +1,89 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.osgi
+
+import scala.util.parsing.combinator.JavaTokenParsers
+import ExportPackages.{Parameter, Export}
+import com.yahoo.container.plugin.util.Extractors.ListOf
+import scala.util.parsing.input.CharSequenceReader
+import scala.annotation.tailrec
+
+/**
+ * @author tonytv
+ */
+object ExportPackageParser extends JavaTokenParsers {
+ val ListOfParameter = new ListOf(classOf[Parameter])
+
+
+ def exportPackage = rep1sep(export, ",")
+
+ //TODO: remove when fix is in current scala library
+ //Fix for https://github.com/scala/scala-parser-combinators/pull/4
+ def stringLiteral_fixed: Parser[String] = ("\""+"""([^"\p{Cntrl}\\]|\\[\\'"bfnrt]|\\u[a-fA-F0-9]{4})*+"""+"\"").r
+
+ @SuppressWarnings(Array("unchecked"))
+ def export : Parser[Export] = packageName ~ opt(";" ~> (parameters | export)) ^^ {
+ case (packageName : String) ~ optional => {
+ optional match {
+ case None => Export(List(packageName.asInstanceOf[String]), List())
+ case Some(e: Export) => e.copy(packageNames = packageName +: e.packageNames)
+ case Some(ListOfParameter(parameters)) => Export(List(packageName), parameters)
+ }
+ }
+ }
+
+ def parameters = rep1sep(parameter, ";")
+
+ def parameter = (directive | attribute) ^^ {
+ case k ~ v => Parameter(k.toString, v.toString)
+ }
+
+ def directive = (extended_ <~ ":=") ~ argument
+ def attribute = (extended_ <~ "=") ~ argument
+
+ def packageName = rep1sep(ident_, ".") ^^ {
+ x => x.mkString(".")
+ }
+
+ def extended = rep1("""\p{Alnum}""".r | "_" | "-" | ".") ^^ {
+ _.mkString
+ }
+
+ def argument = (extended_ | stringLiteral_ | failure("argument expected")) ^^ {
+ val quote = '"'.toString
+ _.toString.stripPrefix(quote).stripSuffix(quote)
+ }
+
+ def parseAll(in: CharSequence): ParseResult[List[Export]] = {
+ try {
+ parseAll(exportPackage, in)
+ } catch {
+ case e: StackOverflowError =>
+ throw new RuntimeException("Failed parsing Export-Package: '''\n" + in + "\n'''", e)
+ }
+ }
+
+ //*** For debugging StackOverflow error **/
+ def ident_ = printStackOverflow(ident)("ident")
+ def stringLiteral_ = printStackOverflow(stringLiteral_fixed)("stringLiteral_fixed")
+ def extended_ = printStackOverflow(extended)("extended")
+
+ def printStackOverflow[T](p: => Parser[T])(name: String): Parser[T] = Parser{ in =>
+ try {
+ p(in)
+ } catch {
+ case e: StackOverflowError =>
+ val input = in match {
+ case reader: CharSequenceReader => readerToString(reader)
+ case other => other.toString
+ }
+ println(s"***StackOverflow for $name with input '''$input'''")
+ throw e
+ }
+ }
+
+ @tailrec
+ def readerToString(reader: CharSequenceReader, current: String = ""): String = {
+ if (reader.atEnd) current
+ else readerToString(reader.rest, current + reader.first)
+ }
+}
diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/osgi/ExportPackages.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/osgi/ExportPackages.scala
new file mode 100644
index 00000000000..d0e63cdc212
--- /dev/null
+++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/osgi/ExportPackages.scala
@@ -0,0 +1,27 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.osgi
+
+/**
+ * @author tonytv
+ */
+object ExportPackages {
+
+ case class Export(packageNames: List[String], parameters: List[Parameter]) {
+ def version: Option[String] = {
+ (for (
+ param <- parameters if param.name == "version"
+ ) yield param.value).
+ headOption
+ }
+ }
+
+ case class Parameter(name: String, value: String)
+
+ def exportsByPackageName(exports: Seq[Export]): Map[String, Export] = {
+ (for {
+ export <- exports.reverse //ensure that earlier exports of a package overrides later exports.
+ packageName <- export.packageNames
+ } yield packageName -> export).
+ toMap
+ }
+}
diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/osgi/ImportPackages.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/osgi/ImportPackages.scala
new file mode 100644
index 00000000000..651f389640a
--- /dev/null
+++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/osgi/ImportPackages.scala
@@ -0,0 +1,51 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.osgi
+
+import ExportPackages.Export
+import util.control.Exception
+
+/**
+ * @author tonytv
+ */
+object ImportPackages {
+ case class Import(packageName : String, version : Option[String]) {
+ val majorMinorMicroVersion = Exception.handling(classOf[NumberFormatException]).
+ by( e => throw new IllegalArgumentException(
+ "Invalid version number '%s' for package '%s'.".format(version.get, packageName), e)) {
+
+ version map { _.split('.') take 3 map {_.toInt} }
+ }
+
+ def majorVersion = majorMinorMicroVersion map { _.head }
+
+ // TODO: Detecting guava packages should be based on Bundle-SymbolicName, not package name.
+ def importVersionRange = {
+ def upperLimit =
+ if (isGuavaPackage) InfiniteVersion // guava increases major version for each release
+ else majorVersion.get + 1
+
+ version map (v => "[%s,%s)".format(majorMinorMicroVersion.get.mkString("."), upperLimit))
+ }
+
+ def isGuavaPackage = packageName.equals(GuavaBasePackage) || packageName.startsWith(GuavaBasePackage + ".")
+
+ def asOsgiImport = packageName + (importVersionRange map {";version=\"" + _ + '"'} getOrElse(""))
+ }
+
+
+ val GuavaBasePackage = "com.google.common"
+ val InfiniteVersion = 99999
+
+ def calculateImports(referencedPackages : Set[String],
+ implementedPackages : Set[String],
+ exportedPackages : Map[String, Export]) : Map[String, Import] = {
+ (for {
+ undefinedPackage <- referencedPackages diff implementedPackages
+ export <- exportedPackages.get(undefinedPackage)
+ } yield undefinedPackage -> Import(undefinedPackage, version(export)))(
+ collection.breakOut)
+ }
+
+ def version(export: Export): Option[String] =
+ export.parameters.find(_.name == "version").map(_.value)
+}
diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/util/Extractors.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/util/Extractors.scala
new file mode 100644
index 00000000000..9d51d7c6d6d
--- /dev/null
+++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/util/Extractors.scala
@@ -0,0 +1,17 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.util
+
+/**
+* @author tonytv
+*/
+object Extractors {
+ class ListOf[C](val c : Class[C]) {
+ def unapply[X](xs : X) : Option[List[C]] = {
+ xs match {
+ case x :: xr if c.isInstance(x) => unapply(xr) map ( c.cast(x) :: _)
+ case Nil => Some(Nil)
+ case _ => None
+ }
+ }
+ }
+}
diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/util/Files.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/util/Files.scala
new file mode 100644
index 00000000000..b84bd253867
--- /dev/null
+++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/util/Files.scala
@@ -0,0 +1,23 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.util
+
+import java.io.{FileOutputStream, File}
+import com.yahoo.container.plugin.util.IO._
+
+/**
+ * @author tonytv
+ */
+object Files {
+ def allDescendantFiles(file: File): Stream[File] = {
+ if (file.isFile)
+ Stream(file)
+ else if (file.isDirectory)
+ file.listFiles().toStream.map(allDescendantFiles).flatten
+ else
+ Stream.empty
+ }
+
+ def withFileOutputStream[T](file: File)(f: FileOutputStream => T): T = {
+ using(new FileOutputStream(file), readOnly = false)(f)
+ }
+}
diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/util/IO.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/util/IO.scala
new file mode 100644
index 00000000000..e33a9575d55
--- /dev/null
+++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/util/IO.scala
@@ -0,0 +1,46 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.util
+
+import java.io.{Closeable, FileOutputStream, OutputStream, FileInputStream, File}
+import util.control.Exception
+import scala.Either
+
+/** Utility methods relating to IO
+ * @author tonytv
+ */
+object IO {
+ def withFileInputStream[T](file : File)(f : FileInputStream => T) = {
+ using(new FileInputStream(file), readOnly = true)(f)
+ }
+
+ /**
+ * Creates a new file and all it's parent directories,
+ * and provides a file output stream to the file.
+ *
+ * Exceptions from closing have priority over exceptions from f.
+ */
+ def withFileOutputStream[T](file: File)(f: OutputStream => T) {
+ makeDirectoriesRecursive(file.getParentFile)
+ using(new FileOutputStream(file), readOnly = false )(f)
+ }
+
+ def makeDirectoriesRecursive(file: File) {
+ if (!file.mkdirs() && !file.isDirectory) {
+ throw new RuntimeException("Could not create directory " + file.getPath)
+ }
+ }
+
+ def using[RESOURCE <: Closeable, T](resource : RESOURCE, readOnly : Boolean)(f : RESOURCE => T) : T = {
+ def catchPromiscuously = Exception.catchingPromiscuously(classOf[Throwable])
+
+ val resultOrException = catchPromiscuously either f(resource)
+ val closeException = Exception.allCatch either resource.close()
+
+ prioritizeFirstException(
+ resultOrException,
+ if (readOnly) Right(()) else closeException) fold (throw _, identity)
+ }
+
+ private def prioritizeFirstException[T](first: Either[Throwable, T], second: Either[Throwable, Unit]) =
+ first fold ( Left(_), value => second fold ( Left(_), _ => Right(value) ) )
+}
diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/util/Iteration.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/util/Iteration.scala
new file mode 100644
index 00000000000..ec16ce712e6
--- /dev/null
+++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/util/Iteration.scala
@@ -0,0 +1,14 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.util
+
+ /**
+ * @author tonytv
+ */
+object Iteration {
+ def toStream[T](enumeration: java.util.Enumeration[T]): Stream[T] = {
+ if (enumeration.hasMoreElements)
+ Stream.cons(enumeration.nextElement(), toStream(enumeration))
+ else
+ Stream.Empty
+ }
+}
diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/util/JarFiles.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/util/JarFiles.scala
new file mode 100644
index 00000000000..86e46295448
--- /dev/null
+++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/util/JarFiles.scala
@@ -0,0 +1,24 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.util
+
+import java.util.jar.JarFile
+import java.util.zip.{ZipFile, ZipEntry}
+import IO.using
+import java.io.{Closeable, InputStream, File}
+
+/**
+ * @author tonytv
+ */
+object JarFiles {
+ def withJarFile[T](file : File)(f : JarFile => T ) : T =
+ using(new JarFile(file) with Closeable, readOnly = true)(f)
+
+ def withInputStream[T](zipFile: ZipFile, zipEntry: ZipEntry)(f: InputStream => T): T =
+ using(zipFile.getInputStream(zipEntry), readOnly = true)(f)
+
+ def getManifest(jarFile : File) : Option[java.util.jar.Manifest] = {
+ withJarFile(jarFile) { jar =>
+ Option(jar.getManifest)
+ }
+ }
+}
diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/util/Maps.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/util/Maps.scala
new file mode 100644
index 00000000000..7f12c5ba95d
--- /dev/null
+++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/util/Maps.scala
@@ -0,0 +1,19 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.util
+
+import collection.mutable.MultiMap
+
+/**
+ * @author tonytv
+ */
+object Maps {
+ def combine[K, V](map1 : Map[K, V], map2 : Map[K, V])(f : (V, V) => V) : Map[K, V] = {
+ def logicError : V = throw new RuntimeException("Logic error.")
+ def combineValues(key : K) = key -> f(map1.getOrElse(key, logicError), map2.getOrElse(key, logicError))
+
+ val keysInBoth = map1.keySet intersect map2.keySet
+ def notInBoth = !keysInBoth.contains(_ : K)
+
+ map1.filterKeys(notInBoth) ++ map2.filterKeys(notInBoth) ++ keysInBoth.map(combineValues)
+ }
+}
diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/util/Strings.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/util/Strings.scala
new file mode 100644
index 00000000000..a0c3c1d001f
--- /dev/null
+++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/util/Strings.scala
@@ -0,0 +1,14 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.util
+
+ /**
+ * @author tonytv
+ */
+object Strings {
+ def emptyStringTo(replacement: String)(s: String) = {
+ if (s.isEmpty) replacement
+ else s
+ }
+
+ def noneIfEmpty(s: String) = Option(s).filterNot(_.isEmpty)
+}