diff options
Diffstat (limited to 'container-jersey2/src/main/java/com/yahoo/container/servlet')
4 files changed, 319 insertions, 0 deletions
diff --git a/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/ComponentGraphProvider.java b/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/ComponentGraphProvider.java new file mode 100644 index 00000000000..7ff9646cb27 --- /dev/null +++ b/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/ComponentGraphProvider.java @@ -0,0 +1,73 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.servlet.jersey; + +import com.yahoo.container.di.config.ResolveDependencyException; +import com.yahoo.container.di.config.RestApiContext; +import com.yahoo.container.jaxrs.annotation.Component; +import org.glassfish.hk2.api.Injectee; +import org.glassfish.hk2.api.InjectionResolver; +import org.glassfish.hk2.api.ServiceHandle; + +import javax.inject.Singleton; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Resolves jdisc container components for jersey 2 components. + * + * @author Tony Vaagenes + * @author ollivir + */ +@Singleton // jersey2 requirement: InjectionResolvers must be in the Singleton scope +public class ComponentGraphProvider implements InjectionResolver<Component> { + private Collection<RestApiContext.Injectable> injectables; + + public ComponentGraphProvider(Collection<RestApiContext.Injectable> injectables) { + this.injectables = injectables; + } + + @Override + public Object resolve(Injectee injectee, ServiceHandle<?> root) { + Class<?> wantedClass; + Type type = injectee.getRequiredType(); + if (type instanceof Class) { + wantedClass = (Class<?>) type; + } else { + throw new UnsupportedOperationException("Only classes are supported, got " + type); + } + + List<RestApiContext.Injectable> componentsWithMatchingType = new ArrayList<>(); + for (RestApiContext.Injectable injectable : injectables) { + if (wantedClass.isInstance(injectable.instance)) { + componentsWithMatchingType.add(injectable); + } + } + + if (componentsWithMatchingType.size() == 1) { + return componentsWithMatchingType.get(0).instance; + } else { + String injectionDescription = "class '" + wantedClass + "' to inject into Jersey resource/provider '" + + injectee.getInjecteeClass() + "')"; + if (componentsWithMatchingType.size() > 1) { + String ids = componentsWithMatchingType.stream().map(c -> c.id.toString()).collect(Collectors.joining(",")); + throw new ResolveDependencyException("Multiple components found of " + injectionDescription + ": " + ids); + } else { + throw new ResolveDependencyException("Could not find a component of " + injectionDescription + "."); + } + } + } + + @Override + public boolean isMethodParameterIndicator() { + return true; + } + + @Override + public boolean isConstructorParameterIndicator() { + return true; + } +} diff --git a/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/JerseyApplication.java b/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/JerseyApplication.java new file mode 100644 index 00000000000..4c4e43bc8d5 --- /dev/null +++ b/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/JerseyApplication.java @@ -0,0 +1,25 @@ +// Copyright 2018 Yahoo Holdings. 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 java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +/** + * @author Tony Vaagenes + * @author ollivir + */ +public class JerseyApplication extends Application { + private Set<Class<?>> classes; + + public JerseyApplication(Collection<Class<?>> resourcesAndProviderClasses) { + this.classes = new HashSet<>(resourcesAndProviderClasses); + } + + @Override + public Set<Class<?>> getClasses() { + return classes; + } +} diff --git a/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/JerseyServletProvider.java b/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/JerseyServletProvider.java new file mode 100644 index 00000000000..1dbe410ba54 --- /dev/null +++ b/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/JerseyServletProvider.java @@ -0,0 +1,118 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.servlet.jersey; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +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 org.eclipse.jetty.servlet.ServletHolder; +import org.glassfish.hk2.api.InjectionResolver; +import org.glassfish.hk2.api.TypeLiteral; +import org.glassfish.hk2.utilities.Binder; +import org.glassfish.hk2.utilities.binding.AbstractBinder; +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 java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import static com.yahoo.container.servlet.jersey.util.ResourceConfigUtil.registerComponent; + +/** + * @author Tony Vaagenes + * @author ollivir + */ +public class JerseyServletProvider implements Provider<ServletHolder> { + private final ServletHolder jerseyServletHolder; + + public JerseyServletProvider(RestApiContext restApiContext) { + this.jerseyServletHolder = new ServletHolder(new ServletContainer(resourceConfig(restApiContext))); + } + + private ResourceConfig resourceConfig(RestApiContext restApiContext) { + final ResourceConfig resourceConfig = ResourceConfig + .forApplication(new JerseyApplication(resourcesAndProviders(restApiContext.getBundles()))); + + registerComponent(resourceConfig, componentInjectorBinder(restApiContext)); + registerComponent(resourceConfig, jacksonDatatypeJdk8Provider()); + resourceConfig.register(MultiPartFeature.class); + + return resourceConfig; + } + + private static Collection<Class<?>> resourcesAndProviders(Collection<BundleInfo> bundles) { + final List<Class<?>> ret = new ArrayList<>(); + + for (BundleInfo bundle : bundles) { + for (String classEntry : bundle.getClassEntries()) { + Optional<String> className = detectResourceOrProvider(bundle.classLoader, classEntry); + className.ifPresent(cname -> ret.add(loadClass(bundle.symbolicName, bundle.classLoader, cname))); + } + } + return ret; + } + + private static Optional<String> detectResourceOrProvider(ClassLoader bundleClassLoader, String classEntry) { + try (InputStream inputStream = getResourceAsStream(bundleClassLoader, classEntry)) { + ResourceOrProviderClassVisitor visitor = ResourceOrProviderClassVisitor.visit(new ClassReader(inputStream)); + return visitor.getJerseyClassName(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static InputStream getResourceAsStream(ClassLoader bundleClassLoader, String classEntry) { + InputStream is = bundleClassLoader.getResourceAsStream(classEntry); + if (is == null) { + throw new RuntimeException("No entry " + classEntry + " in bundle " + bundleClassLoader); + } else { + return is; + } + } + + private static Class<?> loadClass(String bundleSymbolicName, ClassLoader classLoader, String className) { + try { + return classLoader.loadClass(className); + } catch (Exception e) { + throw new RuntimeException("Failed loading class " + className + " from bundle " + bundleSymbolicName, e); + } + } + + private static Binder componentInjectorBinder(RestApiContext restApiContext) { + final ComponentGraphProvider componentGraphProvider = new ComponentGraphProvider(restApiContext.getInjectableComponents()); + final TypeLiteral<InjectionResolver<Component>> componentAnnotationType = new TypeLiteral<InjectionResolver<Component>>() { + }; + + return new AbstractBinder() { + @Override + public void configure() { + bind(componentGraphProvider).to(componentAnnotationType); + } + }; + } + + private static JacksonJaxbJsonProvider jacksonDatatypeJdk8Provider() { + JacksonJaxbJsonProvider provider = new JacksonJaxbJsonProvider(); + provider.setMapper(new ObjectMapper().registerModule(new Jdk8Module()).registerModule(new JavaTimeModule())); + return provider; + } + + @Override + public ServletHolder get() { + return jerseyServletHolder; + } + + @Override + public void deconstruct() { + } +} diff --git a/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/ResourceOrProviderClassVisitor.java b/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/ResourceOrProviderClassVisitor.java new file mode 100644 index 00000000000..7cb47ac6118 --- /dev/null +++ b/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/ResourceOrProviderClassVisitor.java @@ -0,0 +1,103 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.servlet.jersey; + +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; + +import javax.ws.rs.Path; +import javax.ws.rs.ext.Provider; + +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +/** + * @author Tony Vaagenes + * @author ollivir + */ +public class ResourceOrProviderClassVisitor extends ClassVisitor { + private String className = null; + private boolean isPublic = false; + private boolean isAbstract = false; + + private boolean isInnerClass = false; + private boolean isStatic = false; + + private boolean isAnnotated = false; + + public ResourceOrProviderClassVisitor() { + super(Opcodes.ASM6); + } + + public Optional<String> getJerseyClassName() { + if (isJerseyClass()) { + return Optional.of(getClassName()); + } else { + return Optional.empty(); + } + } + + public boolean isJerseyClass() { + return isAnnotated && isPublic && !isAbstract && (!isInnerClass || isStatic); + } + + public String getClassName() { + assert (className != null); + return org.objectweb.asm.Type.getObjectType(className).getClassName(); + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + isPublic = isPublic(access); + className = name; + isAbstract = isAbstract(access); + } + + @Override + public void visitInnerClass(String name, String outerName, String innerName, int access) { + assert (className != null); + + if (name.equals(className)) { + isInnerClass = true; + isStatic = isStatic(access); + } + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + isAnnotated |= annotationClassDescriptors.contains(desc); + return null; + } + + private static Set<String> annotationClassDescriptors = new HashSet<>(); + + static { + annotationClassDescriptors.add(Type.getDescriptor(Path.class)); + annotationClassDescriptors.add(Type.getDescriptor(Provider.class)); + } + + private static boolean isPublic(int access) { + return isSet(Opcodes.ACC_PUBLIC, access); + } + + private static boolean isStatic(int access) { + return isSet(Opcodes.ACC_STATIC, access); + } + + private static boolean isAbstract(int access) { + return isSet(Opcodes.ACC_ABSTRACT, access); + } + + private static boolean isSet(int bits, int access) { + return (access & bits) == bits; + } + + public static ResourceOrProviderClassVisitor visit(ClassReader classReader) { + ResourceOrProviderClassVisitor visitor = new ResourceOrProviderClassVisitor(); + classReader.accept(visitor, ClassReader.SKIP_DEBUG | ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES); + return visitor; + } +} |