diff options
author | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
---|---|---|
committer | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
commit | 72231250ed81e10d66bfe70701e64fa5fe50f712 (patch) | |
tree | 2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /container-jersey2/src/main |
Publish
Diffstat (limited to 'container-jersey2/src/main')
7 files changed, 279 insertions, 0 deletions
diff --git a/container-jersey2/src/main/java/com/yahoo/container/jaxrs/annotation/Component.java b/container-jersey2/src/main/java/com/yahoo/container/jaxrs/annotation/Component.java new file mode 100644 index 00000000000..0dc18350bb0 --- /dev/null +++ b/container-jersey2/src/main/java/com/yahoo/container/jaxrs/annotation/Component.java @@ -0,0 +1,17 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.jaxrs.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation for injecting jdisc container components into jaxrs resources and providers + * @author tonytv + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface Component {} + + diff --git a/container-jersey2/src/main/java/com/yahoo/container/jaxrs/annotation/package-info.java b/container-jersey2/src/main/java/com/yahoo/container/jaxrs/annotation/package-info.java new file mode 100644 index 00000000000..4adb642ddeb --- /dev/null +++ b/container-jersey2/src/main/java/com/yahoo/container/jaxrs/annotation/package-info.java @@ -0,0 +1,10 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +/** + * @author tonytv + */ +@PublicApi +@ExportPackage +package com.yahoo.container.jaxrs.annotation; + +import com.yahoo.api.annotations.PublicApi; +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/util/ResourceConfigUtil.java b/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/util/ResourceConfigUtil.java new file mode 100644 index 00000000000..e1246d0a125 --- /dev/null +++ b/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/util/ResourceConfigUtil.java @@ -0,0 +1,17 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.servlet.jersey.util; + +import org.glassfish.jersey.server.ResourceConfig; + +/** + * @author tonytv + */ +public class ResourceConfigUtil { + /** + * Solves ambiguous reference to overloaded definition, see + * http://stackoverflow.com/questions/3313929/how-do-i-disambiguate-in-scala-between-methods-with-vararg-and-without + */ + public static void registerComponent(ResourceConfig config, Object component) { + config.register(component); + } +} diff --git a/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/ComponentGraphProvider.scala b/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/ComponentGraphProvider.scala new file mode 100644 index 00000000000..8a18821ed7b --- /dev/null +++ b/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/ComponentGraphProvider.scala @@ -0,0 +1,40 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.servlet.jersey + +import javax.inject.Singleton + +import com.yahoo.container.di.config.{ResolveDependencyException, RestApiContext} +import com.yahoo.container.jaxrs.annotation.Component +import org.glassfish.hk2.api.{ServiceHandle, Injectee, InjectionResolver} + +/** + * Resolves jdisc container components for jersey 2 components. + * Similar to Gjoran's ComponentGraphProvider for jersey 1. + * @author tonytv + */ +@Singleton //jersey2 requirement: InjectionResolvers must be in the Singleton scope +class ComponentGraphProvider(injectables: Traversable[RestApiContext.Injectable]) extends InjectionResolver[Component] { + override def resolve(injectee: Injectee, root: ServiceHandle[_]): AnyRef = { + val wantedClass = injectee.getRequiredType match { + case c: Class[_] => c + case unsupported => throw new UnsupportedOperationException("Only classes are supported, got " + unsupported) + } + + val componentsWithMatchingType = injectables.filter{ injectable => + wantedClass.isInstance(injectable.instance) } + + val injectionDescription = + s"class '$wantedClass' to inject into Jersey resource/provider '${injectee.getInjecteeClass}')" + + if (componentsWithMatchingType.size > 1) + throw new ResolveDependencyException(s"Multiple components found of $injectionDescription: " + + componentsWithMatchingType.map(_.id).mkString(",")) + + componentsWithMatchingType.headOption.map(_.instance).getOrElse { + throw new ResolveDependencyException(s"Could not find a component of $injectionDescription.") + } + } + + override def isMethodParameterIndicator: Boolean = true + override def isConstructorParameterIndicator: Boolean = true +} diff --git a/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/JerseyApplication.scala b/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/JerseyApplication.scala new file mode 100644 index 00000000000..44e3fb2e767 --- /dev/null +++ b/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/JerseyApplication.scala @@ -0,0 +1,16 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.servlet.jersey + +import javax.ws.rs.core.Application + +import scala.collection.convert.wrapAsJava._ + +/** + * @author tonytv + */ +class JerseyApplication(resourcesAndProviderClasses: Set[Class[_]]) extends Application { + private val classes: java.util.Set[Class[_]] = resourcesAndProviderClasses + + override def getClasses = classes + override def getSingletons = super.getSingletons +} diff --git a/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/JerseyServletProvider.scala b/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/JerseyServletProvider.scala new file mode 100644 index 00000000000..30868f916a3 --- /dev/null +++ b/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/JerseyServletProvider.scala @@ -0,0 +1,105 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.servlet.jersey + +import java.io.{IOException, InputStream} + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module +import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider +import com.yahoo.container.di.componentgraph.Provider +import com.yahoo.container.di.config.RestApiContext +import com.yahoo.container.di.config.RestApiContext.BundleInfo +import com.yahoo.container.jaxrs.annotation.Component +import com.yahoo.container.servlet.jersey.util.ResourceConfigUtil.registerComponent +import org.eclipse.jetty.servlet.ServletHolder +import org.glassfish.hk2.api.{TypeLiteral, InjectionResolver} +import org.glassfish.hk2.utilities.binding.AbstractBinder +import org.glassfish.hk2.utilities.Binder +import org.glassfish.jersey.media.multipart.MultiPartFeature +import org.glassfish.jersey.server.ResourceConfig +import org.glassfish.jersey.servlet.ServletContainer +import org.objectweb.asm.ClassReader + +import scala.collection.convert.wrapAsScala._ +import scala.util.control.Exception + + +/** + * @author tonytv + */ +class JerseyServletProvider(restApiContext: RestApiContext) extends Provider[ServletHolder] { + private val jerseyServletHolder = new ServletHolder(new ServletContainer(resourceConfig(restApiContext))) + + private def resourceConfig(restApiContext: RestApiContext) = { + val resourceConfig = ResourceConfig.forApplication( + new JerseyApplication(resourcesAndProviders(restApiContext.getBundles))) + + registerComponent(resourceConfig, componentInjectorBinder(restApiContext)) + registerComponent(resourceConfig, jacksonDatatypeJdk8Provider) + resourceConfig.register(classOf[MultiPartFeature]) + + resourceConfig + } + + def resourcesAndProviders(bundles: Traversable[BundleInfo]) = + (for { + bundle <- bundles.view + classEntry <- bundle.getClassEntries + className <- detectResourceOrProvider(bundle.classLoader, classEntry) + } yield loadClass(bundle.symbolicName, bundle.classLoader, className)).toSet + + + def detectResourceOrProvider(bundleClassLoader: ClassLoader, classEntry: String): Option[String] = { + using(getResourceAsStream(bundleClassLoader, classEntry)) { inputStream => + val visitor = ResourceOrProviderClassVisitor.visit(new ClassReader(inputStream)) + visitor.getJerseyClassName + } + } + + private def getResourceAsStream(bundleClassLoader: ClassLoader, classEntry: String) = { + bundleClassLoader.getResourceAsStream(classEntry) match { + case null => throw new RuntimeException(s"No entry $classEntry in bundle $bundleClassLoader") + case stream => stream + } + + } + + def using[T <: InputStream, R](stream: T)(f: T => R): R = { + try { + f(stream) + } finally { + Exception.ignoring(classOf[IOException]) { + stream.close() + } + } + } + + def loadClass(bundleSymbolicName: String, classLoader: ClassLoader, className: String) = { + try { + classLoader.loadClass(className) + } catch { + case e: Exception => throw new RuntimeException(s"Failed loading class $className from bundle $bundleSymbolicName", e) + } + } + + def componentInjectorBinder(restApiContext: RestApiContext): Binder = { + val componentGraphProvider = new ComponentGraphProvider(restApiContext.getInjectableComponents.toTraversable) + val componentAnnotationType = new TypeLiteral[InjectionResolver[Component]] {} + + new AbstractBinder { + override def configure() { + bind(componentGraphProvider).to(componentAnnotationType) + } + } + } + + def jacksonDatatypeJdk8Provider: JacksonJaxbJsonProvider = { + val provider = new JacksonJaxbJsonProvider() + provider.setMapper(new ObjectMapper().registerModule(new Jdk8Module)) + provider + } + + override def get() = jerseyServletHolder + override def deconstruct() {} +} + diff --git a/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/ResourceOrProviderClassVisitor.scala b/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/ResourceOrProviderClassVisitor.scala new file mode 100644 index 00000000000..a8c1afe3d7b --- /dev/null +++ b/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/ResourceOrProviderClassVisitor.scala @@ -0,0 +1,74 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.servlet.jersey + +import javax.ws.rs.Path +import javax.ws.rs.ext.Provider + +import org.objectweb.asm.{ClassVisitor, Opcodes, Type, AnnotationVisitor, ClassReader} + + +/** + * @author tonytv + */ +class ResourceOrProviderClassVisitor private () extends ClassVisitor(Opcodes.ASM5) { + private var className: String = null + private var isPublic: Boolean = false + private var isAbstract = false + + private var isInnerClass: Boolean = false + private var isStatic: Boolean = false + + private var isAnnotated: Boolean = false + + def getJerseyClassName: Option[String] = { + if (isJerseyClass) Some(getClassName) + else None + } + + def isJerseyClass: Boolean = { + isAnnotated && isPublic && !isAbstract && + (!isInnerClass || isStatic) + } + + def getClassName = { + assert (className != null) + Type.getObjectType(className).getClassName + } + + override def visit(version: Int, access: Int, name: String, signature: String, superName: String, interfaces: Array[String]) { + isPublic = ResourceOrProviderClassVisitor.isPublic(access) + className = name + isAbstract = ResourceOrProviderClassVisitor.isAbstract(access) + } + + override def visitInnerClass(name: String, outerName: String, innerName: String, access: Int) { + assert (className != null) + + if (name == className) { + isInnerClass = true + isStatic = ResourceOrProviderClassVisitor.isStatic(access) + } + } + + override def visitAnnotation(desc: String, visible: Boolean): AnnotationVisitor = { + isAnnotated |= ResourceOrProviderClassVisitor.annotationClassDescriptors(desc) + null + } +} + + +object ResourceOrProviderClassVisitor { + val annotationClassDescriptors = Set(classOf[Path], classOf[Provider]) map Type.getDescriptor + + def isPublic = isSet(Opcodes.ACC_PUBLIC) _ + def isStatic = isSet(Opcodes.ACC_STATIC) _ + def isAbstract = isSet(Opcodes.ACC_ABSTRACT) _ + + private def isSet(bits: Int)(access: Int): Boolean = (access & bits) == bits + + def visit(classReader: ClassReader): ResourceOrProviderClassVisitor = { + val visitor = new ResourceOrProviderClassVisitor + classReader.accept(visitor, ClassReader.SKIP_DEBUG | ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES) + visitor + } +} |