diff options
24 files changed, 427 insertions, 982 deletions
diff --git a/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java b/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java index 4b0e30e04da..944a989d79f 100644 --- a/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java +++ b/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java @@ -89,7 +89,6 @@ public interface ModelContext { @ModelFeatureFlag(owners = {"baldersheim"}) default boolean dryRunOnnxOnSetup() { throw new UnsupportedOperationException("TODO specify default value"); } @ModelFeatureFlag(owners = {"geirst"}) default boolean enableFeedBlockInDistributor() { return true; } @ModelFeatureFlag(owners = {"hmusum"}, removeAfter = "7.428") default int clusterControllerMaxHeapSizeInMb() { return 128; } - @ModelFeatureFlag(owners = {"hmusum"}, removeAfter = "7.422") default int metricsProxyMaxHeapSizeInMb(ClusterSpec.Type type) { return 256; } @ModelFeatureFlag(owners = {"bjorncs", "tokle"}) default List<String> allowedAthenzProxyIdentities() { return List.of(); } @ModelFeatureFlag(owners = {"tokle"}) default boolean tenantIamRole() { return false; } @ModelFeatureFlag(owners = {"vekterli"}) default int maxActivationInhibitedOutOfSyncGroups() { return 0; } diff --git a/container-core/src/main/java/com/yahoo/container/core/config/HandlersConfigurerDi.java b/container-core/src/main/java/com/yahoo/container/core/config/HandlersConfigurerDi.java index 08a468a3031..1d63fb2312a 100644 --- a/container-core/src/main/java/com/yahoo/container/core/config/HandlersConfigurerDi.java +++ b/container-core/src/main/java/com/yahoo/container/core/config/HandlersConfigurerDi.java @@ -5,7 +5,6 @@ import com.google.inject.AbstractModule; import com.google.inject.Inject; import com.google.inject.Injector; import com.yahoo.component.AbstractComponent; -import com.yahoo.component.ComponentSpecification; import com.yahoo.component.provider.ComponentRegistry; import com.yahoo.concurrent.ThreadFactoryFactory; import com.yahoo.config.FileReference; @@ -13,8 +12,6 @@ import com.yahoo.container.di.ComponentDeconstructor; import com.yahoo.container.di.Container; import com.yahoo.container.di.componentgraph.core.ComponentGraph; import com.yahoo.container.di.config.SubscriberFactory; -import com.yahoo.container.di.osgi.BundleClasses; -import com.yahoo.container.di.osgi.OsgiUtil; import com.yahoo.container.logging.AccessLog; import com.yahoo.filedistribution.fileacquirer.FileAcquirer; import com.yahoo.jdisc.application.OsgiFramework; @@ -25,7 +22,6 @@ import com.yahoo.osgi.OsgiImpl; import com.yahoo.osgi.OsgiWrapper; import com.yahoo.statistics.Statistics; import org.osgi.framework.Bundle; -import org.osgi.framework.wiring.BundleWiring; import java.util.ArrayList; import java.util.Collection; @@ -35,8 +31,6 @@ import java.util.concurrent.Executors; import java.util.logging.Level; import java.util.logging.Logger; -import static com.yahoo.collections.CollectionUtil.first; - /** * For internal use only. * @@ -94,27 +88,6 @@ public class HandlersConfigurerDi { platformBundleLoader = new PlatformBundleLoader(this); } - - // TODO Vespa 8: Remove, only used for Jersey - @Override - public BundleClasses getBundleClasses(ComponentSpecification bundleSpec, Set<String> packagesToScan) { - //Temporary hack: Using class name since ClassLoaderOsgiFramework is not available at compile time in this bundle. - if (osgiFramework.getClass().getName().equals("com.yahoo.application.container.impl.ClassLoaderOsgiFramework")) { - Bundle syntheticClassPathBundle = first(osgiFramework.bundles()); - ClassLoader classLoader = syntheticClassPathBundle.adapt(BundleWiring.class).getClassLoader(); - - return new BundleClasses( - syntheticClassPathBundle, - OsgiUtil.getClassEntriesForBundleUsingProjectClassPathMappings(classLoader, bundleSpec, packagesToScan)); - } else { - Bundle bundle = getBundle(bundleSpec); - if (bundle == null) - throw new RuntimeException("No bundle matching '" + bundleSpec + "'"); - - return new BundleClasses(bundle, OsgiUtil.getClassEntriesInBundleClassPath(bundle, packagesToScan)); - } - } - @Override public void installPlatformBundles(Collection<String> bundlePaths) { // Don't install physical bundles for test frameworks, where all platform bundles are on the classpath. diff --git a/container-core/src/main/java/com/yahoo/container/di/Container.java b/container-core/src/main/java/com/yahoo/container/di/Container.java index a91aa6eb588..2bd8b2a90ec 100644 --- a/container-core/src/main/java/com/yahoo/container/di/Container.java +++ b/container-core/src/main/java/com/yahoo/container/di/Container.java @@ -12,11 +12,9 @@ import com.yahoo.container.di.ConfigRetriever.ComponentsConfigs; import com.yahoo.container.di.ConfigRetriever.ConfigSnapshot; import com.yahoo.container.di.componentgraph.core.ComponentGraph; import com.yahoo.container.di.componentgraph.core.ComponentNode; -import com.yahoo.container.di.componentgraph.core.JerseyNode; import com.yahoo.container.di.componentgraph.core.Node; import com.yahoo.container.di.config.ApplicationBundlesConfig; import com.yahoo.container.di.config.PlatformBundlesConfig; -import com.yahoo.container.di.config.RestApiContext; import com.yahoo.container.di.config.SubscriberFactory; import com.yahoo.vespa.config.ConfigKey; import org.osgi.framework.Bundle; @@ -200,14 +198,7 @@ public class Container { for (ComponentsConfig.Components config : componentsConfig.components()) { BundleInstantiationSpecification specification = bundleInstantiationSpecification(config); Class<?> componentClass = osgi.resolveClass(specification); - Node componentNode; - - if (RestApiContext.class.isAssignableFrom(componentClass)) { - Class<? extends RestApiContext> nodeClass = componentClass.asSubclass(RestApiContext.class); - componentNode = new JerseyNode(specification.id, config.configId(), nodeClass, osgi); - } else { - componentNode = new ComponentNode(specification.id, config.configId(), componentClass, null); - } + Node componentNode = new ComponentNode(specification.id, config.configId(), componentClass, null); graph.add(componentNode); } } diff --git a/container-core/src/main/java/com/yahoo/container/di/Osgi.java b/container-core/src/main/java/com/yahoo/container/di/Osgi.java index 940986e2f38..2ba93171081 100644 --- a/container-core/src/main/java/com/yahoo/container/di/Osgi.java +++ b/container-core/src/main/java/com/yahoo/container/di/Osgi.java @@ -5,11 +5,9 @@ import com.yahoo.component.ComponentSpecification; import com.yahoo.config.FileReference; import com.yahoo.container.bundle.BundleInstantiationSpecification; import com.yahoo.container.bundle.MockBundle; -import com.yahoo.container.di.osgi.BundleClasses; import org.osgi.framework.Bundle; import java.util.Collection; -import java.util.Collections; import java.util.Set; import java.util.stream.Collectors; @@ -21,9 +19,6 @@ import static java.util.Collections.emptySet; * @author ollivir */ public interface Osgi { - default BundleClasses getBundleClasses(ComponentSpecification bundle, Set<String> packagesToScan) { - return new BundleClasses(new MockBundle(), Collections.emptySet()); - } default void installPlatformBundles(Collection<String> bundlePaths) { System.out.println("installPlatformBundles " + bundlePaths); diff --git a/container-core/src/main/java/com/yahoo/container/di/componentgraph/core/JerseyNode.java b/container-core/src/main/java/com/yahoo/container/di/componentgraph/core/JerseyNode.java deleted file mode 100644 index 0f8aa678934..00000000000 --- a/container-core/src/main/java/com/yahoo/container/di/componentgraph/core/JerseyNode.java +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.di.componentgraph.core; - -import com.yahoo.component.ComponentId; -import com.yahoo.component.ComponentSpecification; -import com.yahoo.container.di.Osgi; -import com.yahoo.container.di.config.JerseyBundlesConfig; -import com.yahoo.container.di.config.RestApiContext; -import com.yahoo.container.di.config.RestApiContext.BundleInfo; -import com.yahoo.container.di.osgi.BundleClasses; -import org.osgi.framework.Bundle; -import org.osgi.framework.wiring.BundleWiring; - -import java.net.URL; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; - -/** - * Represents an instance of RestApiContext - * - * @author gjoranv - * @author Tony Vaagenes - * @author ollivir - */ -public class JerseyNode extends ComponentNode { - private static final String WEB_INF_URL = "WebInfUrl"; - - private final Osgi osgi; - - public JerseyNode(ComponentId componentId, String configId, Class<?> clazz, Osgi osgi) { - super(componentId, configId, clazz, null); - this.osgi = osgi; - } - - @Override - protected RestApiContext newInstance() { - Object instance = super.newInstance(); - RestApiContext restApiContext = (RestApiContext) instance; - - List<JerseyBundlesConfig.Bundles> bundles = restApiContext.bundlesConfig.bundles(); - for (JerseyBundlesConfig.Bundles bundleConfig : bundles) { - BundleClasses bundleClasses = osgi.getBundleClasses(ComponentSpecification.fromString(bundleConfig.spec()), - new HashSet<>(bundleConfig.packages())); - - restApiContext.addBundle(createBundleInfo(bundleClasses.bundle(), bundleClasses.classEntries())); - } - - componentsToInject.forEach(component -> restApiContext.addInjectableComponent(component.instanceKey(), component.componentId(), - component.component())); - - return restApiContext; - } - - @Override - public int hashCode() { - return super.hashCode(); - } - - @Override - public boolean equals(Object other) { - return super.equals(other) - && (other instanceof JerseyNode && this.componentsToInject.equals(((JerseyNode) other).componentsToInject)); - } - - public static BundleInfo createBundleInfo(Bundle bundle, Collection<String> classEntries) { - BundleInfo bundleInfo = new BundleInfo(bundle.getSymbolicName(), bundle.getVersion(), bundle.getLocation(), webInfUrl(bundle), - bundle.adapt(BundleWiring.class).getClassLoader()); - - bundleInfo.setClassEntries(classEntries); - return bundleInfo; - } - - public static Bundle getBundle(Osgi osgi, String bundleSpec) { - Bundle bundle = osgi.getBundle(ComponentSpecification.fromString(bundleSpec)); - if (bundle == null) { - throw new IllegalArgumentException("Bundle not found: " + bundleSpec); - } - return bundle; - } - - private static URL webInfUrl(Bundle bundle) { - String webInfUrlHeader = bundle.getHeaders().get(WEB_INF_URL); - - if (webInfUrlHeader == null) { - return null; - } else { - return bundle.getEntry(webInfUrlHeader); - } - } - -} diff --git a/container-core/src/main/java/com/yahoo/container/di/config/RestApiContext.java b/container-core/src/main/java/com/yahoo/container/di/config/RestApiContext.java deleted file mode 100644 index bfb9a8f9160..00000000000 --- a/container-core/src/main/java/com/yahoo/container/di/config/RestApiContext.java +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.di.config; - -import com.google.common.collect.ImmutableSet; -import com.google.inject.Inject; -import com.google.inject.Key; -import com.yahoo.component.ComponentId; -import org.osgi.framework.Version; - -import java.net.URL; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Set; - -/** - * Only for internal JDisc use. - * - * @author gjoranv - */ -public class RestApiContext { - - private final List<BundleInfo> bundles = new ArrayList<>(); - private final List<Injectable> injectableComponents = new ArrayList<>(); - - public final JerseyBundlesConfig bundlesConfig; - public final JerseyInjectionConfig injectionConfig; - - @Inject - public RestApiContext(JerseyBundlesConfig bundlesConfig, JerseyInjectionConfig injectionConfig) { - this.bundlesConfig = bundlesConfig; - this.injectionConfig = injectionConfig; - } - - public List<BundleInfo> getBundles() { - return Collections.unmodifiableList(bundles); - } - - public void addBundle(BundleInfo bundle) { - bundles.add(bundle); - } - - public List<Injectable> getInjectableComponents() { - return Collections.unmodifiableList(injectableComponents); - } - - public void addInjectableComponent(Key<?> key, ComponentId id, Object component) { - injectableComponents.add(new Injectable(key, id, component)); - } - - public static class Injectable { - public final Key<?> key; - public final ComponentId id; - public final Object instance; - - public Injectable(Key<?> key, ComponentId id, Object instance) { - this.key = key; - this.id = id; - this.instance = instance; - } - @Override - public String toString() { - return id.toString(); - } - } - - public static class BundleInfo { - public final String symbolicName; - public final Version version; - public final String fileLocation; - public final URL webInfUrl; - public final ClassLoader classLoader; - - private Set<String> classEntries; - - public BundleInfo(String symbolicName, Version version, String fileLocation, URL webInfUrl, ClassLoader classLoader) { - this.symbolicName = symbolicName; - this.version = version; - this.fileLocation = fileLocation; - this.webInfUrl = webInfUrl; - this.classLoader = classLoader; - } - - @Override - public String toString() { - return symbolicName + ":" + version; - } - - public void setClassEntries(Collection<String> entries) { - this.classEntries = ImmutableSet.copyOf(entries); - } - - public Set<String> getClassEntries() { - return classEntries; - } - } -} diff --git a/container-core/src/main/java/com/yahoo/container/di/osgi/BundleClasses.java b/container-core/src/main/java/com/yahoo/container/di/osgi/BundleClasses.java deleted file mode 100644 index bca3ed73d0b..00000000000 --- a/container-core/src/main/java/com/yahoo/container/di/osgi/BundleClasses.java +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.di.osgi; - -import org.osgi.framework.Bundle; - -import java.util.Collection; - -/** - * @author ollivir - */ -public class BundleClasses { - private final Bundle bundle; - private final Collection<String> classEntries; - - public BundleClasses(Bundle bundle, Collection<String> classEntries) { - this.bundle = bundle; - this.classEntries = classEntries; - } - - public Bundle bundle() { - return bundle; - } - - public Collection<String> classEntries() { - return classEntries; - } -} diff --git a/container-core/src/main/java/com/yahoo/container/di/osgi/OsgiUtil.java b/container-core/src/main/java/com/yahoo/container/di/osgi/OsgiUtil.java deleted file mode 100644 index e1854155e5b..00000000000 --- a/container-core/src/main/java/com/yahoo/container/di/osgi/OsgiUtil.java +++ /dev/null @@ -1,168 +0,0 @@ -// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.di.osgi; - -import com.yahoo.component.ComponentSpecification; -import com.yahoo.osgi.maven.ProjectBundleClassPaths; -import com.yahoo.osgi.maven.ProjectBundleClassPaths.BundleClasspathMapping; -import org.osgi.framework.Bundle; -import org.osgi.framework.wiring.BundleWiring; - -import java.io.File; -import java.io.IOException; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Set; -import java.util.function.Predicate; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; - -import static com.google.common.io.Files.fileTreeTraverser; - -/** - * Tested by com.yahoo.application.container.jersey.JerseyTest - * - * @author Tony Vaagenes - * @author ollivir - */ -public class OsgiUtil { - private static final Logger log = Logger.getLogger(OsgiUtil.class.getName()); - private static final String CLASS_FILE_TYPE_SUFFIX = ".class"; - - public static Collection<String> getClassEntriesInBundleClassPath(Bundle bundle, Set<String> packagesToScan) { - BundleWiring bundleWiring = bundle.adapt(BundleWiring.class); - - if (packagesToScan.isEmpty()) { - return bundleWiring.listResources("/", "*" + CLASS_FILE_TYPE_SUFFIX, - BundleWiring.LISTRESOURCES_LOCAL | BundleWiring.LISTRESOURCES_RECURSE); - } else { - List<String> ret = new ArrayList<>(); - for (String pkg : packagesToScan) { - ret.addAll(bundleWiring.listResources(packageToPath(pkg), "*" + CLASS_FILE_TYPE_SUFFIX, BundleWiring.LISTRESOURCES_LOCAL)); - } - return ret; - } - } - - public static Collection<String> getClassEntriesForBundleUsingProjectClassPathMappings(ClassLoader classLoader, - ComponentSpecification bundleSpec, Set<String> packagesToScan) { - return classEntriesFrom(bundleClassPathMapping(bundleSpec, classLoader).classPathElements, packagesToScan); - } - - private static BundleClasspathMapping bundleClassPathMapping(ComponentSpecification bundleSpec, ClassLoader classLoader) { - ProjectBundleClassPaths projectBundleClassPaths = loadProjectBundleClassPaths(classLoader); - - if (projectBundleClassPaths.mainBundle.bundleSymbolicName.equals(bundleSpec.getName())) { - return projectBundleClassPaths.mainBundle; - } else { - log.log(Level.WARNING, - "Dependencies of the bundle " + bundleSpec + " will not be scanned. Please file a feature request if you need this"); - return matchingBundleClassPathMapping(bundleSpec, projectBundleClassPaths.providedDependencies); - } - } - - public static BundleClasspathMapping matchingBundleClassPathMapping(ComponentSpecification bundleSpec, - Collection<BundleClasspathMapping> providedBundlesClassPathMappings) { - for (BundleClasspathMapping mapping : providedBundlesClassPathMappings) { - if (mapping.bundleSymbolicName.equals(bundleSpec.getName())) { - return mapping; - } - } - throw new RuntimeException("No such bundle: " + bundleSpec); - } - - private static ProjectBundleClassPaths loadProjectBundleClassPaths(ClassLoader classLoader) { - URL classPathMappingsFileLocation = classLoader.getResource(ProjectBundleClassPaths.CLASSPATH_MAPPINGS_FILENAME); - if (classPathMappingsFileLocation == null) { - throw new RuntimeException("Couldn't find " + ProjectBundleClassPaths.CLASSPATH_MAPPINGS_FILENAME + " in the class path."); - } - - try { - return ProjectBundleClassPaths.load(Paths.get(classPathMappingsFileLocation.toURI())); - } catch (IOException | URISyntaxException e) { - throw new RuntimeException(e); - } - } - - private static Collection<String> classEntriesFrom(List<String> classPathEntries, Set<String> packagesToScan) { - Set<String> packagePathsToScan = packagesToScan.stream().map(OsgiUtil::packageToPath).collect(Collectors.toSet()); - List<String> ret = new ArrayList<>(); - - for (String entry : classPathEntries) { - Path path = Paths.get(entry); - if (Files.isDirectory(path)) { - ret.addAll(classEntriesInPath(path, packagePathsToScan)); - } else if (Files.isRegularFile(path) && path.getFileName().toString().endsWith(".jar")) { - ret.addAll(classEntriesInJar(path, packagePathsToScan)); - } else { - throw new RuntimeException("Unsupported path " + path + " in the class path"); - } - } - return ret; - } - - private static String relativePathToClass(Path rootPath, Path pathToClass) { - Path relativePath = rootPath.relativize(pathToClass); - return relativePath.toString(); - } - - private static Collection<String> classEntriesInPath(Path rootPath, Collection<String> packagePathsToScan) { - Iterable<File> fileIterator; - if (packagePathsToScan.isEmpty()) { - fileIterator = fileTreeTraverser().preOrderTraversal(rootPath.toFile()); - } else { - List<File> files = new ArrayList<>(); - for (String packagePath : packagePathsToScan) { - for (File file : fileTreeTraverser().children(rootPath.resolve(packagePath).toFile())) { - files.add(file); - } - } - fileIterator = files; - } - - List<String> ret = new ArrayList<>(); - for (File file : fileIterator) { - if (file.isFile() && file.getName().endsWith(CLASS_FILE_TYPE_SUFFIX)) { - ret.add(relativePathToClass(rootPath, file.toPath())); - } - } - return ret; - } - - private static String packagePath(String name) { - int index = name.lastIndexOf('/'); - if (index < 0) { - return name; - } else { - return name.substring(0, index); - } - } - - private static Collection<String> classEntriesInJar(Path jarPath, Set<String> packagePathsToScan) { - Predicate<String> acceptedPackage; - if (packagePathsToScan.isEmpty()) { - acceptedPackage = ign -> true; - } else { - acceptedPackage = name -> packagePathsToScan.contains(packagePath(name)); - } - - try (JarFile jarFile = new JarFile(jarPath.toFile())) { - return jarFile.stream().map(JarEntry::getName).filter(name -> name.endsWith(CLASS_FILE_TYPE_SUFFIX)).filter(acceptedPackage) - .collect(Collectors.toList()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private static String packageToPath(String packageName) { - return packageName.replace('.', '/'); - } -} diff --git a/container-core/src/main/java/com/yahoo/container/di/osgi/package-info.java b/container-core/src/main/java/com/yahoo/container/di/osgi/package-info.java deleted file mode 100644 index 9685cf571bd..00000000000 --- a/container-core/src/main/java/com/yahoo/container/di/osgi/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -/** - * @author Tony Vaagenes - */ -@ExportPackage -package com.yahoo.container.di.osgi; - -import com.yahoo.osgi.annotation.ExportPackage; diff --git a/container-core/src/main/java/com/yahoo/restapi/RestApi.java b/container-core/src/main/java/com/yahoo/restapi/RestApi.java index 6f5bf298de3..2ef14679553 100644 --- a/container-core/src/main/java/com/yahoo/restapi/RestApi.java +++ b/container-core/src/main/java/com/yahoo/restapi/RestApi.java @@ -2,8 +2,10 @@ package com.yahoo.restapi; import com.fasterxml.jackson.databind.ObjectMapper; +import com.yahoo.container.jdisc.AclMapping; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.jdisc.RequestHandlerSpec; import java.io.InputStream; import java.util.List; @@ -20,38 +22,78 @@ public interface RestApi { static Builder builder() { return new RestApiImpl.BuilderImpl(); } static RouteBuilder route(String pathPattern) { return new RestApiImpl.RouteBuilderImpl(pathPattern); } + static HandlerConfigBuilder handlerConfig() { return new RestApiImpl.HandlerConfigBuilderImpl(); } HttpResponse handleRequest(HttpRequest request); ObjectMapper jacksonJsonMapper(); + /** @see com.yahoo.container.jdisc.HttpRequestHandler#requestHandlerSpec() */ + RequestHandlerSpec requestHandlerSpec(); + interface Builder { Builder setObjectMapper(ObjectMapper mapper); Builder setDefaultRoute(RouteBuilder route); Builder addRoute(RouteBuilder route); Builder addFilter(Filter filter); + /** see {@link RestApiMappers#DEFAULT_EXCEPTION_MAPPERS} for default mappers */ <EXCEPTION extends RuntimeException> Builder addExceptionMapper(Class<EXCEPTION> type, ExceptionMapper<EXCEPTION> mapper); + /** see {@link RestApiMappers#DEFAULT_RESPONSE_MAPPERS} for default mappers */ <RESPONSE_ENTITY> Builder addResponseMapper(Class<RESPONSE_ENTITY> type, ResponseMapper<RESPONSE_ENTITY> mapper); + /** see {@link RestApiMappers#DEFAULT_REQUEST_MAPPERS} for default mappers */ <REQUEST_ENTITY> Builder addRequestMapper(Class<REQUEST_ENTITY> type, RequestMapper<REQUEST_ENTITY> mapper); <RESPONSE_ENTITY> Builder registerJacksonResponseEntity(Class<RESPONSE_ENTITY> type); <REQUEST_ENTITY> Builder registerJacksonRequestEntity(Class<REQUEST_ENTITY> type); + /** Disables mappers listed in {@link RestApiMappers#DEFAULT_EXCEPTION_MAPPERS} */ Builder disableDefaultExceptionMappers(); + /** Disables mappers listed in {@link RestApiMappers#DEFAULT_RESPONSE_MAPPERS} */ Builder disableDefaultResponseMappers(); + Builder disableDefaultAclMapping(); RestApi build(); } interface RouteBuilder { RouteBuilder name(String name); + RouteBuilder addFilter(Filter filter); + + // GET RouteBuilder get(Handler<?> handler); + RouteBuilder get(Handler<?> handler, HandlerConfigBuilder config); + + // POST RouteBuilder post(Handler<?> handler); - <REQUEST_ENTITY> RouteBuilder post(Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler); + <REQUEST_ENTITY> RouteBuilder post( + Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler); + RouteBuilder post(Handler<?> handler, HandlerConfigBuilder config); + <REQUEST_ENTITY> RouteBuilder post( + Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler, HandlerConfigBuilder config); + + // PUT RouteBuilder put(Handler<?> handler); - <REQUEST_ENTITY> RouteBuilder put(Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler); + <REQUEST_ENTITY> RouteBuilder put( + Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler); + RouteBuilder put(Handler<?> handler, HandlerConfigBuilder config); + <REQUEST_ENTITY> RouteBuilder put( + Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler, HandlerConfigBuilder config); + + // DELETE RouteBuilder delete(Handler<?> handler); + RouteBuilder delete(Handler<?> handler, HandlerConfigBuilder config); + + // PATCH RouteBuilder patch(Handler<?> handler); - <REQUEST_ENTITY> RouteBuilder patch(Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler); + <REQUEST_ENTITY> RouteBuilder patch( + Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler); + RouteBuilder patch(Handler<?> handler, HandlerConfigBuilder config); + <REQUEST_ENTITY> RouteBuilder patch( + Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler, HandlerConfigBuilder config); + + // Default RouteBuilder defaultHandler(Handler<?> handler); - <REQUEST_ENTITY> RouteBuilder defaultHandler(Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler); - RouteBuilder addFilter(Filter filter); + RouteBuilder defaultHandler(Handler<?> handler, HandlerConfigBuilder config); + <REQUEST_ENTITY> RouteBuilder defaultHandler( + Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler); + <REQUEST_ENTITY> RouteBuilder defaultHandler( + Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler, HandlerConfigBuilder config); } @FunctionalInterface interface Handler<RESPONSE_ENTITY> { @@ -70,6 +112,12 @@ public interface RestApi { @FunctionalInterface interface Filter { HttpResponse filterRequest(FilterContext context); } + interface HandlerConfigBuilder { + HandlerConfigBuilder withReadAclAction(); + HandlerConfigBuilder withWriteAclAction(); + HandlerConfigBuilder withCustomAclAction(AclMapping.Action action); + } + interface RequestContext { HttpRequest request(); PathParameters pathParameters(); @@ -80,6 +128,7 @@ public interface RestApi { RequestContent requestContentOrThrow(); ObjectMapper jacksonJsonMapper(); UriBuilder uriBuilder(); + AclMapping.Action aclAction(); interface Parameters { Optional<String> getString(String name); diff --git a/container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java b/container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java index d63add5ed1d..646177e60db 100644 --- a/container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java +++ b/container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java @@ -1,20 +1,20 @@ // Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.restapi; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.yahoo.container.jdisc.AclMapping; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.jdisc.RequestHandlerSpec; +import com.yahoo.container.jdisc.RequestView; import com.yahoo.jdisc.http.HttpRequest.Method; -import com.yahoo.slime.Slime; -import com.yahoo.slime.SlimeUtils; -import com.yahoo.yolean.Exceptions; +import com.yahoo.restapi.RestApiMappers.ExceptionMapperHolder; +import com.yahoo.restapi.RestApiMappers.RequestMapperHolder; +import com.yahoo.restapi.RestApiMappers.ResponseMapperHolder; -import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.util.ArrayList; -import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.ListIterator; @@ -23,8 +23,6 @@ import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; -import static java.nio.charset.StandardCharsets.UTF_8; - /** * @author bjorncs */ @@ -39,6 +37,7 @@ class RestApiImpl implements RestApi { private final List<RequestMapperHolder<?>> requestMappers; private final List<Filter> filters; private final ObjectMapper jacksonJsonMapper; + private final boolean disableDefaultAclMapping; private RestApiImpl(RestApi.Builder builder) { BuilderImpl builderImpl = (BuilderImpl) builder; @@ -48,18 +47,19 @@ class RestApiImpl implements RestApi { this.exceptionMappers = combineWithDefaultExceptionMappers( builderImpl.exceptionMappers, Boolean.TRUE.equals(builderImpl.disableDefaultExceptionMappers)); this.responseMappers = combineWithDefaultResponseMappers( - builderImpl.responseMappers, jacksonJsonMapper, Boolean.TRUE.equals(builderImpl.disableDefaultResponseMappers)); - this.requestMappers = combineWithDefaultRequestMappers( - builderImpl.requestMappers, jacksonJsonMapper); + builderImpl.responseMappers, Boolean.TRUE.equals(builderImpl.disableDefaultResponseMappers)); + this.requestMappers = combineWithDefaultRequestMappers(builderImpl.requestMappers); this.filters = List.copyOf(builderImpl.filters); this.jacksonJsonMapper = jacksonJsonMapper; + this.disableDefaultAclMapping = Boolean.TRUE.equals(builderImpl.disableDefaultAclMapping); } @Override public HttpResponse handleRequest(HttpRequest request) { Path pathMatcher = new Path(request.getUri()); Route resolvedRoute = resolveRoute(pathMatcher); - RequestContextImpl requestContext = new RequestContextImpl(request, pathMatcher, jacksonJsonMapper); + AclMapping.Action aclAction = getAclMapping(request.getMethod(), request.getUri()); + RequestContextImpl requestContext = new RequestContextImpl(request, pathMatcher, aclAction, jacksonJsonMapper); FilterContextImpl filterContext = createFilterContextRecursive( resolvedRoute, requestContext, filters, @@ -73,8 +73,33 @@ class RestApiImpl implements RestApi { @Override public ObjectMapper jacksonJsonMapper() { return jacksonJsonMapper; } + @Override + public RequestHandlerSpec requestHandlerSpec() { + return RequestHandlerSpec.builder() + .withAclMapping(requestView -> getAclMapping(requestView.method(), requestView.uri())) + .build(); + } + + private AclMapping.Action getAclMapping(Method method, URI uri) { + Path pathMatcher = new Path(uri); + Route route = resolveRoute(pathMatcher); + HandlerHolder<?> handler = resolveHandler(method, route); + AclMapping.Action aclAction = handler.config.aclAction; + if (aclAction != null) return aclAction; + if (!disableDefaultAclMapping) { + // Fallback to default request handler spec which is used by the default implementation of + // HttpRequestHandler.requestHandlerSpec(). + return RequestHandlerSpec.DEFAULT_INSTANCE.aclMapping().get( + new RequestView() { + @Override public Method method() { return method; } + @Override public URI uri() { return uri; } + }); + } + throw new IllegalStateException(String.format("No ACL mapping configured for '%s' to '%s'", method, route.name)); + } + private HttpResponse dispatchToRoute(Route route, RequestContextImpl context) { - HandlerHolder<?> resolvedHandler = resolveHandler(context, route); + HandlerHolder<?> resolvedHandler = resolveHandler(context.request.getMethod(), route); RequestMapperHolder<?> resolvedRequestMapper = resolveRequestMapper(resolvedHandler); Object requestEntity; try { @@ -97,8 +122,8 @@ class RestApiImpl implements RestApi { } } - private HandlerHolder<?> resolveHandler(RequestContextImpl context, Route route) { - HandlerHolder<?> resolvedHandler = route.handlerPerMethod.get(context.request().getMethod()); + private HandlerHolder<?> resolveHandler(Method method, Route route) { + HandlerHolder<?> resolvedHandler = route.handlerPerMethod.get(method); return resolvedHandler == null ? route.defaultHandler : resolvedHandler; } @@ -154,7 +179,7 @@ class RestApiImpl implements RestApi { List<ExceptionMapperHolder<?>> configuredExceptionMappers, boolean disableDefaultMappers) { List<ExceptionMapperHolder<?>> exceptionMappers = new ArrayList<>(configuredExceptionMappers); if (!disableDefaultMappers){ - exceptionMappers.add(new ExceptionMapperHolder<>(RestApiException.class, (context, exception) -> exception.response())); + exceptionMappers.addAll(RestApiMappers.DEFAULT_EXCEPTION_MAPPERS); } // Topologically sort children before superclasses, so most the specific match is found by iterating through mappers in order. exceptionMappers.sort((a, b) -> (a.type.isAssignableFrom(b.type) ? 1 : 0) + (b.type.isAssignableFrom(a.type) ? -1 : 0)); @@ -162,71 +187,21 @@ class RestApiImpl implements RestApi { } private static List<ResponseMapperHolder<?>> combineWithDefaultResponseMappers( - List<ResponseMapperHolder<?>> configuredResponseMappers, ObjectMapper jacksonJsonMapper, boolean disableDefaultMappers) { + List<ResponseMapperHolder<?>> configuredResponseMappers, boolean disableDefaultMappers) { List<ResponseMapperHolder<?>> responseMappers = new ArrayList<>(configuredResponseMappers); if (!disableDefaultMappers) { - responseMappers.add(new ResponseMapperHolder<>(HttpResponse.class, (context, entity) -> entity)); - responseMappers.add(new ResponseMapperHolder<>(String.class, (context, entity) -> new MessageResponse(entity))); - responseMappers.add(new ResponseMapperHolder<>(Slime.class, (context, entity) -> new SlimeJsonResponse(entity))); - responseMappers.add(new ResponseMapperHolder<>(JsonNode.class, (context, entity) -> new JacksonJsonResponse<>(200, entity, jacksonJsonMapper, true))); + responseMappers.addAll(RestApiMappers.DEFAULT_RESPONSE_MAPPERS); } return responseMappers; } private static List<RequestMapperHolder<?>> combineWithDefaultRequestMappers( - List<RequestMapperHolder<?>> configuredRequestMappers, ObjectMapper jacksonJsonMapper) { + List<RequestMapperHolder<?>> configuredRequestMappers) { List<RequestMapperHolder<?>> requestMappers = new ArrayList<>(configuredRequestMappers); - requestMappers.add(new RequestMapperHolder<>(Slime.class, RestApiImpl::toSlime)); - requestMappers.add(new RequestMapperHolder<>(JsonNode.class, ctx -> toJsonNode(ctx, jacksonJsonMapper))); - requestMappers.add(new RequestMapperHolder<>(String.class, RestApiImpl::toString)); - requestMappers.add(new RequestMapperHolder<>(byte[].class, RestApiImpl::toByteArray)); - requestMappers.add(new RequestMapperHolder<>(InputStream.class, RestApiImpl::toInputStream)); - requestMappers.add(new RequestMapperHolder<>(Void.class, ctx -> Optional.empty())); + requestMappers.addAll(RestApiMappers.DEFAULT_REQUEST_MAPPERS); return requestMappers; } - private static Optional<InputStream> toInputStream(RequestContext context) { - return context.requestContent().map(RequestContext.RequestContent::content); - } - - private static Optional<byte[]> toByteArray(RequestContext context) { - InputStream in = toInputStream(context).orElse(null); - if (in == null) return Optional.empty(); - return convertIoException(() -> Optional.of(in.readAllBytes())); - } - - private static Optional<String> toString(RequestContext context) { - try { - return toByteArray(context).map(bytes -> new String(bytes, UTF_8)); - } catch (RuntimeException e) { - throw new RestApiException.BadRequest("Failed parse request content as UTF-8: " + Exceptions.toMessageString(e), e); - } - } - - private static Optional<JsonNode> toJsonNode(RequestContext context, ObjectMapper jacksonJsonMapper) { - if (log.isLoggable(Level.FINE)) { - return toString(context).map(string -> { - log.fine(() -> "Request content: " + string); - return convertIoException("Failed to parse JSON", () -> jacksonJsonMapper.readTree(string)); - }); - } else { - return toInputStream(context) - .map(in -> convertIoException("Invalid JSON", () -> jacksonJsonMapper.readTree(in))); - } - } - - private static Optional<Slime> toSlime(RequestContext context) { - try { - return toString(context).map(string -> { - log.fine(() -> "Request content: " + string); - return SlimeUtils.jsonToSlimeOrThrow(string); - }); - } catch (com.yahoo.slime.JsonParseException e) { - log.log(Level.FINE, e.getMessage(), e); - throw new RestApiException.BadRequest("Invalid JSON: " + Exceptions.toMessageString(e), e); - } - } - static class BuilderImpl implements RestApi.Builder { private final List<Route> routes = new ArrayList<>(); private final List<ExceptionMapperHolder<?>> exceptionMappers = new ArrayList<>(); @@ -237,6 +212,7 @@ class RestApiImpl implements RestApi { private ObjectMapper jacksonJsonMapper; private Boolean disableDefaultExceptionMappers; private Boolean disableDefaultResponseMappers; + private Boolean disableDefaultAclMapping; @Override public RestApi.Builder setObjectMapper(ObjectMapper mapper) { this.jacksonJsonMapper = mapper; return this; } @Override public RestApi.Builder setDefaultRoute(RestApi.RouteBuilder route) { this.defaultRoute = ((RouteBuilderImpl)route).build(); return this; } @@ -256,20 +232,21 @@ class RestApiImpl implements RestApi { } @Override public <ENTITY> Builder registerJacksonResponseEntity(Class<ENTITY> type) { - addResponseMapper(type, new JacksonResponseMapper<>()); return this; + addResponseMapper(type, new RestApiMappers.JacksonResponseMapper<>()); return this; } @Override public <ENTITY> Builder registerJacksonRequestEntity(Class<ENTITY> type) { - addRequestMapper(type, new JacksonRequestMapper<>(type)); return this; + addRequestMapper(type, new RestApiMappers.JacksonRequestMapper<>(type)); return this; } @Override public Builder disableDefaultExceptionMappers() { this.disableDefaultExceptionMappers = true; return this; } @Override public Builder disableDefaultResponseMappers() { this.disableDefaultResponseMappers = true; return this; } + @Override public Builder disableDefaultAclMapping() { this.disableDefaultAclMapping = true; return this; } @Override public RestApi build() { return new RestApiImpl(this); } } - public static class RouteBuilderImpl implements RestApi.RouteBuilder { + static class RouteBuilderImpl implements RestApi.RouteBuilder { private final String pathPattern; private String name; private final Map<Method, HandlerHolder<?>> handlerPerMethod = new HashMap<>(); @@ -279,50 +256,118 @@ class RestApiImpl implements RestApi { RouteBuilderImpl(String pathPattern) { this.pathPattern = pathPattern; } @Override public RestApi.RouteBuilder name(String name) { this.name = name; return this; } - @Override public RestApi.RouteBuilder get(Handler<?> handler) { - return addHandler(Method.GET, handler); + @Override public RestApi.RouteBuilder addFilter(RestApi.Filter filter) { filters.add(filter); return this; } + + // GET + @Override public RouteBuilder get(Handler<?> handler) { return get(handler, null); } + @Override public RouteBuilder get(Handler<?> handler, HandlerConfigBuilder config) { + return addHandler(Method.GET, handler, config); } - @Override public RestApi.RouteBuilder post(Handler<?> handler) { - return addHandler(Method.POST, handler); + + // POST + @Override public RouteBuilder post(Handler<?> handler) { return post(handler, null); } + @Override public <REQUEST_ENTITY> RouteBuilder post( + Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler) { + return post(type, handler, null); } - @Override public <ENTITY> RouteBuilder post(Class<ENTITY> type, HandlerWithRequestEntity<ENTITY, ?> handler) { - return addHandler(Method.POST, type, handler); + @Override public RouteBuilder post(Handler<?> handler, HandlerConfigBuilder config) { + return addHandler(Method.POST, handler, config); } - @Override public RestApi.RouteBuilder put(Handler<?> handler) { - return addHandler(Method.PUT, handler); + @Override public <REQUEST_ENTITY> RouteBuilder post( + Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler, HandlerConfigBuilder config) { + return addHandler(Method.POST, type, handler, config); } - @Override public <ENTITY> RouteBuilder put(Class<ENTITY> type, HandlerWithRequestEntity<ENTITY, ?> handler) { - return addHandler(Method.PUT, type, handler); + + // PUT + @Override public RouteBuilder put(Handler<?> handler) { return put(handler, null); } + @Override public <REQUEST_ENTITY> RouteBuilder put( + Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler) { + return put(type, handler, null); } - @Override public RestApi.RouteBuilder delete(Handler<?> handler) { - return addHandler(Method.DELETE, handler); + @Override public RouteBuilder put(Handler<?> handler, HandlerConfigBuilder config) { + return addHandler(Method.PUT, handler, null); } - @Override public RestApi.RouteBuilder patch(Handler<?> handler) { - return addHandler(Method.PATCH, handler); + @Override public <REQUEST_ENTITY> RouteBuilder put( + Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler, HandlerConfigBuilder config) { + return addHandler(Method.PUT, type, handler, config); } - @Override public <ENTITY> RouteBuilder patch(Class<ENTITY> type, HandlerWithRequestEntity<ENTITY, ?> handler) { - return addHandler(Method.PATCH, type, handler); + + // DELETE + @Override public RouteBuilder delete(Handler<?> handler) { return delete(handler, null); } + @Override public RouteBuilder delete(Handler<?> handler, HandlerConfigBuilder config) { + return addHandler(Method.DELETE, handler, config); } - @Override public RestApi.RouteBuilder defaultHandler(Handler<?> handler) { - defaultHandler = HandlerHolder.of(handler); return this; + + // PATCH + @Override public RouteBuilder patch(Handler<?> handler) { return patch(handler, null); } + @Override public <REQUEST_ENTITY> RouteBuilder patch( + Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler) { + return patch(type, handler, null); } - @Override public <ENTITY> RouteBuilder defaultHandler(Class<ENTITY> type, HandlerWithRequestEntity<ENTITY, ?> handler) { - defaultHandler = HandlerHolder.of(type, handler); return this; + @Override public RouteBuilder patch(Handler<?> handler, HandlerConfigBuilder config) { + return addHandler(Method.PATCH, handler, config); + } + @Override public <REQUEST_ENTITY> RouteBuilder patch( + Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler, HandlerConfigBuilder config) { + return addHandler(Method.PATCH, type, handler, config); } - @Override public RestApi.RouteBuilder addFilter(RestApi.Filter filter) { filters.add(filter); return this; } - private RestApi.RouteBuilder addHandler(Method method, Handler<?> handler) { - handlerPerMethod.put(method, HandlerHolder.of(handler)); return this; + // Default + @Override public RouteBuilder defaultHandler(Handler<?> handler) { + return defaultHandler(handler, null); + } + @Override public RouteBuilder defaultHandler(Handler<?> handler, HandlerConfigBuilder config) { + defaultHandler = HandlerHolder.of(handler, build(config)); return this; + } + @Override public <REQUEST_ENTITY> RouteBuilder defaultHandler( + Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler) { + return defaultHandler(type, handler, null); + } + @Override + public <REQUEST_ENTITY> RouteBuilder defaultHandler( + Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler, HandlerConfigBuilder config) { + defaultHandler = HandlerHolder.of(type, handler, build(config)); return this; + } + + private RestApi.RouteBuilder addHandler(Method method, Handler<?> handler, HandlerConfigBuilder config) { + handlerPerMethod.put(method, HandlerHolder.of(handler, build(config))); return this; } private <ENTITY> RestApi.RouteBuilder addHandler( - Method method, Class<ENTITY> type, HandlerWithRequestEntity<ENTITY, ?> handler) { - handlerPerMethod.put(method, HandlerHolder.of(type, handler)); return this; + Method method, Class<ENTITY> type, HandlerWithRequestEntity<ENTITY, ?> handler, HandlerConfigBuilder config) { + handlerPerMethod.put(method, HandlerHolder.of(type, handler, build(config))); return this; + } + + private static HandlerConfig build(HandlerConfigBuilder builder) { + if (builder == null) return HandlerConfig.empty(); + return ((HandlerConfigBuilderImpl)builder).build(); } private Route build() { return new Route(this); } } + static class HandlerConfigBuilderImpl implements HandlerConfigBuilder { + private AclMapping.Action aclAction; + + @Override public HandlerConfigBuilder withReadAclAction() { return withCustomAclAction(AclMapping.Action.READ); } + @Override public HandlerConfigBuilder withWriteAclAction() { return withCustomAclAction(AclMapping.Action.WRITE); } + @Override public HandlerConfigBuilder withCustomAclAction(AclMapping.Action action) { + this.aclAction = action; return this; + } + + HandlerConfig build() { return new HandlerConfig(this); } + } + + private static class HandlerConfig { + final AclMapping.Action aclAction; + + HandlerConfig(HandlerConfigBuilderImpl builder) { + this.aclAction = builder.aclAction; + } + + static HandlerConfig empty() { return new HandlerConfigBuilderImpl().build(); } + } + private static class RequestContextImpl implements RestApi.RequestContext { final HttpRequest request; final Path pathMatcher; @@ -332,12 +377,14 @@ class RestApiImpl implements RestApi { final Headers headers = new HeadersImpl(); final Attributes attributes = new AttributesImpl(); final RequestContent requestContent; + final AclMapping.Action aclAction; - RequestContextImpl(HttpRequest request, Path pathMatcher, ObjectMapper jacksonJsonMapper) { + RequestContextImpl(HttpRequest request, Path pathMatcher, AclMapping.Action aclAction, ObjectMapper jacksonJsonMapper) { this.request = request; this.pathMatcher = pathMatcher; this.jacksonJsonMapper = jacksonJsonMapper; this.requestContent = request.getData() != null ? new RequestContentImpl() : null; + this.aclAction = aclAction; } @Override public HttpRequest request() { return request; } @@ -357,6 +404,7 @@ class RestApiImpl implements RestApi { ? new UriBuilder(uri.getScheme() + "://" + uri.getHost() + ':' + uriPort) : new UriBuilder(uri.getScheme() + "://" + uri.getHost()); } + @Override public AclMapping.Action aclAction() { return aclAction; } private class PathParametersImpl implements RestApi.RequestContext.PathParameters { @Override @@ -433,63 +481,37 @@ class RestApiImpl implements RestApi { } } - private static class ExceptionMapperHolder<EXCEPTION extends RuntimeException> { - final Class<EXCEPTION> type; - final RestApi.ExceptionMapper<EXCEPTION> mapper; - - ExceptionMapperHolder(Class<EXCEPTION> type, RestApi.ExceptionMapper<EXCEPTION> mapper) { - this.type = type; - this.mapper = mapper; - } - - HttpResponse toResponse(RestApi.RequestContext context, RuntimeException e) { return mapper.toResponse(context, type.cast(e)); } - } - - private static class ResponseMapperHolder<ENTITY> { - final Class<ENTITY> type; - final RestApi.ResponseMapper<ENTITY> mapper; - - ResponseMapperHolder(Class<ENTITY> type, RestApi.ResponseMapper<ENTITY> mapper) { - this.type = type; - this.mapper = mapper; - } - - HttpResponse toHttpResponse(RestApi.RequestContext context, Object entity) { return mapper.toHttpResponse(context, type.cast(entity)); } - } - private static class HandlerHolder<REQUEST_ENTITY> { final Class<REQUEST_ENTITY> type; final HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler; + final HandlerConfig config; - HandlerHolder(Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler) { + private HandlerHolder( + Class<REQUEST_ENTITY> type, + HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler, + HandlerConfig config) { this.type = type; this.handler = handler; + this.config = config; } static <RESPONSE_ENTITY, REQUEST_ENTITY> HandlerHolder<REQUEST_ENTITY> of( - Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, RESPONSE_ENTITY> handler) { - return new HandlerHolder<>(type, handler); + Class<REQUEST_ENTITY> type, + HandlerWithRequestEntity<REQUEST_ENTITY, RESPONSE_ENTITY> handler, + HandlerConfig config) { + return new HandlerHolder<>(type, handler, config); } - static <RESPONSE_ENTITY> HandlerHolder<Void> of(Handler<RESPONSE_ENTITY> handler) { + static <RESPONSE_ENTITY> HandlerHolder<Void> of(Handler<RESPONSE_ENTITY> handler, HandlerConfig config) { return new HandlerHolder<>( Void.class, - (HandlerWithRequestEntity<Void, RESPONSE_ENTITY>) (context, nullEntity) -> handler.handleRequest(context)); + (HandlerWithRequestEntity<Void, RESPONSE_ENTITY>) (context, nullEntity) -> handler.handleRequest(context), + config); } Object executeHandler(RestApi.RequestContext context, Object entity) { return handler.handleRequest(context, type.cast(entity)); } } - private static class RequestMapperHolder<ENTITY> { - final Class<ENTITY> type; - final RestApi.RequestMapper<ENTITY> mapper; - - RequestMapperHolder(Class<ENTITY> type, RequestMapper<ENTITY> mapper) { - this.type = type; - this.mapper = mapper; - } - } - static class Route { private final String pathPattern; private final String name; @@ -507,47 +529,10 @@ class RestApiImpl implements RestApi { } private HandlerHolder<?> createDefaultMethodHandler() { - return HandlerHolder.of(context -> { throw new RestApiException.MethodNotAllowed(context.request()); }); + return HandlerHolder.of( + context -> { throw new RestApiException.MethodNotAllowed(context.request()); }, + HandlerConfig.empty()); } } - private static class JacksonRequestMapper<ENTITY> implements RequestMapper<ENTITY> { - private final Class<ENTITY> type; - - JacksonRequestMapper(Class<ENTITY> type) { this.type = type; } - - @Override - public Optional<ENTITY> toRequestEntity(RequestContext context) throws RestApiException { - if (log.isLoggable(Level.FINE)) { - return RestApiImpl.toString(context).map(string -> { - log.fine(() -> "Request content: " + string); - return convertIoException("Failed to parse JSON", () -> context.jacksonJsonMapper().readValue(string, type)); - }); - } else { - return RestApiImpl.toInputStream(context) - .map(in -> convertIoException("Invalid JSON", () -> context.jacksonJsonMapper().readValue(in, type))); - } - } - } - - private static class JacksonResponseMapper<ENTITY> implements ResponseMapper<ENTITY> { - @Override - public HttpResponse toHttpResponse(RequestContext context, ENTITY responseEntity) throws RestApiException { - return new JacksonJsonResponse<>(200, responseEntity, context.jacksonJsonMapper(), true); - } - } - - @FunctionalInterface private interface SupplierThrowingIoException<T> { T get() throws IOException; } - private static <T> T convertIoException(String messagePrefix, SupplierThrowingIoException<T> supplier) { - try { - return supplier.get(); - } catch (IOException e) { - log.log(Level.FINE, e.getMessage(), e); - throw new RestApiException.InternalServerError(messagePrefix + ": " + Exceptions.toMessageString(e), e); - } - } - - private static <T> T convertIoException(SupplierThrowingIoException<T> supplier) { - return convertIoException("Failed to read request content", supplier); - } } diff --git a/container-core/src/main/java/com/yahoo/restapi/RestApiMappers.java b/container-core/src/main/java/com/yahoo/restapi/RestApiMappers.java new file mode 100644 index 00000000000..36d98421e6a --- /dev/null +++ b/container-core/src/main/java/com/yahoo/restapi/RestApiMappers.java @@ -0,0 +1,172 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.restapi; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.restapi.RestApi.ExceptionMapper; +import com.yahoo.restapi.RestApi.RequestMapper; +import com.yahoo.restapi.RestApi.ResponseMapper; +import com.yahoo.slime.Slime; +import com.yahoo.slime.SlimeUtils; +import com.yahoo.yolean.Exceptions; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * Implementations of {@link ExceptionMapper}, {@link RequestMapper} and {@link ResponseMapper}. + * + * @author bjorncs + */ +public class RestApiMappers { + + private static final Logger log = Logger.getLogger(RestApiMappers.class.getName()); + + public static List<RequestMapperHolder<?>> DEFAULT_REQUEST_MAPPERS = List.of( + new RequestMapperHolder<>(Slime.class, RestApiMappers::toSlime), + new RequestMapperHolder<>(JsonNode.class, ctx -> toJsonNode(ctx, ctx.jacksonJsonMapper())), + new RequestMapperHolder<>(String.class, RestApiMappers::toString), + new RequestMapperHolder<>(byte[].class, RestApiMappers::toByteArray), + new RequestMapperHolder<>(InputStream .class, RestApiMappers::toInputStream), + new RequestMapperHolder<>(Void.class, ctx -> Optional.empty())); + + public static List<ResponseMapperHolder<?>> DEFAULT_RESPONSE_MAPPERS = List.of( + new ResponseMapperHolder<>(HttpResponse.class, (context, entity) -> entity), + new ResponseMapperHolder<>(String.class, (context, entity) -> new MessageResponse(entity)), + new ResponseMapperHolder<>(Slime.class, (context, entity) -> new SlimeJsonResponse(entity)), + new ResponseMapperHolder<>(JsonNode.class, + (context, entity) -> new JacksonJsonResponse<>(200, entity, context.jacksonJsonMapper(), true))); + + public static List<ExceptionMapperHolder<?>> DEFAULT_EXCEPTION_MAPPERS = List.of( + new ExceptionMapperHolder<>(RestApiException.class, (context, exception) -> exception.response())); + + private RestApiMappers() {} + + public static class JacksonRequestMapper<ENTITY> implements RequestMapper<ENTITY> { + private final Class<ENTITY> type; + + JacksonRequestMapper(Class<ENTITY> type) { this.type = type; } + + @Override + public Optional<ENTITY> toRequestEntity(RestApi.RequestContext context) throws RestApiException { + if (log.isLoggable(Level.FINE)) { + return RestApiMappers.toString(context).map(string -> { + log.fine(() -> "Request content: " + string); + return convertIoException("Failed to parse JSON", () -> context.jacksonJsonMapper().readValue(string, type)); + }); + } else { + return toInputStream(context) + .map(in -> convertIoException("Invalid JSON", () -> context.jacksonJsonMapper().readValue(in, type))); + } + } + } + + public static class JacksonResponseMapper<ENTITY> implements ResponseMapper<ENTITY> { + @Override + public HttpResponse toHttpResponse(RestApi.RequestContext context, ENTITY responseEntity) throws RestApiException { + return new JacksonJsonResponse<>(200, responseEntity, context.jacksonJsonMapper(), true); + } + } + + public static class RequestMapperHolder<ENTITY> { + final Class<ENTITY> type; + final RestApi.RequestMapper<ENTITY> mapper; + + RequestMapperHolder(Class<ENTITY> type, RequestMapper<ENTITY> mapper) { + this.type = type; + this.mapper = mapper; + } + } + + public static class ResponseMapperHolder<ENTITY> { + final Class<ENTITY> type; + final RestApi.ResponseMapper<ENTITY> mapper; + + ResponseMapperHolder(Class<ENTITY> type, RestApi.ResponseMapper<ENTITY> mapper) { + this.type = type; + this.mapper = mapper; + } + + HttpResponse toHttpResponse(RestApi.RequestContext ctx, Object entity) { + return mapper.toHttpResponse(ctx, type.cast(entity)); + } + } + + public static class ExceptionMapperHolder<EXCEPTION extends RuntimeException> { + final Class<EXCEPTION> type; + final RestApi.ExceptionMapper<EXCEPTION> mapper; + + ExceptionMapperHolder(Class<EXCEPTION> type, RestApi.ExceptionMapper<EXCEPTION> mapper) { + this.type = type; + this.mapper = mapper; + } + + HttpResponse toResponse(RestApi.RequestContext ctx, RuntimeException e) { + return mapper.toResponse(ctx, type.cast(e)); + } + } + + private static Optional<InputStream> toInputStream(RestApi.RequestContext context) { + return context.requestContent().map(RestApi.RequestContext.RequestContent::content); + } + + private static Optional<byte[]> toByteArray(RestApi.RequestContext context) { + InputStream in = toInputStream(context).orElse(null); + if (in == null) return Optional.empty(); + return convertIoException(() -> Optional.of(in.readAllBytes())); + } + + private static Optional<String> toString(RestApi.RequestContext context) { + try { + return toByteArray(context).map(bytes -> new String(bytes, UTF_8)); + } catch (RuntimeException e) { + throw new RestApiException.BadRequest("Failed parse request content as UTF-8: " + Exceptions.toMessageString(e), e); + } + } + + private static Optional<JsonNode> toJsonNode(RestApi.RequestContext context, ObjectMapper jacksonJsonMapper) { + if (log.isLoggable(Level.FINE)) { + return toString(context).map(string -> { + log.fine(() -> "Request content: " + string); + return convertIoException("Failed to parse JSON", () -> jacksonJsonMapper.readTree(string)); + }); + } else { + return toInputStream(context) + .map(in -> convertIoException("Invalid JSON", () -> jacksonJsonMapper.readTree(in))); + } + } + + @FunctionalInterface private interface SupplierThrowingIoException<T> { T get() throws IOException; } + private static <T> T convertIoException(String messagePrefix, SupplierThrowingIoException<T> supplier) { + try { + return supplier.get(); + } catch (IOException e) { + log.log(Level.FINE, e.getMessage(), e); + throw new RestApiException.InternalServerError(messagePrefix + ": " + Exceptions.toMessageString(e), e); + } + } + + private static <T> T convertIoException(SupplierThrowingIoException<T> supplier) { + return convertIoException("Failed to read request content", supplier); + } + + private static Optional<Slime> toSlime(RestApi.RequestContext context) { + try { + return toString(context).map(string -> { + log.fine(() -> "Request content: " + string); + return SlimeUtils.jsonToSlimeOrThrow(string); + }); + } catch (com.yahoo.slime.JsonParseException e) { + log.log(Level.FINE, e.getMessage(), e); + throw new RestApiException.BadRequest("Invalid JSON: " + Exceptions.toMessageString(e), e); + } + } + +} diff --git a/container-core/src/main/java/com/yahoo/restapi/RestApiRequestHandler.java b/container-core/src/main/java/com/yahoo/restapi/RestApiRequestHandler.java index c501ad8c804..4f8adfe9bef 100644 --- a/container-core/src/main/java/com/yahoo/restapi/RestApiRequestHandler.java +++ b/container-core/src/main/java/com/yahoo/restapi/RestApiRequestHandler.java @@ -4,6 +4,7 @@ package com.yahoo.restapi; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.container.jdisc.LoggingRequestHandler; +import com.yahoo.container.jdisc.RequestHandlerSpec; import com.yahoo.jdisc.Metric; import java.util.concurrent.Executor; @@ -48,6 +49,7 @@ public abstract class RestApiRequestHandler<T extends RestApiRequestHandler<T>> } @Override public final HttpResponse handle(HttpRequest request) { return restApi.handleRequest(request); } + @Override public RequestHandlerSpec requestHandlerSpec() { return restApi.requestHandlerSpec(); } public RestApi restApi() { return restApi; } } diff --git a/container-core/src/main/resources/configdefinitions/container.di.config.jersey-bundles.def b/container-core/src/main/resources/configdefinitions/container.di.config.jersey-bundles.def deleted file mode 100644 index a226420274d..00000000000 --- a/container-core/src/main/resources/configdefinitions/container.di.config.jersey-bundles.def +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -namespace=container.di.config - -# The SymbolicName[:Version] of the Jersey bundles -bundles[].spec string - -# The packages to scan for Jersey resources -bundles[].packages[] string diff --git a/container-core/src/main/resources/configdefinitions/container.di.config.jersey-injection.def b/container-core/src/main/resources/configdefinitions/container.di.config.jersey-injection.def deleted file mode 100644 index 9f5be59abbd..00000000000 --- a/container-core/src/main/resources/configdefinitions/container.di.config.jersey-injection.def +++ /dev/null @@ -1,5 +0,0 @@ -# Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -namespace=container.di.config - -inject[].instance string -inject[].forClass string diff --git a/container-core/src/test/java/com/yahoo/container/di/ContainerTest.java b/container-core/src/test/java/com/yahoo/container/di/ContainerTest.java index b596246a43d..47ac3018fde 100644 --- a/container-core/src/test/java/com/yahoo/container/di/ContainerTest.java +++ b/container-core/src/test/java/com/yahoo/container/di/ContainerTest.java @@ -5,13 +5,10 @@ import com.google.inject.Guice; import com.yahoo.component.AbstractComponent; import com.yahoo.config.di.IntConfig; import com.yahoo.config.test.TestConfig; -import com.yahoo.container.bundle.MockBundle; import com.yahoo.container.di.componentgraph.Provider; import com.yahoo.container.di.componentgraph.core.ComponentGraph; import com.yahoo.container.di.componentgraph.core.ComponentGraphTest.SimpleComponent; -import com.yahoo.container.di.componentgraph.core.ComponentGraphTest.SimpleComponent2; import com.yahoo.container.di.componentgraph.core.ComponentNode.ComponentConstructorException; -import com.yahoo.container.di.config.RestApiContext; import org.junit.Ignore; import org.junit.Test; import org.osgi.framework.Bundle; @@ -218,64 +215,6 @@ public class ContainerTest extends ContainerTestBase { assertNotNull(newGraph.get(5, TimeUnit.MINUTES)); } - - @Test - public void bundle_info_is_set_on_rest_api_context() { - Class<RestApiContext> clazz = RestApiContext.class; - - writeBootstrapConfigs("restApiContext", clazz); - dirConfigSource.writeConfig("jersey-bundles", "bundles[0].spec \"mock-entry-to-enforce-a-MockBundle\""); - dirConfigSource.writeConfig("jersey-injection", "inject[0]"); - - Container container = newContainer(dirConfigSource); - ComponentGraph componentGraph = getNewComponentGraph(container); - - RestApiContext restApiContext = componentGraph.getInstance(clazz); - assertNotNull(restApiContext); - - assertEquals(1, restApiContext.getBundles().size()); - assertEquals(MockBundle.SymbolicName, restApiContext.getBundles().get(0).symbolicName); - assertEquals(MockBundle.BundleVersion, restApiContext.getBundles().get(0).version); - - container.shutdownConfigurer(); - } - - @Test - public void restApiContext_has_all_components_injected() { - Class<RestApiContext> restApiClass = RestApiContext.class; - Class<SimpleComponent> injectedClass = SimpleComponent.class; - String injectedComponentId = "injectedComponent"; - Class<SimpleComponent2> anotherComponentClass = SimpleComponent2.class; - String anotherComponentId = "anotherComponent"; - - String componentsConfig = - new ComponentEntry(injectedComponentId, injectedClass).asConfig(0) + "\n" + - new ComponentEntry(anotherComponentId, anotherComponentClass).asConfig(1) + "\n" + - new ComponentEntry("restApiContext", restApiClass).asConfig(2) + "\n" + - "components[2].inject[0].id " + injectedComponentId + "\n" + - "components[2].inject[1].id " + anotherComponentId + "\n"; - - String injectionConfig = "inject[1]\n" +// - "inject[0].instance " + injectedComponentId + "\n" +// - "inject[0].forClass \"" + injectedClass.getName() + "\"\n"; - - dirConfigSource.writeConfig("components", componentsConfig); - dirConfigSource.writeConfig("platform-bundles", ""); - dirConfigSource.writeConfig("application-bundles", ""); - dirConfigSource.writeConfig("jersey-bundles", "bundles[0].spec \"mock-entry-to-enforce-a-MockBundle\""); - dirConfigSource.writeConfig("jersey-injection", injectionConfig); - - Container container = newContainer(dirConfigSource); - ComponentGraph componentGraph = getNewComponentGraph(container); - - RestApiContext restApiContext = componentGraph.getInstance(restApiClass); - - assertFalse(restApiContext.getInjectableComponents().isEmpty()); - assertEquals(2, restApiContext.getInjectableComponents().size()); - - container.shutdownConfigurer(); - } - @Test public void providers_are_destructed() { writeBootstrapConfigs("id1", DestructableProvider.class); diff --git a/container-core/src/test/java/com/yahoo/container/di/ContainerTestBase.java b/container-core/src/test/java/com/yahoo/container/di/ContainerTestBase.java index 2106a1f3671..0d1a308f182 100644 --- a/container-core/src/test/java/com/yahoo/container/di/ContainerTestBase.java +++ b/container-core/src/test/java/com/yahoo/container/di/ContainerTestBase.java @@ -7,7 +7,6 @@ import com.yahoo.config.FileReference; import com.yahoo.container.bundle.BundleInstantiationSpecification; import com.yahoo.container.di.ContainerTest.ComponentTakingConfig; import com.yahoo.container.di.componentgraph.core.ComponentGraph; -import com.yahoo.container.di.osgi.BundleClasses; import org.junit.After; import org.junit.Before; import org.osgi.framework.Bundle; @@ -57,11 +56,6 @@ public class ContainerTestBase { } @Override - public BundleClasses getBundleClasses(ComponentSpecification bundle, Set<String> packagesToScan) { - throw new UnsupportedOperationException("getBundleClasses not supported"); - } - - @Override public Set<Bundle> useApplicationBundles(Collection<FileReference> bundles) { return emptySet(); } diff --git a/container-core/src/test/java/com/yahoo/container/di/componentgraph/core/ComponentGraphTest.java b/container-core/src/test/java/com/yahoo/container/di/componentgraph/core/ComponentGraphTest.java index 70dc4c8665c..43e36781101 100644 --- a/container-core/src/test/java/com/yahoo/container/di/componentgraph/core/ComponentGraphTest.java +++ b/container-core/src/test/java/com/yahoo/container/di/componentgraph/core/ComponentGraphTest.java @@ -16,11 +16,7 @@ import com.yahoo.config.ConfigInstance; import com.yahoo.config.subscription.ConfigGetter; import com.yahoo.config.test.Test2Config; import com.yahoo.config.test.TestConfig; -import com.yahoo.container.di.Osgi; import com.yahoo.container.di.componentgraph.Provider; -import com.yahoo.container.di.config.JerseyBundlesConfig; -import com.yahoo.container.di.config.JerseyInjectionConfig; -import com.yahoo.container.di.config.RestApiContext; import com.yahoo.vespa.config.ConfigKey; import org.junit.Test; @@ -485,25 +481,6 @@ public class ComponentGraphTest { assertThat(componentGraph.getInstance(ComponentTakingComponentId.class).componentId, is(ComponentId.fromString(componentId))); } - @Test - public void rest_api_context_can_be_instantiated() { - String configId = "raw:\"\""; - - Class<RestApiContext> clazz = RestApiContext.class; - JerseyNode jerseyNode = new JerseyNode(uniqueComponentId(clazz.getName()), configId, clazz, new Osgi() { - }); - - ComponentGraph componentGraph = new ComponentGraph(); - componentGraph.add(jerseyNode); - componentGraph.complete(); - - componentGraph - .setAvailableConfigs(ConfigMap.newMap(JerseyBundlesConfig.class, configId).add(JerseyInjectionConfig.class, configId)); - - RestApiContext restApiContext = componentGraph.getInstance(clazz); - assertNotNull(restApiContext); - assertThat(restApiContext.getBundles().size(), is(0)); - } //Note that all Components must be defined in a static context, //otherwise their constructor will take the outer class as the first parameter. diff --git a/container-core/src/test/java/com/yahoo/container/di/componentgraph/core/JerseyNodeTest.java b/container-core/src/test/java/com/yahoo/container/di/componentgraph/core/JerseyNodeTest.java deleted file mode 100644 index f30f9260830..00000000000 --- a/container-core/src/test/java/com/yahoo/container/di/componentgraph/core/JerseyNodeTest.java +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.di.componentgraph.core; - -import com.yahoo.container.bundle.MockBundle; -import com.yahoo.container.di.config.RestApiContext; -import com.yahoo.container.di.osgi.OsgiUtil; -import org.junit.Test; -import org.osgi.framework.wiring.BundleWiring; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.junit.Assert.assertThat; - -/** - * @author gjoranv - * @author ollivir - */ - -public class JerseyNodeTest { - private MockBundle bundle; - private List<String> bundleClasses; - private final Map<String, String> resources; - - public JerseyNodeTest() { - resources = new HashMap<>(); - resources.put("com/foo", "com/foo/Foo.class"); - resources.put("com/bar", "com/bar/Bar.class"); - bundle = new MockBundle() { - @Override - public Collection<String> listResources(String path, String ignored, int options) { - if ((options & BundleWiring.LISTRESOURCES_RECURSE) != 0 && path.equals("/")) { - return resources.values(); - } else { - return Collections.singleton(resources.get(path)); - } - } - }; - bundleClasses = new ArrayList<>(resources.values()); - } - - @Test - public void all_bundle_entries_are_returned_when_no_packages_are_given() { - Collection<String> entries = OsgiUtil.getClassEntriesInBundleClassPath(bundle, Collections.emptySet()); - assertThat(entries, containsInAnyOrder(bundleClasses.toArray())); - } - - @Test - public void only_bundle_entries_from_the_given_packages_are_returned() { - Collection<String> entries = OsgiUtil.getClassEntriesInBundleClassPath(bundle, Collections.singleton("com.foo")); - assertThat(entries, contains(resources.get("com/foo"))); - } - - @Test - public void bundle_info_is_initialized() { - RestApiContext.BundleInfo bundleInfo = JerseyNode.createBundleInfo(bundle, Collections.emptyList()); - assertThat(bundleInfo.symbolicName, is(bundle.getSymbolicName())); - assertThat(bundleInfo.version, is(bundle.getVersion())); - assertThat(bundleInfo.fileLocation, is(bundle.getLocation())); - } -} diff --git a/container-core/src/test/java/com/yahoo/restapi/RestApiImplTest.java b/container-core/src/test/java/com/yahoo/restapi/RestApiImplTest.java index 06fc6d80741..44dd61836d6 100644 --- a/container-core/src/test/java/com/yahoo/restapi/RestApiImplTest.java +++ b/container-core/src/test/java/com/yahoo/restapi/RestApiImplTest.java @@ -1,14 +1,18 @@ package com.yahoo.restapi;// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. import com.fasterxml.jackson.annotation.JsonProperty; +import com.yahoo.container.jdisc.AclMapping; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.jdisc.RequestHandlerSpec; +import com.yahoo.container.jdisc.RequestView; import com.yahoo.test.json.JsonTestHelper; import com.yahoo.yolean.Exceptions; import org.junit.jupiter.api.Test; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.net.URI; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.ArrayList; @@ -16,6 +20,7 @@ import java.util.List; import java.util.Map; import static com.yahoo.jdisc.http.HttpRequest.Method; +import static com.yahoo.restapi.RestApi.handlerConfig; import static com.yahoo.restapi.RestApi.route; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -103,13 +108,32 @@ class RestApiImplTest { } @Test - public void uri_builder_creates_valid_uri_prefix() { + void uri_builder_creates_valid_uri_prefix() { RestApi restApi = RestApi.builder() .addRoute(route("/test").get(ctx -> new MessageResponse(ctx.uriBuilder().toString()))) .build(); verifyJsonResponse(restApi, Method.GET, "/test", null, 200, "{\"message\":\"http://localhost\"}"); } + @Test + void resolves_correct_acl_action() { + AclMapping.Action customAclAction = AclMapping.Action.custom("custom-action"); + RestApi restApi = RestApi.builder() + .addRoute(route("/api1") + .get(ctx -> new MessageResponse(ctx.aclAction().name()), + handlerConfig().withCustomAclAction(customAclAction))) + .addRoute(route("/api2") + .post(ctx -> new MessageResponse(ctx.aclAction().name()))) + .build(); + + verifyJsonResponse(restApi, Method.GET, "/api1", null, 200, "{\"message\":\"custom-action\"}"); + verifyJsonResponse(restApi, Method.POST, "/api2", "ignored", 200, "{\"message\":\"write\"}"); + + RequestHandlerSpec spec = restApi.requestHandlerSpec(); + assertRequestHandlerSpecAclMapping(spec, customAclAction, Method.GET, "/api1"); + assertRequestHandlerSpecAclMapping(spec, AclMapping.Action.WRITE, Method.POST, "/api2"); + } + private static void verifyJsonResponse(RestApi restApi, Method method, String path, String requestContent, int expectedStatusCode, String expectedJson) { HttpRequest testRequest; String uri = "http://localhost" + path; @@ -132,6 +156,15 @@ class RestApiImplTest { } } + private static void assertRequestHandlerSpecAclMapping( + RequestHandlerSpec spec, AclMapping.Action expectedAction, Method method, String uriPath) { + RequestView requestView = new RequestView() { + @Override public Method method() { return method; } + @Override public URI uri() { return URI.create("http://localhost" + uriPath); } + }; + assertEquals(expectedAction, spec.aclMapping().get(requestView)); + } + public static class TestEntity { @JsonProperty("mystring") public String stringValue; @JsonProperty("myinstant") public Instant instantValue; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2.java index bfcefecba0c..58afed4143f 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2.java @@ -11,7 +11,6 @@ import com.yahoo.restapi.SlimeJsonResponse; import com.yahoo.slime.Cursor; import com.yahoo.slime.Inspector; import com.yahoo.slime.Slime; -import com.yahoo.slime.SlimeUtils; import com.yahoo.slime.Type; import com.yahoo.vespa.hosted.controller.ApplicationController; import com.yahoo.vespa.hosted.controller.Controller; @@ -26,7 +25,6 @@ import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; import com.yahoo.vespa.hosted.controller.tenant.Tenant; import javax.ws.rs.BadRequestException; -import java.io.IOException; import java.math.BigDecimal; import java.time.Clock; import java.time.Instant; @@ -34,8 +32,8 @@ import java.time.LocalDate; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.util.Comparator; -import java.util.Optional; import java.util.List; +import java.util.Optional; /** * @author ogronnesby @@ -78,11 +76,6 @@ public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandler .addRoute(RestApi.route("/billing/v2/accountant/preview/tenant/{tenant}") .get(self::previewBill) .post(Slime.class, self::createBill)) - /* - * Utility - map Slime.class => SlimeJsonResponse - */ - .addRequestMapper(Slime.class, BillingApiHandlerV2::slimeRequestMapper) - .addResponseMapper(Slime.class, BillingApiHandlerV2::slimeResponseMapper) .build(); } @@ -337,16 +330,4 @@ public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandler return inspector.field(field).asString(); } - private static Optional<Slime> slimeRequestMapper(RestApi.RequestContext requestContext) { - try { - return Optional.of(SlimeUtils.jsonToSlime(requestContext.requestContentOrThrow().content().readAllBytes())); - } catch (IOException e) { - throw new IllegalArgumentException("Could not parse JSON input"); - } - } - - private static HttpResponse slimeResponseMapper(RestApi.RequestContext ctx, Slime slime) { - return new SlimeJsonResponse(slime); - } - } diff --git a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java index 4a104deaf55..d57b15b50b4 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java @@ -150,13 +150,6 @@ public class Flags { "Takes effect at redeployment", ZONE_ID, APPLICATION_ID); - public static final UnboundIntFlag METRICS_PROXY_MAX_HEAP_SIZE_IN_MB = defineIntFlag( - "metrics-proxy-max-heap-size-in-mb", 256, - List.of("hmusum"), "2021-03-01", "2021-07-15", - "JVM max heap size for metrics proxy in Mb", - "Takes effect when restarting metrics proxy", - CLUSTER_TYPE); - public static final UnboundStringFlag DEDICATED_CLUSTER_CONTROLLER_FLAVOR = defineStringFlag( "dedicated-cluster-controller-flavor", "", List.of("jonmv"), "2021-02-25", "2021-08-25", "Flavor as <vpu>-<memgb>-<diskgb> to use for dedicated cluster controller nodes", diff --git a/vespajlib/src/main/java/com/yahoo/osgi/maven/ProjectBundleClassPaths.java b/vespajlib/src/main/java/com/yahoo/osgi/maven/ProjectBundleClassPaths.java deleted file mode 100644 index 5d64548e4b9..00000000000 --- a/vespajlib/src/main/java/com/yahoo/osgi/maven/ProjectBundleClassPaths.java +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.osgi.maven; - -import com.yahoo.slime.Cursor; -import com.yahoo.slime.Inspector; -import com.yahoo.slime.JsonFormat; -import com.yahoo.slime.Slime; -import com.yahoo.slime.SlimeUtils; - -import java.io.BufferedOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -/** - * Represents the bundles in a maven project and the classpath elements - * corresponding to code that would end up in the bundle. - * - * @author Tony Vaagenes - * @author bjorncs - */ - -public class ProjectBundleClassPaths { - public static final String CLASSPATH_MAPPINGS_FILENAME = "bundle-plugin.bundle-classpath-mappings.json"; - - public final BundleClasspathMapping mainBundle; - public final List<BundleClasspathMapping> providedDependencies; - - public ProjectBundleClassPaths(BundleClasspathMapping mainBundle, - List<BundleClasspathMapping> providedDependencies) { - this.mainBundle = mainBundle; - this.providedDependencies = providedDependencies; - } - - public static void save(Path path, ProjectBundleClassPaths mappings) throws IOException { - Files.createDirectories(path.getParent()); - try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(path))) { - save(out, mappings); - } - } - - static void save(OutputStream out, ProjectBundleClassPaths mappings) throws IOException { - Slime slime = new Slime(); - Cursor rootCursor = slime.setObject(); - Cursor mainBundleCursor = rootCursor.setObject("mainBundle"); - BundleClasspathMapping.save(mainBundleCursor, mappings.mainBundle); - Cursor dependenciesCursor = rootCursor.setArray("providedDependencies"); - mappings.providedDependencies - .forEach(d -> BundleClasspathMapping.save(dependenciesCursor.addObject(), d)); - new JsonFormat(false).encode(out, slime); - } - - public static ProjectBundleClassPaths load(Path path) throws IOException { - byte[] bytes = Files.readAllBytes(path); - return load(bytes); - } - - static ProjectBundleClassPaths load(byte[] bytes) { - Inspector inspector = SlimeUtils.jsonToSlime(bytes).get(); - BundleClasspathMapping mainBundle = BundleClasspathMapping.load(inspector.field("mainBundle")); - Inspector dependenciesInspector = inspector.field("providedDependencies"); - List<BundleClasspathMapping> providedDependencies = new ArrayList<>(); - for (int i = 0; i < dependenciesInspector.entries(); i++) { - providedDependencies.add(BundleClasspathMapping.load(dependenciesInspector.entry(i))); - } - return new ProjectBundleClassPaths(mainBundle, providedDependencies); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ProjectBundleClassPaths that = (ProjectBundleClassPaths) o; - return Objects.equals(mainBundle, that.mainBundle) && - Objects.equals(providedDependencies, that.providedDependencies); - } - - @Override - public int hashCode() { - return Objects.hash(mainBundle, providedDependencies); - } - - public static class BundleClasspathMapping { - public final String bundleSymbolicName; - public final List<String> classPathElements; - - public BundleClasspathMapping(String bundleSymbolicName, - List<String> classPathElements) { - this.bundleSymbolicName = bundleSymbolicName; - this.classPathElements = classPathElements; - } - - static void save(Cursor rootCursor, BundleClasspathMapping mapping) { - rootCursor.setString("bundleSymbolicName", mapping.bundleSymbolicName); - Cursor arrayCursor = rootCursor.setArray("classPathElements"); - mapping.classPathElements.forEach(arrayCursor::addString); - } - - static BundleClasspathMapping load(Inspector inspector) { - String bundleSymoblicName = inspector.field("bundleSymbolicName").asString(); - Inspector elementsInspector = inspector.field("classPathElements"); - List<String> classPathElements = new ArrayList<>(); - for (int i = 0; i < elementsInspector.entries(); i++) { - classPathElements.add(elementsInspector.entry(i).asString()); - } - return new BundleClasspathMapping(bundleSymoblicName, classPathElements); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - BundleClasspathMapping that = (BundleClasspathMapping) o; - return Objects.equals(bundleSymbolicName, that.bundleSymbolicName) && - Objects.equals(classPathElements, that.classPathElements); - } - - @Override - public int hashCode() { - return Objects.hash(bundleSymbolicName, classPathElements); - } - } - -} diff --git a/vespajlib/src/test/java/com/yahoo/osgi/maven/ProjectBundleClassPathsTest.java b/vespajlib/src/test/java/com/yahoo/osgi/maven/ProjectBundleClassPathsTest.java deleted file mode 100644 index 44a6a2eee8d..00000000000 --- a/vespajlib/src/test/java/com/yahoo/osgi/maven/ProjectBundleClassPathsTest.java +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.osgi.maven; - -import com.yahoo.osgi.maven.ProjectBundleClassPaths.BundleClasspathMapping; -import org.junit.Test; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; - -import static java.util.Arrays.asList; -import static org.junit.Assert.assertEquals; - -/** - * @author bjorncs - */ -public class ProjectBundleClassPathsTest { - - @Test - public void bundle_classpaths_serializes_correctly_to_json() throws IOException { - ProjectBundleClassPaths projectBundleClassPaths = - new ProjectBundleClassPaths( - new BundleClasspathMapping("main-bundle-name", asList("classpath-elem-0-1", "classpath-elem-0-2")), - asList( - new BundleClasspathMapping( - "main-bundle-dep1", - asList("classpath-elem-1-1", "classpath-elem-1-2")), - new BundleClasspathMapping( - "main-bundle-dep2", - asList("classpath-elem-2-1", "classpath-elem-2-2")))); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - ProjectBundleClassPaths.save(out, projectBundleClassPaths); - ProjectBundleClassPaths deserialized = ProjectBundleClassPaths.load(out.toByteArray()); - assertEquals(projectBundleClassPaths, deserialized); - } - -} |