aboutsummaryrefslogtreecommitdiffstats
path: root/bundle-plugin/src/main/java/com/yahoo/container
diff options
context:
space:
mode:
authorOlli Virtanen <olli.virtanen@oath.com>2018-06-13 13:38:26 +0200
committerOlli Virtanen <olli.virtanen@oath.com>2018-06-13 13:38:26 +0200
commitd5836f186c269986cb9d86374b54b540bf600930 (patch)
treee95e2ec309835ef1d285630e87bae5d177e9934b /bundle-plugin/src/main/java/com/yahoo/container
parenteb80fb0d3a6004431ff13e36e9f480ccb32ec31f (diff)
Bundle-plugin Scala code converted to Java
Diffstat (limited to 'bundle-plugin/src/main/java/com/yahoo/container')
-rw-r--r--bundle-plugin/src/main/java/com/yahoo/container/plugin/bundle/AnalyzeBundle.java94
-rw-r--r--bundle-plugin/src/main/java/com/yahoo/container/plugin/bundle/TransformExportPackages.java62
-rw-r--r--bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/Analyze.java87
-rw-r--r--bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/AnalyzeClassVisitor.java162
-rw-r--r--bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/AnalyzeFieldVisitor.java49
-rw-r--r--bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/AnalyzeMethodVisitor.java168
-rw-r--r--bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/AnalyzeSignatureVisitor.java119
-rw-r--r--bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/ClassFileMetaData.java35
-rw-r--r--bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/ExportPackageAnnotation.java62
-rw-r--r--bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/ImportCollector.java35
-rw-r--r--bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/PackageTally.java79
-rw-r--r--bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/Packages.java43
-rw-r--r--bundle-plugin/src/main/java/com/yahoo/container/plugin/mojo/Artifacts.java69
-rw-r--r--bundle-plugin/src/main/java/com/yahoo/container/plugin/mojo/AssembleContainerPluginMojo.java145
-rw-r--r--bundle-plugin/src/main/java/com/yahoo/container/plugin/mojo/GenerateBundleClassPathMappingsMojo.java115
-rw-r--r--bundle-plugin/src/main/java/com/yahoo/container/plugin/mojo/GenerateOsgiManifestMojo.java313
-rw-r--r--bundle-plugin/src/main/java/com/yahoo/container/plugin/osgi/ExportPackageParser.java283
-rw-r--r--bundle-plugin/src/main/java/com/yahoo/container/plugin/osgi/ExportPackages.java70
-rw-r--r--bundle-plugin/src/main/java/com/yahoo/container/plugin/osgi/ImportPackages.java97
-rw-r--r--bundle-plugin/src/main/java/com/yahoo/container/plugin/util/Files.java30
-rw-r--r--bundle-plugin/src/main/java/com/yahoo/container/plugin/util/IO.java41
-rw-r--r--bundle-plugin/src/main/java/com/yahoo/container/plugin/util/JarFiles.java36
-rw-r--r--bundle-plugin/src/main/java/com/yahoo/container/plugin/util/Maps.java31
-rw-r--r--bundle-plugin/src/main/java/com/yahoo/container/plugin/util/Strings.java26
-rw-r--r--bundle-plugin/src/main/java/com/yahoo/container/plugin/util/ThrowingFunction.java11
25 files changed, 2262 insertions, 0 deletions
diff --git a/bundle-plugin/src/main/java/com/yahoo/container/plugin/bundle/AnalyzeBundle.java b/bundle-plugin/src/main/java/com/yahoo/container/plugin/bundle/AnalyzeBundle.java
new file mode 100644
index 00000000000..798fea2644e
--- /dev/null
+++ b/bundle-plugin/src/main/java/com/yahoo/container/plugin/bundle/AnalyzeBundle.java
@@ -0,0 +1,94 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.bundle;
+
+import com.yahoo.container.plugin.osgi.ExportPackageParser;
+import com.yahoo.container.plugin.osgi.ExportPackages.Export;
+import com.yahoo.container.plugin.util.JarFiles;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.jar.Manifest;
+import java.util.stream.Collectors;
+
+/**
+ * @author Tony Vaagenes
+ * @author ollivir
+ */
+public class AnalyzeBundle {
+ public static class PublicPackages {
+ public final List<Export> exports;
+ public final List<String> globals;
+
+ public PublicPackages(List<Export> exports, List<String> globals) {
+ this.exports = exports;
+ this.globals = globals;
+ }
+ }
+
+ public static PublicPackages publicPackagesAggregated(Collection<File> jarFiles) {
+ List<Export> exports = new ArrayList<>();
+ List<String> globals = new ArrayList<>();
+
+ for (File jarFile : jarFiles) {
+ PublicPackages pp = publicPackages(jarFile);
+ exports.addAll(pp.exports);
+ globals.addAll(pp.globals);
+ }
+ return new PublicPackages(exports, globals);
+ }
+
+ public static PublicPackages publicPackages(File jarFile) {
+ try {
+ Optional<Manifest> jarManifest = JarFiles.getManifest(jarFile);
+ if (jarManifest.isPresent()) {
+ Manifest manifest = jarManifest.get();
+ if (isOsgiManifest(manifest)) {
+ return new PublicPackages(parseExports(manifest), parseGlobals(manifest));
+ }
+ }
+ return new PublicPackages(Collections.emptyList(), Collections.emptyList());
+ } catch (Exception e) {
+ throw new RuntimeException(String.format("Invalid manifest in bundle '%s'", jarFile.getPath()), e);
+ }
+ }
+
+ public static Optional<String> bundleSymbolicName(File jarFile) {
+ return JarFiles.getManifest(jarFile).flatMap(AnalyzeBundle::getBundleSymbolicName);
+ }
+
+ private static List<Export> parseExportsFromAttribute(Manifest manifest, String attributeName) {
+ return getMainAttributeValue(manifest, attributeName).map(ExportPackageParser::parseExports).orElseGet(() -> new ArrayList<>());
+ }
+
+ private static List<Export> parseExports(Manifest jarManifest) {
+ return parseExportsFromAttribute(jarManifest, "Export-Package");
+ }
+
+ private static List<String> parseGlobals(Manifest manifest) {
+ List<Export> globals = parseExportsFromAttribute(manifest, "Global-Package");
+
+ for (Export export : globals) {
+ if (export.getParameters().isEmpty() == false) {
+ throw new RuntimeException("Parameters not valid for Global-Package.");
+ }
+ }
+
+ return globals.stream().flatMap(g -> g.getPackageNames().stream()).collect(Collectors.toList());
+ }
+
+ private static Optional<String> getMainAttributeValue(Manifest manifest, String attributeName) {
+ return Optional.ofNullable(manifest.getMainAttributes().getValue(attributeName));
+ }
+
+ private static boolean isOsgiManifest(Manifest mf) {
+ return getBundleSymbolicName(mf).isPresent();
+ }
+
+ private static Optional<String> getBundleSymbolicName(Manifest mf) {
+ return getMainAttributeValue(mf, "Bundle-SymbolicName");
+ }
+}
diff --git a/bundle-plugin/src/main/java/com/yahoo/container/plugin/bundle/TransformExportPackages.java b/bundle-plugin/src/main/java/com/yahoo/container/plugin/bundle/TransformExportPackages.java
new file mode 100644
index 00000000000..8686fef0a55
--- /dev/null
+++ b/bundle-plugin/src/main/java/com/yahoo/container/plugin/bundle/TransformExportPackages.java
@@ -0,0 +1,62 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.bundle;
+
+import com.yahoo.container.plugin.osgi.ExportPackages.Export;
+import com.yahoo.container.plugin.osgi.ExportPackages.Parameter;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author Tony Vaagenes
+ * @author ollivir
+ */
+public class TransformExportPackages {
+ public static List<Export> replaceVersions(List<Export> exports, String newVersion) {
+ List<Export> ret = new ArrayList<>();
+
+ for (Export export : exports) {
+ List<Parameter> newParams = new ArrayList<>();
+ for (Parameter param : export.getParameters()) {
+ if ("version".equals(param.getName())) {
+ newParams.add(new Parameter("version", newVersion));
+ } else {
+ newParams.add(param);
+ }
+ }
+ ret.add(new Export(export.getPackageNames(), newParams));
+ }
+ return ret;
+ }
+
+ public static List<Export> removeUses(List<Export> exports) {
+ List<Export> ret = new ArrayList<>();
+
+ for (Export export : exports) {
+ List<Parameter> newParams = new ArrayList<>();
+ for (Parameter param : export.getParameters()) {
+ if ("uses".equals(param.getName()) == false) {
+ newParams.add(param);
+ }
+ }
+ ret.add(new Export(export.getPackageNames(), newParams));
+ }
+ return ret;
+ }
+
+ public static String toExportPackageProperty(List<Export> exports) {
+ return exports.stream().map(exp -> {
+ String oneExport = String.join(";", exp.getPackageNames());
+ if (exp.getParameters().size() > 0) {
+ String paramString = exp.getParameters().stream().map(param -> param.getName() + "=" + quote(param.getValue())).collect(Collectors.joining(";"));
+ oneExport += ";" + paramString;
+ }
+ return oneExport;
+ }).collect(Collectors.joining(","));
+ }
+
+ public static String quote(String s) {
+ return "\"" + s + "\"";
+ }
+}
diff --git a/bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/Analyze.java b/bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/Analyze.java
new file mode 100644
index 00000000000..c59f8559405
--- /dev/null
+++ b/bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/Analyze.java
@@ -0,0 +1,87 @@
+// Copyright 2018 Yahoo Holdings. 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.AnnotationVisitor;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Optional;
+
+import static com.yahoo.container.plugin.util.IO.withFileInputStream;
+
+/**
+ * Main entry point for class analysis
+ *
+ * @author Tony Vaagenes
+ * @author ollivir
+ */
+public class Analyze {
+ public static ClassFileMetaData analyzeClass(File classFile) {
+ try {
+ return withFileInputStream(classFile, Analyze::analyzeClass);
+ } catch (RuntimeException e) {
+ throw new RuntimeException("An error occurred when analyzing " + classFile.getPath(), e);
+ }
+ }
+
+ public static ClassFileMetaData analyzeClass(InputStream inputStream) {
+ try {
+ AnalyzeClassVisitor visitor = new AnalyzeClassVisitor();
+ new ClassReader(inputStream).accept(visitor, ClassReader.SKIP_DEBUG);
+ return visitor.result();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ static Optional<String> internalNameToClassName(String internalClassName) {
+ if(internalClassName == null) {
+ return Optional.empty();
+ } else {
+ return getClassName(Type.getObjectType(internalClassName));
+ }
+ }
+
+ static Optional<String> getClassName(Type aType) {
+ switch (aType.getSort()) {
+ case Type.ARRAY:
+ return getClassName(aType.getElementType());
+ case Type.OBJECT:
+ return Optional.of(aType.getClassName());
+ default:
+ return Optional.empty();
+ }
+ }
+
+ static AnnotationVisitor visitAnnotationDefault(ImportCollector collector) {
+ return new AnnotationVisitor(Opcodes.ASM6) {
+ @Override
+ public void visit(String name, Object value) {
+ }
+
+ @Override
+ public void visitEnum(String name, String desc, String value) {
+ collector.addImportWithTypeDesc(desc);
+ }
+
+ @Override
+ public AnnotationVisitor visitArray(String name) {
+ return this;
+ }
+
+ @Override
+ public AnnotationVisitor visitAnnotation(String name, String desc) {
+ collector.addImportWithTypeDesc(desc);
+ return this;
+ }
+
+ @Override
+ public void visitEnd() {
+ }
+ };
+ }
+}
diff --git a/bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/AnalyzeClassVisitor.java b/bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/AnalyzeClassVisitor.java
new file mode 100644
index 00000000000..d9519fd7986
--- /dev/null
+++ b/bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/AnalyzeClassVisitor.java
@@ -0,0 +1,162 @@
+// Copyright 2018 Yahoo Holdings. 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.osgi.annotation.ExportPackage;
+import com.yahoo.osgi.annotation.Version;
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.Attribute;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * Picks up classes used in class files.
+ *
+ * @author Tony Vaagenes
+ * @author ollivir
+ */
+class AnalyzeClassVisitor extends ClassVisitor implements ImportCollector {
+ private String name = null;
+ private Set<String> imports = new HashSet<>();
+ private Optional<ExportPackageAnnotation> exportPackageAnnotation = Optional.empty();
+
+ AnalyzeClassVisitor() {
+ super(Opcodes.ASM6);
+ }
+
+ @Override
+ public Set<String> imports() {
+ return imports;
+ }
+
+ @Override
+ public void visitAttribute(Attribute attribute) {
+ addImport(Type.getObjectType(attribute.type));
+ }
+
+ @Override
+ public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
+ Analyze.getClassName(Type.getReturnType(desc)).ifPresent(imports::add);
+ Arrays.asList(Type.getArgumentTypes(desc)).forEach(argType -> Analyze.getClassName(argType).ifPresent(imports::add));
+ if (exceptions != null) {
+ Arrays.asList(exceptions).forEach(ex -> Analyze.internalNameToClassName(ex).ifPresent(imports::add));
+ }
+
+ AnalyzeSignatureVisitor.analyzeMethod(signature, this);
+ return new AnalyzeMethodVisitor(this);
+ }
+
+ @Override
+ public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
+ Analyze.getClassName(Type.getType(desc)).ifPresent(imports::add);
+
+ AnalyzeSignatureVisitor.analyzeField(signature, this);
+ return new AnalyzeFieldVisitor(this);
+ }
+
+ @Override
+ public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
+ this.name = Analyze.internalNameToClassName(name)
+ .orElseThrow(() -> new RuntimeException("Unable to resolve class name for " + name));
+
+ addImportWithInternalName(superName);
+ Arrays.asList(interfaces).forEach(this::addImportWithInternalName);
+
+ AnalyzeSignatureVisitor.analyzeClass(signature, this);
+ }
+
+ @Override
+ public void visitInnerClass(String name, String outerName, String innerName, int access) {
+ }
+
+ @Override
+ public void visitOuterClass(String owner, String name, String desc) {
+ }
+
+ @Override
+ public void visitSource(String source, String debug) {
+ }
+
+ @Override
+ public void visitEnd() {
+ }
+
+ @SuppressWarnings("unchecked")
+ private static <T> T defaultVersionValue(String name) {
+ try {
+ return (T) Version.class.getMethod(name).getDefaultValue();
+ } catch (NoSuchMethodException e) {
+ throw new IllegalArgumentException("Could not locate method " + name);
+ }
+ }
+
+ private AnnotationVisitor visitExportPackage() {
+ return new AnnotationVisitor(Opcodes.ASM6) {
+ private int major = defaultVersionValue("major");
+ private int minor = defaultVersionValue("minor");
+ private int micro = defaultVersionValue("micro");
+ private String qualifier = defaultVersionValue("qualifier");
+
+ @Override
+ public void visit(String name, Object value) {
+ if (name != null) {
+ switch (name) {
+ case "major":
+ major = (int) value;
+ break;
+ case "minor":
+ minor = (int) value;
+ break;
+ case "micro":
+ micro = (int) value;
+ break;
+ case "qualifier":
+ qualifier = (String) value;
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void visitEnd() {
+ exportPackageAnnotation = Optional.of(new ExportPackageAnnotation(major, minor, micro, qualifier));
+ }
+
+ @Override
+ public void visitEnum(String name, String desc, String value) {
+ }
+
+ @Override
+ public AnnotationVisitor visitArray(String name) {
+ return this;
+ }
+
+ @Override
+ public AnnotationVisitor visitAnnotation(String name, String desc) {
+ return this;
+ }
+ };
+ }
+
+ @Override
+ public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+ if (ExportPackage.class.getName().equals(Type.getType(desc).getClassName())) {
+ return visitExportPackage();
+ } else {
+ addImportWithTypeDesc(desc);
+ return Analyze.visitAnnotationDefault(this);
+ }
+ }
+
+ ClassFileMetaData result() {
+ assert (!imports.contains("int"));
+ return new ClassFileMetaData(name, imports, exportPackageAnnotation);
+ }
+}
diff --git a/bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/AnalyzeFieldVisitor.java b/bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/AnalyzeFieldVisitor.java
new file mode 100644
index 00000000000..ea10b6ef0aa
--- /dev/null
+++ b/bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/AnalyzeFieldVisitor.java
@@ -0,0 +1,49 @@
+// Copyright 2018 Yahoo Holdings. 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.AnnotationVisitor;
+import org.objectweb.asm.Attribute;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * @author ollivir
+ */
+public class AnalyzeFieldVisitor extends FieldVisitor implements ImportCollector {
+ private final AnalyzeClassVisitor analyzeClassVisitor;
+ private final Set<String> imports = new HashSet<>();
+
+ public AnalyzeFieldVisitor(AnalyzeClassVisitor analyzeClassVisitor) {
+ super(Opcodes.ASM6);
+ this.analyzeClassVisitor = analyzeClassVisitor;
+ }
+
+ @Override
+ public Set<String> imports() {
+ return imports;
+ }
+
+ @Override
+ public void visitAttribute(Attribute attribute) {
+ addImport(Type.getObjectType(attribute.type));
+ }
+
+ public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+ addImportWithTypeDesc(desc);
+
+ return Analyze.visitAnnotationDefault(this);
+ }
+
+ public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) {
+ return visitAnnotation(desc, visible);
+ }
+
+ @Override
+ public void visitEnd() {
+ analyzeClassVisitor.addImports(imports);
+ }
+}
diff --git a/bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/AnalyzeMethodVisitor.java b/bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/AnalyzeMethodVisitor.java
new file mode 100644
index 00000000000..b1a92f9c10b
--- /dev/null
+++ b/bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/AnalyzeMethodVisitor.java
@@ -0,0 +1,168 @@
+// Copyright 2018 Yahoo Holdings. 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.AnnotationVisitor;
+import org.objectweb.asm.Attribute;
+import org.objectweb.asm.Handle;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Picks up classes used in method bodies.
+ *
+ * @author Tony Vaagenes
+ * @author ollivir
+ */
+class AnalyzeMethodVisitor extends MethodVisitor implements ImportCollector {
+ private final Set<String> imports = new HashSet<>();
+ private final AnalyzeClassVisitor analyzeClassVisitor;
+
+ AnalyzeMethodVisitor(AnalyzeClassVisitor analyzeClassVisitor) {
+ super(Opcodes.ASM6);
+ this.analyzeClassVisitor = analyzeClassVisitor;
+ }
+
+ public Set<String> imports() {
+ return imports;
+ }
+
+ @Override
+ public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) {
+ return visitAnnotation(desc, visible);
+ }
+
+ @Override
+ public AnnotationVisitor visitAnnotationDefault() {
+ return Analyze.visitAnnotationDefault(this);
+ }
+
+ @Override
+ public void visitAttribute(Attribute attribute) {
+ addImport(Type.getObjectType(attribute.type));
+ }
+
+ @Override
+ public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+ addImportWithTypeDesc(desc);
+
+ return Analyze.visitAnnotationDefault(this);
+ }
+
+ @Override
+ public void visitEnd() {
+ super.visitEnd();
+ analyzeClassVisitor.addImports(imports);
+ }
+
+ @Override
+ public void visitMultiANewArrayInsn(String desc, int dims) {
+ addImportWithTypeDesc(desc);
+ }
+
+ @Override
+ public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
+ addImportWithInternalName(owner);
+ Arrays.asList(Type.getArgumentTypes(desc)).forEach(this::addImport);
+ addImport(Type.getReturnType(desc));
+ }
+
+ @Override
+ public void visitFieldInsn(int opcode, String owner, String name, String desc) {
+ addImportWithInternalName(owner);
+ addImportWithTypeDesc(desc);
+ }
+
+ @Override
+ public void visitTypeInsn(int opcode, String type) {
+ addImportWithInternalName(type);
+ }
+
+ @Override
+ public void visitTryCatchBlock(Label start, Label end, Label handler, String type) {
+ if (type != null) { //null means finally block
+ addImportWithInternalName(type);
+ }
+ }
+
+ @Override
+ public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index) {
+ addImportWithTypeDesc(desc);
+ }
+
+ @Override
+ public void visitLdcInsn(Object constant) {
+ if (constant instanceof Type) {
+ addImport((Type) constant);
+ }
+ }
+
+ @Override
+ public void visitInvokeDynamicInsn(String name, String desc, Handle bootstrapMethod, Object... bootstrapMethodArgs) {
+ for (Object arg : bootstrapMethodArgs) {
+ if (arg instanceof Type) {
+ addImport((Type) arg);
+ } else if (arg instanceof Handle) {
+ addImportWithInternalName(((Handle) arg).getOwner());
+ Arrays.asList(Type.getArgumentTypes(desc)).forEach(this::addImport);
+ } else if ((arg instanceof Number) == false && (arg instanceof String) == false) {
+ throw new AssertionError("Unexpected type " + arg.getClass() + " with value '" + arg + "'");
+ }
+ }
+ }
+
+ @Override
+ public void visitMaxs(int maxStack, int maxLocals) {
+ }
+
+ @Override
+ public void visitLineNumber(int line, Label start) {
+ }
+
+ //only for debugging
+ @Override
+ public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) {
+ }
+
+ @Override
+ public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) {
+ super.visitTableSwitchInsn(min, max, dflt, labels);
+ }
+
+ @Override
+ public void visitIincInsn(int variable, int increment) {
+ }
+
+ @Override
+ public void visitLabel(Label label) {
+ }
+
+ @Override
+ public void visitJumpInsn(int opcode, Label label) {
+ }
+
+ @Override
+ public void visitVarInsn(int opcode, int variable) {
+ }
+
+ @Override
+ public void visitIntInsn(int opcode, int operand) {
+ }
+
+ @Override
+ public void visitInsn(int opcode) {
+ }
+
+ @Override
+ public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) {
+ }
+
+ @Override
+ public void visitCode() {
+ }
+}
diff --git a/bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/AnalyzeSignatureVisitor.java b/bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/AnalyzeSignatureVisitor.java
new file mode 100644
index 00000000000..0f5fcf89f6a
--- /dev/null
+++ b/bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/AnalyzeSignatureVisitor.java
@@ -0,0 +1,119 @@
+// Copyright 2018 Yahoo Holdings. 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;
+import org.objectweb.asm.signature.SignatureVisitor;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * @author Tony Vaagenes
+ * @author ollivir
+ */
+
+class AnalyzeSignatureVisitor extends SignatureVisitor implements ImportCollector {
+ private final AnalyzeClassVisitor analyzeClassVisitor;
+ private Set<String> imports = new HashSet<>();
+
+ AnalyzeSignatureVisitor(AnalyzeClassVisitor analyzeClassVisitor) {
+ super(Opcodes.ASM6);
+ this.analyzeClassVisitor = analyzeClassVisitor;
+ }
+
+ public Set<String> imports() {
+ return imports;
+ }
+
+ @Override
+ public void visitEnd() {
+ super.visitEnd();
+ analyzeClassVisitor.addImports(imports);
+ }
+
+ @Override
+ public void visitClassType(String className) {
+ addImportWithInternalName(className);
+ }
+
+ @Override
+ public void visitFormalTypeParameter(String name) {
+ }
+
+ @Override
+ public SignatureVisitor visitClassBound() {
+ return this;
+ }
+
+ @Override
+ public SignatureVisitor visitInterfaceBound() {
+ return this;
+ }
+
+ @Override
+ public SignatureVisitor visitSuperclass() {
+ return this;
+ }
+
+ @Override
+ public SignatureVisitor visitInterface() {
+ return this;
+ }
+
+ @Override
+ public SignatureVisitor visitParameterType() {
+ return this;
+ }
+
+ @Override
+ public SignatureVisitor visitReturnType() {
+ return this;
+ }
+
+ @Override
+ public SignatureVisitor visitExceptionType() {
+ return this;
+ }
+
+ @Override
+ public void visitBaseType(char descriptor) {
+ }
+
+ @Override
+ public void visitTypeVariable(String name) {
+ }
+
+ @Override
+ public SignatureVisitor visitArrayType() {
+ return this;
+ }
+
+ @Override
+ public void visitInnerClassType(String name) {
+ }
+
+ @Override
+ public void visitTypeArgument() {
+ }
+
+ @Override
+ public SignatureVisitor visitTypeArgument(char wildcard) {
+ return this;
+ }
+
+ static void analyzeClass(String signature, AnalyzeClassVisitor analyzeClassVisitor) {
+ if (signature != null) {
+ new SignatureReader(signature).accept(new AnalyzeSignatureVisitor(analyzeClassVisitor));
+ }
+ }
+
+ static void analyzeMethod(String signature, AnalyzeClassVisitor analyzeClassVisitor) {
+ analyzeClass(signature, analyzeClassVisitor);
+ }
+
+ static void analyzeField(String signature, AnalyzeClassVisitor analyzeClassVisitor) {
+ if (signature != null)
+ new SignatureReader(signature).acceptType(new AnalyzeSignatureVisitor(analyzeClassVisitor));
+ }
+}
diff --git a/bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/ClassFileMetaData.java b/bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/ClassFileMetaData.java
new file mode 100644
index 00000000000..198618cabc4
--- /dev/null
+++ b/bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/ClassFileMetaData.java
@@ -0,0 +1,35 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.classanalysis;
+
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * The result of analyzing a .class file.
+ *
+ * @author Tony Vaagenes
+ * @author ollivir
+ */
+public class ClassFileMetaData {
+ private final String name;
+ private final Set<String> referencedClasses;
+ private final Optional<ExportPackageAnnotation> exportPackage;
+
+ public ClassFileMetaData(String name, Set<String> referencedClasses, Optional<ExportPackageAnnotation> exportPackage) {
+ this.name = name;
+ this.referencedClasses = referencedClasses;
+ this.exportPackage = exportPackage;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Set<String> getReferencedClasses() {
+ return referencedClasses;
+ }
+
+ public Optional<ExportPackageAnnotation> getExportPackage() {
+ return exportPackage;
+ }
+}
diff --git a/bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/ExportPackageAnnotation.java b/bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/ExportPackageAnnotation.java
new file mode 100644
index 00000000000..d2da9b5a226
--- /dev/null
+++ b/bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/ExportPackageAnnotation.java
@@ -0,0 +1,62 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.classanalysis;
+
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+/**
+ * @author Tony Vaagenes
+ * @author ollivir
+ */
+public class ExportPackageAnnotation {
+ private final int major;
+ private final int minor;
+ private final int micro;
+ private final String qualifier;
+
+ private static final Pattern QUALIFIER_PATTERN = Pattern.compile("[\\p{Alpha}\\p{Digit}_-]*");
+
+ public ExportPackageAnnotation(int major, int minor, int micro, String qualifier) {
+ this.major = major;
+ this.minor = minor;
+ this.micro = micro;
+ this.qualifier = qualifier;
+
+ requireNonNegative(major, "major");
+ requireNonNegative(minor, "minor");
+ requireNonNegative(micro, "micro");
+ if (QUALIFIER_PATTERN.matcher(qualifier).matches() == false) {
+ throw new IllegalArgumentException(
+ exportPackageError(String.format("qualifier must follow the format (alpha|digit|'_'|'-')* but was '%s'.", qualifier)));
+ }
+ }
+
+ public String osgiVersion() {
+ return String.format("%d.%d.%d", major, minor, micro) + (qualifier.isEmpty() ? "" : "." + qualifier);
+ }
+
+ private static String exportPackageError(String msg) {
+ return "ExportPackage anntotation: " + msg;
+ }
+
+ private static void requireNonNegative(int i, String fieldName) {
+ if (i < 0) {
+ throw new IllegalArgumentException(exportPackageError(String.format("%s must be non-negative but was %d.", fieldName, i)));
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ ExportPackageAnnotation that = (ExportPackageAnnotation) o;
+ return major == that.major && minor == that.minor && micro == that.micro && Objects.equals(qualifier, that.qualifier);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(major, minor, micro, qualifier);
+ }
+}
diff --git a/bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/ImportCollector.java b/bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/ImportCollector.java
new file mode 100644
index 00000000000..3946fe297f9
--- /dev/null
+++ b/bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/ImportCollector.java
@@ -0,0 +1,35 @@
+// Copyright 2018 Yahoo Holdings. 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;
+
+import java.util.Collection;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * @author ollivir
+ */
+public interface ImportCollector {
+ Set<String> imports();
+
+ default void addImportWithTypeDesc(String typeDescriptor) {
+ addImport(Type.getType(typeDescriptor));
+ }
+
+ default void addImport(Type type) {
+ addImport(Analyze.getClassName(type));
+ }
+
+ default void addImportWithInternalName(String name) {
+ addImport(Analyze.internalNameToClassName(name));
+ }
+
+ default void addImports(Collection<String> imports) {
+ imports().addAll(imports);
+ }
+
+ default void addImport(Optional<String> anImport) {
+ anImport.ifPresent(pkg -> imports().add(pkg));
+ }
+}
diff --git a/bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/PackageTally.java b/bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/PackageTally.java
new file mode 100644
index 00000000000..13bbc63192c
--- /dev/null
+++ b/bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/PackageTally.java
@@ -0,0 +1,79 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.classanalysis;
+
+import com.google.common.collect.Sets;
+import com.yahoo.container.plugin.util.Maps;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * @author Tony Vaagenes
+ * @author ollivir
+ */
+public class PackageTally {
+ private final Map<String, Optional<ExportPackageAnnotation>> definedPackagesMap;
+ private final Set<String> referencedPackagesUnfiltered;
+
+ public PackageTally(Map<String, Optional<ExportPackageAnnotation>> definedPackagesMap, Set<String> referencedPackagesUnfiltered) {
+ this.definedPackagesMap = definedPackagesMap;
+ this.referencedPackagesUnfiltered = referencedPackagesUnfiltered;
+ }
+
+ public Set<String> definedPackages() {
+ return definedPackagesMap.keySet();
+ }
+
+ public Set<String> referencedPackages() {
+ return Sets.difference(referencedPackagesUnfiltered, definedPackages());
+ }
+
+ public Map<String, ExportPackageAnnotation> exportedPackages() {
+ Map<String, ExportPackageAnnotation> ret = new HashMap<>();
+ definedPackagesMap.forEach((k, v) -> {
+ v.ifPresent(annotation -> ret.put(k, annotation));
+ });
+ return ret;
+ }
+
+ /**
+ * Represents the classes for two package tallies that are deployed as a single unit.
+ * <p>
+ * ExportPackageAnnotations from this has precedence over the other.
+ */
+ public PackageTally combine(PackageTally other) {
+ Map<String, Optional<ExportPackageAnnotation>> map = Maps.combine(this.definedPackagesMap, other.definedPackagesMap,
+ (l, r) -> l.isPresent() ? l : r);
+ Set<String> referencedPkgs = new HashSet<>(this.referencedPackagesUnfiltered);
+ referencedPkgs.addAll(other.referencedPackagesUnfiltered);
+
+ return new PackageTally(map, referencedPkgs);
+ }
+
+ public static PackageTally combine(Collection<PackageTally> packageTallies) {
+ Map<String, Optional<ExportPackageAnnotation>> map = new HashMap<>();
+ Set<String> referencedPkgs = new HashSet<>();
+
+ for (PackageTally pt : packageTallies) {
+ pt.definedPackagesMap.forEach((k, v) -> map.merge(k, v, (l, r) -> l.isPresent() ? l : r));
+ referencedPkgs.addAll(pt.referencedPackagesUnfiltered);
+ }
+ return new PackageTally(map, referencedPkgs);
+ }
+
+ public static PackageTally fromAnalyzedClassFiles(Collection<ClassFileMetaData> analyzedClassFiles) {
+ Map<String, Optional<ExportPackageAnnotation>> map = new HashMap<>();
+ Set<String> referencedPkgs = new HashSet<>();
+
+ for (ClassFileMetaData metaData : analyzedClassFiles) {
+ String packageName = Packages.packageName(metaData.getName());
+ map.merge(packageName, metaData.getExportPackage(), (l, r) -> l.isPresent() ? l : r);
+ metaData.getReferencedClasses().forEach(className -> referencedPkgs.add(Packages.packageName(className)));
+ }
+ return new PackageTally(map, referencedPkgs);
+ }
+}
diff --git a/bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/Packages.java b/bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/Packages.java
new file mode 100644
index 00000000000..f9c6503c475
--- /dev/null
+++ b/bundle-plugin/src/main/java/com/yahoo/container/plugin/classanalysis/Packages.java
@@ -0,0 +1,43 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.classanalysis;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Utility methods related to packages.
+ *
+ * @author Tony Vaagenes
+ * @author ollivir
+ */
+public class Packages {
+ public static class PackageMetaData {
+ public final Set<String> definedPackages;
+ public final Set<String> referencedExternalPackages;
+
+ public PackageMetaData(Set<String> definedPackages, Set<String> referencedExternalPackages) {
+ this.definedPackages = definedPackages;
+ this.referencedExternalPackages = referencedExternalPackages;
+ }
+ }
+
+ public static String packageName(String fullClassName) {
+ int index = fullClassName.lastIndexOf('.');
+ if (index == -1) {
+ return "";
+ } else {
+ return fullClassName.substring(0, index);
+ }
+ }
+
+ public static PackageMetaData analyzePackages(Set<ClassFileMetaData> allClasses) {
+ Set<String> definedPackages = new HashSet<>();
+ Set<String> referencedPackages = new HashSet<>();
+ for (ClassFileMetaData metaData : allClasses) {
+ definedPackages.add(packageName(metaData.getName()));
+ metaData.getReferencedClasses().forEach(className -> referencedPackages.add(packageName(className)));
+ }
+ referencedPackages.removeAll(definedPackages);
+ return new PackageMetaData(definedPackages, referencedPackages);
+ }
+}
diff --git a/bundle-plugin/src/main/java/com/yahoo/container/plugin/mojo/Artifacts.java b/bundle-plugin/src/main/java/com/yahoo/container/plugin/mojo/Artifacts.java
new file mode 100644
index 00000000000..fff88d413d0
--- /dev/null
+++ b/bundle-plugin/src/main/java/com/yahoo/container/plugin/mojo/Artifacts.java
@@ -0,0 +1,69 @@
+// Copyright 2018 Yahoo Holdings. 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.artifact.Artifact;
+import org.apache.maven.project.MavenProject;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * @author Tony Vaagenes
+ * @author ollivir
+ */
+public class Artifacts {
+ public static class ArtifactSet {
+ private final List<Artifact> jarArtifactsToInclude;
+ private final List<Artifact> jarArtifactsProvided;
+ private final List<Artifact> nonJarArtifacts;
+
+ private ArtifactSet(List<Artifact> jarArtifactsToInclude, List<Artifact> jarArtifactsProvided, List<Artifact> nonJarArtifacts) {
+ this.jarArtifactsToInclude = jarArtifactsToInclude;
+ this.jarArtifactsProvided = jarArtifactsProvided;
+ this.nonJarArtifacts = nonJarArtifacts;
+ }
+
+ public List<Artifact> getJarArtifactsToInclude() {
+ return jarArtifactsToInclude;
+ }
+
+ public List<Artifact> getJarArtifactsProvided() {
+ return jarArtifactsProvided;
+ }
+
+ public List<Artifact> getNonJarArtifacts() {
+ return nonJarArtifacts;
+ }
+ }
+
+ public static ArtifactSet getArtifacts(MavenProject project) {
+
+ List<Artifact> jarArtifactsToInclude = new ArrayList<>();
+ List<Artifact> jarArtifactsProvided = new ArrayList<>();
+ List<Artifact> nonJarArtifactsToInclude = new ArrayList<>();
+ List<Artifact> nonJarArtifactsProvided = new ArrayList<>();
+
+ for (Artifact artifact : project.getArtifacts()) {
+ if ("jar".equals(artifact.getType())) {
+ if (Artifact.SCOPE_COMPILE.equals(artifact.getScope())) {
+ jarArtifactsToInclude.add(artifact);
+ } else if (Artifact.SCOPE_PROVIDED.equals(artifact.getScope())) {
+ jarArtifactsProvided.add(artifact);
+ }
+ } else {
+ if (Artifact.SCOPE_COMPILE.equals(artifact.getScope())) {
+ nonJarArtifactsToInclude.add(artifact);
+ } else if (Artifact.SCOPE_PROVIDED.equals(artifact.getScope())) {
+ nonJarArtifactsProvided.add(artifact);
+ }
+ }
+ }
+ nonJarArtifactsToInclude.addAll(nonJarArtifactsProvided);
+ return new ArtifactSet(jarArtifactsToInclude, jarArtifactsProvided, nonJarArtifactsToInclude);
+ }
+
+ public static Collection<Artifact> getArtifactsToInclude(MavenProject project) {
+ return getArtifacts(project).getJarArtifactsToInclude();
+ }
+}
diff --git a/bundle-plugin/src/main/java/com/yahoo/container/plugin/mojo/AssembleContainerPluginMojo.java b/bundle-plugin/src/main/java/com/yahoo/container/plugin/mojo/AssembleContainerPluginMojo.java
new file mode 100644
index 00000000000..b5fac517c9d
--- /dev/null
+++ b/bundle-plugin/src/main/java/com/yahoo/container/plugin/mojo/AssembleContainerPluginMojo.java
@@ -0,0 +1,145 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.mojo;
+
+import com.yahoo.container.plugin.util.Files;
+import com.yahoo.container.plugin.util.JarFiles;
+import org.apache.maven.archiver.MavenArchiveConfiguration;
+import org.apache.maven.archiver.MavenArchiver;
+import org.apache.maven.execution.MavenSession;
+import org.apache.maven.model.Build;
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.plugins.annotations.ResolutionScope;
+import org.apache.maven.project.MavenProject;
+import org.codehaus.plexus.archiver.jar.JarArchiver;
+
+import java.io.File;
+import java.nio.channels.Channels;
+import java.util.EnumMap;
+import java.util.Enumeration;
+import java.util.Map;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.zip.ZipEntry;
+
+/**
+ * @author Tony Vaagenes
+ * @author ollivir
+ */
+@Mojo(name = "assemble-container-plugin", requiresDependencyResolution = ResolutionScope.COMPILE, threadSafe = true)
+public class AssembleContainerPluginMojo extends AbstractMojo {
+ private static enum Dependencies {
+ WITH, WITHOUT
+ }
+
+ @Parameter(defaultValue = "${project}")
+ private MavenProject project = null;
+
+ @Parameter(defaultValue = "${session}", readonly = true, required = true)
+ private MavenSession session = null;
+
+ @Parameter
+ private MavenArchiveConfiguration archiveConfiguration = new MavenArchiveConfiguration();
+
+ @Parameter(alias = "UseCommonAssemblyIds", defaultValue = "false")
+ private boolean useCommonAssemblyIds = false;
+
+ @Override
+ public void execute() throws MojoExecutionException {
+ Map<Dependencies, String> jarSuffixes = new EnumMap<Dependencies, String>(Dependencies.class);
+
+ if (useCommonAssemblyIds) {
+ jarSuffixes.put(Dependencies.WITHOUT, ".jar");
+ jarSuffixes.put(Dependencies.WITH, "-jar-with-dependencies.jar");
+ } else {
+ jarSuffixes.put(Dependencies.WITHOUT, "-without-dependencies.jar");
+ jarSuffixes.put(Dependencies.WITH, "-deploy.jar");
+ }
+
+ Map<Dependencies, File> jarFiles = new EnumMap<Dependencies, File>(Dependencies.class);
+ jarSuffixes.forEach((dep, suffix) -> {
+ jarFiles.put(dep, jarFileInBuildDirectory(build().getFinalName(), suffix));
+ });
+
+ // force recreating the archive
+ archiveConfiguration.setForced(true);
+ archiveConfiguration.setManifestFile(new File(new File(build().getOutputDirectory()), JarFile.MANIFEST_NAME));
+
+ JarArchiver jarWithoutDependencies = new JarArchiver();
+ addClassesDirectory(jarWithoutDependencies);
+ createArchive(jarFiles.get(Dependencies.WITHOUT), jarWithoutDependencies);
+ project.getArtifact().setFile(jarFiles.get(Dependencies.WITHOUT));
+
+ JarArchiver jarWithDependencies = new JarArchiver();
+ addClassesDirectory(jarWithDependencies);
+ addDependencies(jarWithDependencies);
+ createArchive(jarFiles.get(Dependencies.WITH), jarWithDependencies);
+ }
+
+ private File jarFileInBuildDirectory(String name, String suffix) {
+ return new File(build().getDirectory(), name + suffix);
+ }
+
+ private void addClassesDirectory(JarArchiver jarArchiver) {
+ File classesDirectory = new File(build().getOutputDirectory());
+ if (classesDirectory.isDirectory()) {
+ jarArchiver.addDirectory(classesDirectory);
+ }
+ }
+
+ private void createArchive(File jarFile, JarArchiver jarArchiver) throws MojoExecutionException {
+ MavenArchiver mavenArchiver = new MavenArchiver();
+ mavenArchiver.setArchiver(jarArchiver);
+ mavenArchiver.setOutputFile(jarFile);
+ try {
+ mavenArchiver.createArchive(session, project, archiveConfiguration);
+ } catch (Exception e) {
+ throw new MojoExecutionException("Error creating archive " + jarFile.getName(), e);
+ }
+ }
+
+ private void addDependencies(JarArchiver jarArchiver) {
+ Artifacts.getArtifactsToInclude(project).forEach(artifact -> {
+ if ("jar".equals(artifact.getType())) {
+ jarArchiver.addFile(artifact.getFile(), "dependencies/" + artifact.getFile().getName());
+ copyConfigDefinitions(artifact.getFile(), jarArchiver);
+ } else {
+ getLog().warn("Unkown artifact type " + artifact.getType());
+ }
+ });
+ }
+
+ private void copyConfigDefinitions(File file, JarArchiver jarArchiver) {
+ JarFiles.withJarFile(file, jarFile -> {
+ for (Enumeration<JarEntry> entries = jarFile.entries(); entries.hasMoreElements();) {
+ JarEntry entry = entries.nextElement();
+ String name = entry.getName();
+ if (name.startsWith("configdefinitions/") && name.endsWith(".def")) {
+ copyConfigDefinition(jarFile, entry, jarArchiver);
+ }
+ }
+ return null;
+ });
+ }
+
+ private void copyConfigDefinition(JarFile jarFile, ZipEntry entry, JarArchiver jarArchiver) {
+ JarFiles.withInputStream(jarFile, entry, input -> {
+ String defPath = entry.getName().replace("/", File.separator);
+ File destinationFile = new File(build().getOutputDirectory(), defPath);
+ destinationFile.getParentFile().mkdirs();
+
+ Files.withFileOutputStream(destinationFile, output -> {
+ output.getChannel().transferFrom(Channels.newChannel(input), 0, Long.MAX_VALUE);
+ return null;
+ });
+ jarArchiver.addFile(destinationFile, entry.getName());
+ return null;
+ });
+ }
+
+ private Build build() {
+ return project.getBuild();
+ }
+}
diff --git a/bundle-plugin/src/main/java/com/yahoo/container/plugin/mojo/GenerateBundleClassPathMappingsMojo.java b/bundle-plugin/src/main/java/com/yahoo/container/plugin/mojo/GenerateBundleClassPathMappingsMojo.java
new file mode 100644
index 00000000000..b2abf13695f
--- /dev/null
+++ b/bundle-plugin/src/main/java/com/yahoo/container/plugin/mojo/GenerateBundleClassPathMappingsMojo.java
@@ -0,0 +1,115 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.mojo;
+
+import com.google.common.base.Preconditions;
+import com.yahoo.container.plugin.bundle.AnalyzeBundle;
+import com.yahoo.container.plugin.osgi.ProjectBundleClassPaths;
+import com.yahoo.container.plugin.osgi.ProjectBundleClassPaths.BundleClasspathMapping;
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.plugins.annotations.ResolutionScope;
+import org.apache.maven.project.MavenProject;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Generates mapping from Bundle-SymbolicName to classpath elements, e.g myBundle -&gt; [.m2/repository/com/mylib/Mylib.jar,
+ * myBundleProject/target/classes] The mapping in stored in a json file.
+ *
+ * @author Tony Vaagenes
+ * @author ollivir
+ */
+@Mojo(name = "generate-bundle-classpath-mappings", requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, threadSafe = true)
+public class GenerateBundleClassPathMappingsMojo extends AbstractMojo {
+ @Parameter(defaultValue = "${project}")
+ private MavenProject project = null;
+
+ //TODO: Combine with com.yahoo.container.plugin.mojo.GenerateOsgiManifestMojo.bundleSymbolicName
+ @Parameter(alias = "Bundle-SymbolicName", defaultValue = "${project.artifactId}")
+ private String bundleSymbolicName = 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
+ public void execute() throws MojoExecutionException {
+ Preconditions.checkNotNull(bundleSymbolicName);
+
+ Artifacts.ArtifactSet artifacts = Artifacts.getArtifacts(project);
+ List<Artifact> embeddedArtifacts = artifacts.getJarArtifactsToInclude();
+ List<Artifact> providedJarArtifacts = artifacts.getJarArtifactsProvided();
+
+ List<File> embeddedArtifactsFiles = embeddedArtifacts.stream().map(Artifact::getFile).collect(Collectors.toList());
+
+ List<String> classPathElements = Stream.concat(Stream.of(outputDirectory()), embeddedArtifactsFiles.stream())
+ .map(File::getAbsolutePath).collect(Collectors.toList());
+
+ ProjectBundleClassPaths classPathMappings = new ProjectBundleClassPaths(
+ new BundleClasspathMapping(bundleSymbolicName, classPathElements),
+ providedJarArtifacts.stream().map(f -> createDependencyClasspathMapping(f)).filter(Optional::isPresent).map(Optional::get)
+ .collect(Collectors.toList()));
+
+ try {
+ ProjectBundleClassPaths.save(testOutputPath().resolve(ProjectBundleClassPaths.CLASSPATH_MAPPINGS_FILENAME), classPathMappings);
+ } catch (IOException e) {
+ throw new MojoExecutionException("Error saving to file " + testOutputPath(), e);
+ }
+ }
+
+ private File outputDirectory() {
+ return new File(project.getBuild().getOutputDirectory());
+ }
+
+ private Path testOutputPath() {
+ return 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.
+ */
+ Optional<BundleClasspathMapping> createDependencyClasspathMapping(Artifact artifact) {
+ return bundleSymbolicNameForArtifact(artifact)
+ .map(name -> new BundleClasspathMapping(name, Arrays.asList(artifact.getFile().getAbsolutePath())));
+ }
+
+ private static Optional<String> bundleSymbolicNameForArtifact(Artifact artifact) {
+ if (artifact.getFile().getName().endsWith(".jar")) {
+ return AnalyzeBundle.bundleSymbolicName(artifact.getFile());
+ } else {
+ // Not the best heuristic. The other alternatives are parsing the pom file or
+ // storing information in target/classes when building the provided bundles.
+ return Optional.of(artifact.getArtifactId());
+ }
+ }
+}
diff --git a/bundle-plugin/src/main/java/com/yahoo/container/plugin/mojo/GenerateOsgiManifestMojo.java b/bundle-plugin/src/main/java/com/yahoo/container/plugin/mojo/GenerateOsgiManifestMojo.java
new file mode 100644
index 00000000000..8d19f112765
--- /dev/null
+++ b/bundle-plugin/src/main/java/com/yahoo/container/plugin/mojo/GenerateOsgiManifestMojo.java
@@ -0,0 +1,313 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.mojo;
+
+import com.google.common.collect.Sets;
+import com.yahoo.container.plugin.bundle.AnalyzeBundle;
+import com.yahoo.container.plugin.classanalysis.Analyze;
+import com.yahoo.container.plugin.classanalysis.ClassFileMetaData;
+import com.yahoo.container.plugin.classanalysis.ExportPackageAnnotation;
+import com.yahoo.container.plugin.classanalysis.PackageTally;
+import com.yahoo.container.plugin.osgi.ExportPackageParser;
+import com.yahoo.container.plugin.osgi.ExportPackages;
+import com.yahoo.container.plugin.osgi.ExportPackages.Export;
+import com.yahoo.container.plugin.osgi.ImportPackages;
+import com.yahoo.container.plugin.osgi.ImportPackages.Import;
+import com.yahoo.container.plugin.util.Strings;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.plugins.annotations.ResolutionScope;
+import org.apache.maven.project.MavenProject;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.jar.Attributes;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static com.yahoo.container.plugin.util.Files.allDescendantFiles;
+import static com.yahoo.container.plugin.util.IO.withFileOutputStream;
+import static com.yahoo.container.plugin.util.JarFiles.withInputStream;
+import static com.yahoo.container.plugin.util.JarFiles.withJarFile;
+
+/**
+ * @author Tony Vaagenes
+ * @author ollivir
+ */
+@Mojo(name = "generate-osgi-manifest", requiresDependencyResolution = ResolutionScope.TEST, threadSafe = true)
+public class GenerateOsgiManifestMojo extends AbstractMojo {
+
+ @Parameter(defaultValue = "${project}")
+ private MavenProject project = null;
+
+ @Parameter
+ private String discApplicationClass = null;
+
+ @Parameter
+ private String discPreInstallBundle = null;
+
+ @Parameter(alias = "Bundle-Version", defaultValue = "${project.version}")
+ private String bundleVersion = null;
+
+ @Parameter(alias = "Bundle-SymbolicName", defaultValue = "${project.artifactId}")
+ private String bundleSymbolicName = null;
+
+ @Parameter(alias = "Bundle-Activator")
+ private String bundleActivator = null;
+
+ @Parameter(alias = "X-JDisc-Privileged-Activator")
+ private String jdiscPrivilegedActivator = null;
+
+ @Parameter(alias = "X-Config-Models")
+ private String configModels = null;
+
+ @Parameter(alias = "Import-Package")
+ private String importPackage = null;
+
+ @Parameter(alias = "WebInfUrl")
+ private String webInfUrl = null;
+
+ @Parameter(alias = "Main-Class")
+ private String mainClass = null;
+
+ @Parameter(alias = "X-Jersey-Binding")
+ private String jerseyBinding = null;
+
+ public void execute() throws MojoExecutionException, MojoFailureException {
+ try {
+ Artifacts.ArtifactSet artifactSet = Artifacts.getArtifacts(project);
+ warnOnUnsupportedArtifacts(artifactSet.getNonJarArtifacts());
+
+ AnalyzeBundle.PublicPackages publicPackagesFromProvidedJars = AnalyzeBundle.publicPackagesAggregated(
+ artifactSet.getJarArtifactsProvided().stream().map(Artifact::getFile).collect(Collectors.toList()));
+ PackageTally includedJarPackageTally = definedPackages(artifactSet.getJarArtifactsToInclude());
+
+ PackageTally projectPackageTally = analyzeProjectClasses();
+ PackageTally pluginPackageTally = projectPackageTally.combine(includedJarPackageTally);
+
+ Set<String> definedPackages = new HashSet<>(projectPackageTally.definedPackages());
+ definedPackages.addAll(includedJarPackageTally.definedPackages());
+
+ warnIfPackagesDefinedOverlapsGlobalPackages(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.stream()
+ .map(e -> "(" + e.getPackageNames().toString() + ", " + e.version().orElse("")).collect(Collectors.joining(", ")));
+ }
+
+ Map<String, Import> calculatedImports = ImportPackages.calculateImports(pluginPackageTally.referencedPackages(),
+ pluginPackageTally.definedPackages(), ExportPackages.exportsByPackageName(publicPackagesFromProvidedJars.exports));
+
+ Map<String, Optional<String>> manualImports = emptyToNone(importPackage).map(GenerateOsgiManifestMojo::getManualImports)
+ .orElseGet(HashMap::new);
+ for (String packageName : manualImports.keySet()) {
+ calculatedImports.remove(packageName);
+ }
+ createManifestFile(new File(project.getBuild().getOutputDirectory()), manifestContent(project,
+ artifactSet.getJarArtifactsToInclude(), manualImports, calculatedImports.values(), pluginPackageTally));
+
+ } catch (Exception e) {
+ throw new MojoExecutionException("Failed generating osgi manifest.", e);
+ }
+ }
+
+ private static void warnIfPackagesDefinedOverlapsGlobalPackages(Set<String> internalPackages, List<String> globalPackages)
+ throws MojoExecutionException {
+ Set<String> overlap = Sets.intersection(internalPackages, new HashSet<>(globalPackages));
+ if (overlap.isEmpty() == false) {
+ throw new MojoExecutionException(
+ "The following packages are both global and included in the bundle:\n " + String.join("\n ", overlap));
+ }
+ }
+
+ private Collection<String> osgiExportPackages(Map<String, ExportPackageAnnotation> exportedPackages) {
+ return exportedPackages.entrySet().stream().map(entry -> entry.getKey() + ";version=" + entry.getValue().osgiVersion())
+ .collect(Collectors.toList());
+ }
+
+ private static String trimWhitespace(Optional<String> lines) {
+ return Stream.of(lines.orElse("").split(",")).map(String::trim).collect(Collectors.joining(","));
+ }
+
+ private Map<String, String> manifestContent(MavenProject project, Collection<Artifact> jarArtifactsToInclude,
+ Map<String, Optional<String>> manualImports, Collection<Import> imports, PackageTally pluginPackageTally) {
+ Map<String, String> ret = new HashMap<>();
+ String importPackage = Stream.concat(manualImports.entrySet().stream().map(e -> asOsgiImport(e.getKey(), e.getValue())),
+ imports.stream().map(Import::asOsgiImport)).sorted().collect(Collectors.joining(","));
+ String exportPackage = osgiExportPackages(pluginPackageTally.exportedPackages()).stream().sorted().collect(Collectors.joining(","));
+
+ for (Pair<String, String> element : Arrays.asList(//
+ Pair.of("Created-By", "vespa container maven plugin"), //
+ Pair.of("Bundle-ManifestVersion", "2"), //
+ Pair.of("Bundle-Name", project.getName()), //
+ Pair.of("Bundle-SymbolicName", bundleSymbolicName), //
+ Pair.of("Bundle-Version", asBundleVersion(bundleVersion)), //
+ Pair.of("Bundle-Vendor", "Yahoo!"), //
+ Pair.of("Bundle-ClassPath", bundleClassPath(jarArtifactsToInclude)), //
+ Pair.of("Bundle-Activator", bundleActivator), //
+ Pair.of("X-JDisc-Privileged-Activator", jdiscPrivilegedActivator), //
+ Pair.of("Main-Class", mainClass), //
+ Pair.of("X-JDisc-Application", discApplicationClass), //
+ Pair.of("X-JDisc-Preinstall-Bundle", trimWhitespace(Optional.ofNullable(discPreInstallBundle))), //
+ Pair.of("X-Config-Models", configModels), //
+ Pair.of("X-Jersey-Binding", jerseyBinding), //
+ Pair.of("WebInfUrl", webInfUrl), //
+ Pair.of("Import-Package", importPackage), //
+ Pair.of("Export-Package", exportPackage))) {
+ if (element.getValue() != null && element.getValue().isEmpty() == false) {
+ ret.put(element.getKey(), element.getValue());
+ }
+ }
+ return ret;
+ }
+
+ private static String asOsgiImport(String packageName, Optional<String> version) {
+ return version.map(s -> packageName + ";version=" + quote(s)).orElse(packageName);
+ }
+
+ private static String quote(String s) {
+ return "\"" + s + "\"";
+ }
+
+ private static void createManifestFile(File outputDirectory, Map<String, String> manifestContent) {
+ Manifest manifest = toManifest(manifestContent);
+
+ withFileOutputStream(new File(outputDirectory, JarFile.MANIFEST_NAME), outputStream -> {
+ manifest.write(outputStream);
+ return null;
+ });
+ }
+
+ private static Manifest toManifest(Map<String, String> manifestContent) {
+ Manifest manifest = new Manifest();
+ Attributes mainAttributes = manifest.getMainAttributes();
+
+ mainAttributes.put(Attributes.Name.MANIFEST_VERSION, "1.0");
+ manifestContent.forEach(mainAttributes::putValue);
+
+ return manifest;
+ }
+
+ private static String bundleClassPath(Collection<Artifact> artifactsToInclude) {
+ return Stream.concat(Stream.of("."), artifactsToInclude.stream().map(GenerateOsgiManifestMojo::dependencyPath))
+ .collect(Collectors.joining(","));
+ }
+
+ private static String dependencyPath(Artifact artifact) {
+ return "dependencies/" + artifact.getFile().getName();
+ }
+
+ private static String asBundleVersion(String projectVersion) {
+ if (projectVersion == null) {
+ throw new IllegalArgumentException("Missing project version.");
+ }
+
+ String[] parts = projectVersion.split("-", 2);
+ List<String> numericPart = Stream.of(parts[0].split("\\.")).map(s -> Strings.replaceEmptyString(s, "0")).limit(3)
+ .collect(Collectors.toList());
+ while (numericPart.size() < 3) {
+ numericPart.add("0");
+ }
+
+ return String.join(".", numericPart);
+ }
+
+ private void warnOnUnsupportedArtifacts(Collection<Artifact> nonJarArtifacts) {
+ List<Artifact> unsupportedArtifacts = nonJarArtifacts.stream().filter(a -> "pom".equals(a.getType()) == false)
+ .collect(Collectors.toList());
+
+ unsupportedArtifacts.forEach(artifact -> getLog()
+ .warn(String.format("Unsupported artifact '%s': Type '%s' is not supported. Please file a feature request.",
+ artifact.getId(), artifact.getType())));
+ }
+
+ private PackageTally analyzeProjectClasses() {
+ File outputDirectory = new File(project.getBuild().getOutputDirectory());
+
+ List<ClassFileMetaData> analyzedClasses = allDescendantFiles(outputDirectory).filter(file -> file.getName().endsWith(".class"))
+ .map(Analyze::analyzeClass).collect(Collectors.toList());
+
+ return PackageTally.fromAnalyzedClassFiles(analyzedClasses);
+ }
+
+ private static PackageTally definedPackages(Collection<Artifact> jarArtifacts) {
+ return PackageTally.combine(jarArtifacts.stream().map(ja -> withJarFile(ja.getFile(), GenerateOsgiManifestMojo::definedPackages))
+ .collect(Collectors.toList()));
+ }
+
+ private static PackageTally definedPackages(JarFile jarFile) throws MojoExecutionException {
+ List<ClassFileMetaData> analyzedClasses = new ArrayList<>();
+ for (Enumeration<JarEntry> entries = jarFile.entries(); entries.hasMoreElements();) {
+ JarEntry entry = entries.nextElement();
+ if (entry.isDirectory() == false && entry.getName().endsWith(".class")) {
+ analyzedClasses.add(analyzeClass(jarFile, entry));
+ }
+ }
+ return PackageTally.fromAnalyzedClassFiles(analyzedClasses);
+ }
+
+ private static ClassFileMetaData analyzeClass(JarFile jarFile, JarEntry entry) throws MojoExecutionException {
+ try {
+ return withInputStream(jarFile, entry, Analyze::analyzeClass);
+ } catch (Exception e) {
+ throw new MojoExecutionException(
+ String.format("While analyzing the class '%s' in jar file '%s'", entry.getName(), jarFile.getName()), e);
+ }
+ }
+
+ private static Map<String, Optional<String>> getManualImports(String importPackage) {
+ try {
+ Map<String, Optional<String>> ret = new HashMap<>();
+ List<Export> imports = parseImportPackages(importPackage);
+ for (Export imp : imports) {
+ Optional<String> version = getVersionThrowOthers(imp.getParameters());
+ imp.getPackageNames().forEach(pn -> ret.put(pn, version));
+ }
+
+ return ret;
+ } catch (Exception e) {
+ throw new RuntimeException("Error in Import-Package:" + importPackage, e);
+ }
+ }
+
+ private static Optional<String> getVersionThrowOthers(List<ExportPackages.Parameter> parameters) {
+ if (parameters.size() == 1 && "version".equals(parameters.get(0).getName())) {
+ return Optional.of(parameters.get(0).getValue());
+ } else if (parameters.size() == 0) {
+ return Optional.empty();
+ } else {
+ List<String> paramNames = parameters.stream().map(ExportPackages.Parameter::getName).collect(Collectors.toList());
+ throw new RuntimeException("A single, optional version parameter expected, but got " + paramNames);
+ }
+ }
+
+ private static List<Export> parseImportPackages(String importPackages) {
+ return ExportPackageParser.parseExports(importPackages);
+ }
+
+ private static Optional<String> emptyToNone(String str) {
+ return Optional.ofNullable(str).map(String::trim).filter(s -> s.isEmpty() == false);
+ }
+
+ private static boolean isClassToAnalyze(String name) {
+ return name.endsWith(".class") && name.endsWith("module-info.class") == false;
+ }
+}
diff --git a/bundle-plugin/src/main/java/com/yahoo/container/plugin/osgi/ExportPackageParser.java b/bundle-plugin/src/main/java/com/yahoo/container/plugin/osgi/ExportPackageParser.java
new file mode 100644
index 00000000000..16858808a58
--- /dev/null
+++ b/bundle-plugin/src/main/java/com/yahoo/container/plugin/osgi/ExportPackageParser.java
@@ -0,0 +1,283 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.osgi;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @author Tony Vaagenes
+ * @author ollivir
+ */
+public class ExportPackageParser {
+ public static List<ExportPackages.Export> parseExports(String exportAttribute) {
+ ParsingContext p = new ParsingContext(exportAttribute.trim());
+
+ List<ExportPackages.Export> exports = parseExportPackage(p);
+ if (exports.isEmpty()) {
+ p.fail("Expected a list of exports");
+ } else if (p.atEnd() == false) {
+ p.fail("Exports not fully processed");
+ }
+ return exports;
+ }
+
+ private static class ParsingContext {
+ private enum State {
+ Invalid, WantMore, End
+ }
+
+ private CharSequence input;
+ private int pos;
+ private State state;
+ private int length;
+ private char ch;
+
+ private ParsingContext(CharSequence input) {
+ this.input = input;
+ this.pos = 0;
+ }
+
+ private Optional<String> read(Consumer<ParsingContext> rule) {
+ StringBuilder ret = new StringBuilder();
+
+ parse: while (true) {
+ if (input.length() < pos + 1) {
+ break;
+ }
+ ch = input.charAt(pos);
+ state = State.WantMore;
+ length = ret.length();
+ rule.accept(this);
+
+ switch (state) {
+ case Invalid:
+ if (ret.length() == 0) {
+ break parse;
+ } else {
+ String printable = Character.isISOControl(ch) ? "#" + Integer.toString((int) ch)
+ : "[" + Character.toString(ch) + "]";
+ pos++;
+ fail("Character " + printable + " was not acceptable");
+ }
+ break;
+ case WantMore:
+ ret.append(ch);
+ pos++;
+ break;
+ case End:
+ break parse;
+ }
+ }
+
+ if (ret.length() == 0) {
+ return Optional.empty();
+ } else {
+ return Optional.of(ret.toString());
+ }
+ }
+
+ private Optional<String> regexp(Pattern pattern) {
+ Matcher matcher = pattern.matcher(input);
+ matcher.region(pos, input.length());
+ if (matcher.lookingAt()) {
+ String value = matcher.group();
+ pos += value.length();
+ return Optional.of(value);
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ private Optional<String> exactly(String string) {
+ if (input.length() - pos < string.length()) {
+ return Optional.empty();
+ }
+ if (input.subSequence(pos, pos + string.length()).equals(string)) {
+ pos += string.length();
+ return Optional.of(string);
+ }
+ return Optional.empty();
+ }
+
+ private boolean atEnd() {
+ return pos == input.length();
+ }
+
+ private void invalid() {
+ this.state = State.Invalid;
+ }
+
+ private void end() {
+ this.state = State.End;
+ }
+
+ private void fail(String message) {
+ throw new RuntimeException("Failed parsing Export-Package: " + message + " at position " + pos);
+ }
+ }
+
+ /* ident = ? a valid Java identifier ? */
+ private static Optional<String> parseIdent(ParsingContext p) {
+ Optional<String> ident = p.read(ctx -> {
+ if (ctx.length == 0) {
+ if (Character.isJavaIdentifierStart(ctx.ch) == false) {
+ ctx.invalid();
+ }
+ } else {
+ if (Character.isJavaIdentifierPart(ctx.ch) == false) {
+ ctx.end();
+ }
+ }
+ });
+ return ident;
+ }
+
+ /* stringLiteral = ? sequence of any character except double quotes, control characters or backslash,
+ a backslash followed by another backslash, a single or double quote, or one of the letters b,f,n,r or t
+ a backslash followed by u followed by four hexadecimal digits ? */
+ private static Pattern STRING_LITERAL_PATTERN = Pattern
+ .compile("\"" + "(?:[^\"\\p{Cntrl}\\\\]|\\\\[\\\\'\"bfnrt]|\\\\u[0-9a-fA-F]{4})+" + "\"");
+
+ private static Optional<String> parseStringLiteral(ParsingContext p) {
+ return p.regexp(STRING_LITERAL_PATTERN).map(quoted -> quoted.substring(1, quoted.length() - 1));
+ }
+
+ /* extended = { \p{Alnum} | '_' | '-' | '.' }+ */
+ private static Pattern EXTENDED_PATTERN = Pattern.compile("[\\p{Alnum}_.-]+");
+
+ private static Optional<String> parseExtended(ParsingContext p) {
+ return p.regexp(EXTENDED_PATTERN);
+ }
+
+ /* argument = extended | stringLiteral | ? failure ? */
+ private static String parseArgument(ParsingContext p) {
+ Optional<String> argument = parseExtended(p);
+ if (argument.isPresent() == false) {
+ argument = parseStringLiteral(p);
+ }
+ if (argument.isPresent() == false) {
+ p.fail("Expected an extended token or a string literal");
+ }
+ return argument.get();
+ }
+
+ /*
+ * parameter = ( directive | attribute )
+ * directive = extended, ':=', argument
+ * attribute = extended, '=', argument
+ */
+ private static Pattern DIRECTIVE_OR_ATTRIBUTE_SEPARATOR_PATTERN = Pattern.compile("\\s*:?=\\s*");
+
+ private static Optional<ExportPackages.Parameter> parseParameter(ParsingContext p) {
+ int backtrack = p.pos;
+ Optional<String> ext = parseExtended(p);
+ if (ext.isPresent()) {
+ Optional<String> sep = p.regexp(DIRECTIVE_OR_ATTRIBUTE_SEPARATOR_PATTERN);
+ if (sep.isPresent() == false) {
+ p.pos = backtrack;
+ return Optional.empty();
+ }
+ String argument = parseArgument(p);
+ return Optional.of(new ExportPackages.Parameter(ext.get(), argument));
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ /* parameters = parameter, { ';' parameter } */
+ private static Pattern PARAMETER_SEPARATOR_PATTERN = Pattern.compile("\\s*;\\s*");
+
+ private static List<ExportPackages.Parameter> parseParameters(ParsingContext p) {
+ List<ExportPackages.Parameter> params = new ArrayList<>();
+ boolean wantMore = true;
+ do {
+ Optional<ExportPackages.Parameter> param = parseParameter(p);
+ if (param.isPresent()) {
+ params.add(param.get());
+ wantMore = p.regexp(PARAMETER_SEPARATOR_PATTERN).isPresent();
+ } else {
+ wantMore = false;
+ }
+ } while (wantMore);
+
+ return params;
+ }
+
+ /* packageName = ident, { '.', ident } */
+ private static Optional<String> parsePackageName(ParsingContext p) {
+ StringBuilder ret = new StringBuilder();
+
+ boolean wantMore = true;
+ do {
+ Optional<String> ident = parseIdent(p);
+ if (ident.isPresent()) {
+ ret.append(ident.get());
+ Optional<String> separator = p.exactly(".");
+ if (separator.isPresent()) {
+ ret.append(separator.get());
+ wantMore = true;
+ } else {
+ wantMore = false;
+ }
+ } else {
+ wantMore = false;
+ }
+ } while (wantMore);
+
+ if (ret.length() > 0) {
+ return Optional.of(ret.toString());
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ /* export = packageName, [ ';', ( parameters | export ) ] */
+ private static ExportPackages.Export parseExport(ParsingContext p) {
+ List<String> exports = new ArrayList<>();
+
+ boolean wantMore = true;
+ do {
+ if (exports.isEmpty() == false) { // second+ iteration
+ List<ExportPackages.Parameter> params = parseParameters(p);
+ if (params.isEmpty() == false) {
+ return new ExportPackages.Export(exports, params);
+ }
+ }
+
+ Optional<String> packageName = parsePackageName(p);
+ if (packageName.isPresent()) {
+ exports.add(packageName.get());
+ } else {
+ p.fail(exports.isEmpty() ? "Expected a package name" : "Expected either a package name or a parameter list");
+ }
+
+ wantMore = p.regexp(PARAMETER_SEPARATOR_PATTERN).isPresent();
+ } while (wantMore);
+
+ return new ExportPackages.Export(exports, new ArrayList<>());
+ }
+
+ /* exportPackage = export, { ',', export } */
+ private static Pattern EXPORT_SEPARATOR_PATTERN = Pattern.compile("\\s*,\\s*");
+
+ private static List<ExportPackages.Export> parseExportPackage(ParsingContext p) {
+ List<ExportPackages.Export> exports = new ArrayList<>();
+
+ boolean wantMore = true;
+ do {
+ ExportPackages.Export export = parseExport(p);
+ if (export.getPackageNames().isEmpty()) {
+ wantMore = false;
+ } else {
+ exports.add(export);
+ wantMore = p.regexp(EXPORT_SEPARATOR_PATTERN).isPresent();
+ }
+ } while (wantMore);
+
+ return exports;
+ }
+}
diff --git a/bundle-plugin/src/main/java/com/yahoo/container/plugin/osgi/ExportPackages.java b/bundle-plugin/src/main/java/com/yahoo/container/plugin/osgi/ExportPackages.java
new file mode 100644
index 00000000000..253e0727050
--- /dev/null
+++ b/bundle-plugin/src/main/java/com/yahoo/container/plugin/osgi/ExportPackages.java
@@ -0,0 +1,70 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.osgi;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * @author Tony Vaagenes
+ * @author ollivir
+ */
+public class ExportPackages {
+ public static class Export {
+ private final List<String> packageNames;
+ private final List<Parameter> parameters;
+
+ public Export(List<String> packageNames, List<Parameter> parameters) {
+ this.packageNames = packageNames;
+ this.parameters = parameters;
+ }
+
+ public Optional<String> version() {
+ for (Parameter par : parameters) {
+ if ("version".equals(par.getName())) {
+ return Optional.of(par.getValue());
+ }
+ }
+ return Optional.empty();
+ }
+
+ public List<String> getPackageNames() {
+ return packageNames;
+ }
+
+ public List<Parameter> getParameters() {
+ return parameters;
+ }
+ }
+
+ public static class Parameter {
+ private final String name;
+ private final String value;
+
+ public Parameter(String name, String value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getValue() {
+ return value;
+ }
+ }
+
+ public static Map<String, Export> exportsByPackageName(Collection<Export> exports) {
+ Map<String, Export> ret = new HashMap<>();
+ for (Export export : exports) {
+ for (String packageName : export.getPackageNames()) {
+ //ensure that earlier exports of a package overrides later exports.
+ ret.computeIfAbsent(packageName, ign -> export);
+ }
+ }
+ return ret;
+ }
+}
diff --git a/bundle-plugin/src/main/java/com/yahoo/container/plugin/osgi/ImportPackages.java b/bundle-plugin/src/main/java/com/yahoo/container/plugin/osgi/ImportPackages.java
new file mode 100644
index 00000000000..b58248ec4a6
--- /dev/null
+++ b/bundle-plugin/src/main/java/com/yahoo/container/plugin/osgi/ImportPackages.java
@@ -0,0 +1,97 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.osgi;
+
+import com.google.common.collect.Sets;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author Tony Vaagenes
+ * @author ollivir
+ */
+public class ImportPackages {
+ public static final int INFINITE_VERSION = 99999;
+ private static final String GUAVA_BASE_PACKAGE = "com.google.common";
+
+ public static class Import {
+ private final String packageName;
+ private final List<Integer> versionNumber;
+
+ public Import(String packageName, Optional<String> version) {
+ this.packageName = packageName;
+ this.versionNumber = new ArrayList<>();
+
+ if (version.isPresent()) {
+ try {
+ Arrays.stream(version.get().split("\\.")).map(Integer::parseInt).limit(3).forEach(this.versionNumber::add);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException(
+ String.format("Invalid version number '%s' for package '%s'.", version.get(), packageName), e);
+ }
+ }
+ }
+
+ public Optional<Integer> majorVersion() {
+ if (versionNumber.size() >= 1) {
+ return Optional.of(versionNumber.get(0));
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ public String packageName() {
+ return packageName;
+ }
+
+ public String version() {
+ return versionNumber.stream().map(Object::toString).collect(Collectors.joining("."));
+ }
+
+ // TODO: Detecting guava packages should be based on Bundle-SymbolicName, not package name.
+ public Optional<String> importVersionRange() {
+ if (versionNumber.isEmpty()) {
+ return Optional.empty();
+ } else {
+ int upperLimit = isGuavaPackage() ? INFINITE_VERSION // guava increases major version for each release
+ : versionNumber.get(0) + 1;
+ return Optional.of(String.format("[%s,%d)", version(), upperLimit));
+ }
+ }
+
+ public boolean isGuavaPackage() {
+ return packageName.equals(GUAVA_BASE_PACKAGE) || packageName.startsWith(GUAVA_BASE_PACKAGE + ".");
+ }
+
+ public String asOsgiImport() {
+ return packageName + importVersionRange().map(version -> ";version=\"" + version + '"').orElse("");
+ }
+ }
+
+ public static Map<String, Import> calculateImports(Set<String> referencedPackages, Set<String> implementedPackages,
+ Map<String, ExportPackages.Export> exportedPackages) {
+ Map<String, Import> ret = new HashMap<>();
+ for (String undefinedPackage : Sets.difference(referencedPackages, implementedPackages)) {
+ ExportPackages.Export export = exportedPackages.get(undefinedPackage);
+ if (export != null) {
+ ret.put(undefinedPackage, new Import(undefinedPackage, version(export)));
+ }
+ }
+ return ret;
+ }
+
+ private static Optional<String> version(ExportPackages.Export export) {
+ for (ExportPackages.Parameter param : export.getParameters()) {
+ if ("version".equals(param.getName())) {
+ return Optional.of(param.getValue());
+ }
+ }
+ return Optional.empty();
+ }
+}
diff --git a/bundle-plugin/src/main/java/com/yahoo/container/plugin/util/Files.java b/bundle-plugin/src/main/java/com/yahoo/container/plugin/util/Files.java
new file mode 100644
index 00000000000..bcd5d3768f3
--- /dev/null
+++ b/bundle-plugin/src/main/java/com/yahoo/container/plugin/util/Files.java
@@ -0,0 +1,30 @@
+// Copyright 2018 Yahoo Holdings. 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.File;
+import java.io.FileOutputStream;
+import java.util.stream.Stream;
+
+/**
+ * @author Tony Vaagenes
+ * @author ollivir
+ */
+public class Files {
+ public static Stream<File> allDescendantFiles(File file) {
+ if (file.isFile()) {
+ return Stream.of(file);
+ } else if (file.isDirectory()) {
+ return Stream.of(file.listFiles()).flatMap(Files::allDescendantFiles);
+ } else {
+ return Stream.empty();
+ }
+ }
+
+ public static <T> T withFileOutputStream(File file, ThrowingFunction<FileOutputStream, T> f) {
+ try (FileOutputStream fos = new FileOutputStream(file)) {
+ return f.apply(fos);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/bundle-plugin/src/main/java/com/yahoo/container/plugin/util/IO.java b/bundle-plugin/src/main/java/com/yahoo/container/plugin/util/IO.java
new file mode 100644
index 00000000000..a1e313b920b
--- /dev/null
+++ b/bundle-plugin/src/main/java/com/yahoo/container/plugin/util/IO.java
@@ -0,0 +1,41 @@
+// Copyright 2018 Yahoo Holdings. 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.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.OutputStream;
+
+/**
+ * Utility methods relating to IO
+ *
+ * @author Tony Vaagenes
+ * @author ollivir
+ */
+public class IO {
+ public static <T> T withFileInputStream(File file, ThrowingFunction<FileInputStream, T> f) {
+ try (FileInputStream fis = new FileInputStream(file)) {
+ return f.apply(fis);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Creates a new file and all its parent directories, and provides a file output stream to the file.
+ */
+ public static <T> T withFileOutputStream(File file, ThrowingFunction<OutputStream, T> f) {
+ makeDirectoriesRecursive(file.getParentFile());
+ try (FileOutputStream fos = new FileOutputStream(file)) {
+ return f.apply(fos);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static void makeDirectoriesRecursive(File file) {
+ if (!file.mkdirs() && !file.isDirectory()) {
+ throw new RuntimeException("Could not create directory " + file.getPath());
+ }
+ }
+}
diff --git a/bundle-plugin/src/main/java/com/yahoo/container/plugin/util/JarFiles.java b/bundle-plugin/src/main/java/com/yahoo/container/plugin/util/JarFiles.java
new file mode 100644
index 00000000000..398b2f5a72a
--- /dev/null
+++ b/bundle-plugin/src/main/java/com/yahoo/container/plugin/util/JarFiles.java
@@ -0,0 +1,36 @@
+// Copyright 2018 Yahoo Holdings. 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.File;
+import java.io.InputStream;
+import java.util.Optional;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/**
+ * @author Tony Vaagenes
+ * @author ollivir
+ */
+public class JarFiles {
+ public static <T> T withJarFile(File file, ThrowingFunction<JarFile, T> action) {
+ try (JarFile jar = new JarFile(file)) {
+ return action.apply(jar);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static <T> T withInputStream(ZipFile zipFile, ZipEntry zipEntry, ThrowingFunction<InputStream, T> action) {
+ try (InputStream is = zipFile.getInputStream(zipEntry)) {
+ return action.apply(is);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static Optional<Manifest> getManifest(File jarFile) {
+ return withJarFile(jarFile, jar -> Optional.ofNullable(jar.getManifest()));
+ }
+}
diff --git a/bundle-plugin/src/main/java/com/yahoo/container/plugin/util/Maps.java b/bundle-plugin/src/main/java/com/yahoo/container/plugin/util/Maps.java
new file mode 100644
index 00000000000..5aa14402d4e
--- /dev/null
+++ b/bundle-plugin/src/main/java/com/yahoo/container/plugin/util/Maps.java
@@ -0,0 +1,31 @@
+// Copyright 2018 Yahoo Holdings. 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.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BiFunction;
+
+/**
+ * @author Tony Vaagenes
+ * @author ollivir
+ */
+public class Maps {
+ public static <K, V> Map<K, V> combine(Map<K, V> left, Map<K, V> right, BiFunction<V, V, V> combiner) {
+ Map<K, V> ret = new HashMap<>();
+ Set<K> keysRight = new HashSet<>(right.keySet());
+
+ left.forEach((k, v) -> {
+ if (keysRight.contains(k)) {
+ ret.put(k, combiner.apply(v, right.get(k)));
+ keysRight.remove(k);
+ } else {
+ ret.put(k, v);
+ }
+ });
+ keysRight.forEach(k -> ret.put(k, right.get(k)));
+
+ return ret;
+ }
+}
diff --git a/bundle-plugin/src/main/java/com/yahoo/container/plugin/util/Strings.java b/bundle-plugin/src/main/java/com/yahoo/container/plugin/util/Strings.java
new file mode 100644
index 00000000000..15bdfb153ad
--- /dev/null
+++ b/bundle-plugin/src/main/java/com/yahoo/container/plugin/util/Strings.java
@@ -0,0 +1,26 @@
+// Copyright 2018 Yahoo Holdings. 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.Optional;
+
+/**
+ * @author Tony Vaagenes
+ * @author ollivir
+ */
+public class Strings {
+ public static String replaceEmptyString(String s, String replacement) {
+ if (s == null || s.isEmpty()) {
+ return replacement;
+ } else {
+ return s;
+ }
+ }
+
+ public static Optional<String> noneIfEmpty(String s) {
+ if (s == null || s.isEmpty()) {
+ return Optional.empty();
+ } else {
+ return Optional.of(s);
+ }
+ }
+} \ No newline at end of file
diff --git a/bundle-plugin/src/main/java/com/yahoo/container/plugin/util/ThrowingFunction.java b/bundle-plugin/src/main/java/com/yahoo/container/plugin/util/ThrowingFunction.java
new file mode 100644
index 00000000000..9ca64aabd73
--- /dev/null
+++ b/bundle-plugin/src/main/java/com/yahoo/container/plugin/util/ThrowingFunction.java
@@ -0,0 +1,11 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.plugin.util;
+
+/* Equivalent to java.util.function.Function, but allows throwing of Exceptions */
+
+/**
+ * @author ollivir
+ */
+public interface ThrowingFunction<T, U> {
+ U apply(T input) throws Exception;
+}