diff options
author | Olli Virtanen <olli.virtanen@oath.com> | 2018-06-13 13:38:26 +0200 |
---|---|---|
committer | Olli Virtanen <olli.virtanen@oath.com> | 2018-06-13 13:38:26 +0200 |
commit | d5836f186c269986cb9d86374b54b540bf600930 (patch) | |
tree | e95e2ec309835ef1d285630e87bae5d177e9934b /bundle-plugin/src/main/java/com | |
parent | eb80fb0d3a6004431ff13e36e9f480ccb32ec31f (diff) |
Bundle-plugin Scala code converted to Java
Diffstat (limited to 'bundle-plugin/src/main/java/com')
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 -> [.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; +} |