diff options
author | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
---|---|---|
committer | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
commit | 72231250ed81e10d66bfe70701e64fa5fe50f712 (patch) | |
tree | 2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /bundle-plugin/src/main |
Publish
Diffstat (limited to 'bundle-plugin/src/main')
33 files changed, 1695 insertions, 0 deletions
diff --git a/bundle-plugin/src/main/java/com/yahoo/container/plugin/mojo/GenerateSourcesMojo.java b/bundle-plugin/src/main/java/com/yahoo/container/plugin/mojo/GenerateSourcesMojo.java new file mode 100644 index 00000000000..cb2e54024b4 --- /dev/null +++ b/bundle-plugin/src/main/java/com/yahoo/container/plugin/mojo/GenerateSourcesMojo.java @@ -0,0 +1,164 @@ +// 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 org.apache.maven.execution.MavenSession; +import org.apache.maven.model.Dependency; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugin.BuildPluginManager; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Component; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; +import org.apache.maven.project.MavenProject; +import org.codehaus.plexus.component.annotations.Requirement; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.Properties; +import java.util.regex.Pattern; + +import static org.twdata.maven.mojoexecutor.MojoExecutor.*; + +/** + * Calls the generate-sources phase in the container lifecycle defined in lifecycle.xml. + * + * @author tonytv + */ +@Mojo(name = "generateSources", requiresDependencyResolution = ResolutionScope.COMPILE) +public class GenerateSourcesMojo extends AbstractMojo { + + @Parameter(defaultValue = "${project}") + protected org.apache.maven.project.MavenProject project; + + @Parameter(defaultValue = "${session}", readonly = true, required = true) + protected MavenSession session; + + @Component + @Requirement + private BuildPluginManager pluginManager; + + @Parameter + protected String configGenVersion; + + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + String configGenVersion = getConfigGenVersion(); + getLog().debug("configGenVersion = " + configGenVersion); + + executeMojo( + plugin( + groupId("com.yahoo.vespa"), + artifactId("config-class-plugin"), + version(releaseVersion(configGenVersion))), + goal("config-gen"), + configuration( + element(name("defFilesDirectories"), "src/main/resources/configdefinitions")), + createExecutionEnvironment()); + //Compile source roots added in container-lifecycle is not currently + //propagated automatically to this project. + project.addCompileSourceRoot(project.getBuild().getDirectory() + "/generated-sources/vespa-configgen-plugin"); + } + + private ExecutionEnvironment createExecutionEnvironment() throws MojoExecutionException { + return executionEnvironment( + project, + session, + pluginManager); + } + + private String getConfigGenVersion() throws MojoExecutionException { + if (configGenVersion != null && !configGenVersion.isEmpty()) { + return configGenVersion; + } + Dependency containerDev = getVespaDependency("container-dev"); + if (containerDev != null) + return containerDev.getVersion(); + + Dependency prelude = getVespaDependency("prelude"); + if (prelude != null) + return prelude.getVersion(); + + Dependency docproc = getVespaDependency("docproc"); + if (docproc != null) + return docproc.getVersion(); + + MavenProject parent = getVespaParent(); + if (parent != null) + return parent.getVersion(); + + String defaultConfigGenVersion = loadDefaultConfigGenVersion(); + getLog().warn( + String.format("Did not find container-dev, guessing that version '%s' of config_gen should be used.", + defaultConfigGenVersion)); + + return defaultConfigGenVersion; + } + + static String loadDefaultConfigGenVersion() throws MojoExecutionException { + Properties props = new Properties(); + try { + props.load(GenerateSourcesMojo.class.getResourceAsStream("/build.properties")); + } catch (IOException e) { + throw new MojoExecutionException("Failed to resolve version of com.yahoo.vespa:config-class-plugin.", + new FileNotFoundException("/build.properties")); + } + return props.getProperty("projectVersion"); + } + + private MavenProject getVespaParent() { + MavenProject parent = project.getParent(); + if (parent != null && + "com.yahoo.vespa".equals(parent.getGroupId()) && + "parent".equals(parent.getArtifactId())) { + + return parent; + } + + return null; + } + + private Dependency getVespaDependency(String artifactId) { + for (Object element : project.getDependencies()) { + Dependency dependency = (Dependency) element; + + if ("com.yahoo.vespa".equals(dependency.getGroupId()) && + artifactId.equals(dependency.getArtifactId())) { + return dependency; + } + } + + return null; + } + + static String releaseVersion(String mavenVersion) { + if (mavenVersion.endsWith("-SNAPSHOT")) { + return mavenVersion; + } else { + String[] parts = mavenVersion.split(Pattern.quote(".")); + if (parts.length <= 3) { + return mavenVersion; + } else { + return stringJoin(Arrays.asList(parts).subList(0, 3), "."); + } + } + } + + static String stringJoin(Collection<String> elements, String sep) { + StringBuilder builder = new StringBuilder(); + Iterator<String> i = elements.iterator(); + + if (i.hasNext()) + builder.append(i.next()); + + while(i.hasNext()) { + builder.append(sep).append(i.next()); + } + + return builder.toString(); + } +} diff --git a/bundle-plugin/src/main/resources/META-INF/m2e/lifecycle-mapping-metadata.xml b/bundle-plugin/src/main/resources/META-INF/m2e/lifecycle-mapping-metadata.xml new file mode 100644 index 00000000000..a2665c223d1 --- /dev/null +++ b/bundle-plugin/src/main/resources/META-INF/m2e/lifecycle-mapping-metadata.xml @@ -0,0 +1,18 @@ +<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. --> +<lifecycleMappingMetadata> + <pluginExecutions> + <pluginExecution> + <pluginExecutionFilter> + <goals> + <goal>generateSources</goal> + </goals> + </pluginExecutionFilter> + <action> + <execute> + <runOnIncremental>false</runOnIncremental> + <runOnConfiguration>true</runOnConfiguration> + </execute> + </action> + </pluginExecution> + </pluginExecutions> +</lifecycleMappingMetadata> diff --git a/bundle-plugin/src/main/resources/META-INF/maven/.gitignore b/bundle-plugin/src/main/resources/META-INF/maven/.gitignore new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/bundle-plugin/src/main/resources/META-INF/maven/.gitignore diff --git a/bundle-plugin/src/main/resources/META-INF/plexus/components.xml b/bundle-plugin/src/main/resources/META-INF/plexus/components.xml new file mode 100644 index 00000000000..126c9435ffa --- /dev/null +++ b/bundle-plugin/src/main/resources/META-INF/plexus/components.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. --> + +<component-set> + <components> + <component> + <role>org.apache.maven.lifecycle.mapping.LifecycleMapping</role> + <role-hint>container-plugin</role-hint> + <implementation> + org.apache.maven.lifecycle.mapping.DefaultLifecycleMapping + </implementation> + <configuration> + + <phases> + <process-resources>org.apache.maven.plugins:maven-resources-plugin:resources</process-resources> + <generate-sources>com.yahoo.vespa:bundle-plugin:generateSources</generate-sources> + <compile>org.apache.maven.plugins:maven-compiler-plugin:compile</compile> + <process-test-resources> + org.apache.maven.plugins:maven-resources-plugin:testResources, + com.yahoo.vespa:bundle-plugin:generate-bundle-classpath-mappings + </process-test-resources> + <test-compile>org.apache.maven.plugins:maven-compiler-plugin:testCompile</test-compile> + <test>org.apache.maven.plugins:maven-surefire-plugin:test</test> + <package> + com.yahoo.vespa:bundle-plugin:generate-osgi-manifest, + com.yahoo.vespa:bundle-plugin:assemble-container-plugin + </package> + <install>org.apache.maven.plugins:maven-install-plugin:install</install> + <deploy>org.apache.maven.plugins:maven-deploy-plugin:deploy</deploy> + </phases> + + </configuration> + </component> + + <component> + <role>org.apache.maven.artifact.handler.ArtifactHandler</role> + <role-hint>container-plugin</role-hint> + <implementation> + org.apache.maven.artifact.handler.DefaultArtifactHandler + </implementation> + <configuration> + <type>container-plugin</type> + <extension>jar</extension> + <language>java</language> + <addedToClasspath>true</addedToClasspath> + </configuration> + </component> + + </components> +</component-set> diff --git a/bundle-plugin/src/main/resources/build.properties b/bundle-plugin/src/main/resources/build.properties new file mode 100644 index 00000000000..adcd64886bd --- /dev/null +++ b/bundle-plugin/src/main/resources/build.properties @@ -0,0 +1 @@ +projectVersion=${project.version} 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) +} |