/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package gobblin.restli; import java.net.URI; import java.net.URISyntaxException; import java.util.Collection; import java.util.Random; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Optional; import com.google.common.base.Strings; import com.google.common.collect.Sets; import com.google.common.util.concurrent.AbstractIdleService; import com.google.inject.Binder; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Module; import com.linkedin.r2.filter.FilterChain; import com.linkedin.r2.filter.FilterChains; import com.linkedin.r2.filter.compression.EncodingType; import com.linkedin.r2.filter.compression.ServerCompressionFilter; import com.linkedin.r2.transport.common.bridge.server.TransportDispatcher; import com.linkedin.r2.transport.http.server.HttpNettyServerFactory; import com.linkedin.r2.transport.http.server.HttpServer; import com.linkedin.restli.docgen.DefaultDocumentationRequestHandler; import com.linkedin.restli.server.DelegatingTransportDispatcher; import com.linkedin.restli.server.RestLiConfig; import com.linkedin.restli.server.RestLiServer; import com.linkedin.restli.server.guice.GuiceInjectResourceFactory; import com.linkedin.restli.server.resources.BaseResource; import com.linkedin.restli.server.resources.ResourceFactory; import com.linkedin.restli.server.validation.RestLiInputValidationFilter; import lombok.Builder; import lombok.Getter; /** * An embedded Rest.li server using Netty. * * Usage: * EmbeddedRestliServer server = EmbeddedRestliServer.builder().resources(List<RestliResource>).build(); * server.startAsync() * * The server is a {@link com.google.common.util.concurrent.Service} that provides access to a collection of Rest.li * resources. The following are optional settings (available through builder pattern): * * port - defaults to randomly chosen port between {@link #MIN_PORT} and {@link #MAX_PORT}. * * log - defaults to class Logger. * * name - defaults to the name of the first resource in the resource collection. * * injector - an {@link Injector} to inject dependencies into the Rest.li resources. */ public class EmbeddedRestliServer extends AbstractIdleService { private static final int MAX_PORT = 65535; private static final int MIN_PORT = 1024; private static final Logger LOGGER = LoggerFactory.getLogger(EmbeddedRestliServer.class); @Getter private final URI serverUri; @Getter private final int port; @Getter private final Injector injector; private final Logger log; @Getter private final String name; private final Collection<Class<? extends BaseResource>> resources; private volatile Optional<HttpServer> httpServer; @Builder public EmbeddedRestliServer(URI serverUri, int port, Injector injector, Logger log, String name, Collection<Class<? extends BaseResource>> resources) { this.resources = resources; if (this.resources.isEmpty()) { throw new RuntimeException("No resources specified for embedded server."); } try { this.serverUri = serverUri == null ? new URI("http://localhost") : serverUri; } catch (URISyntaxException use) { throw new RuntimeException("Invalid URI. This is an error in code.", use); } this.port = computePort(port, this.serverUri); this.injector = injector == null ? Guice.createInjector(new Module() { @Override public void configure(Binder binder) { } }) : injector; this.log = log == null ? LOGGER : log; this.name = Strings.isNullOrEmpty(name) ? this.resources.iterator().next().getSimpleName() : name; } private final int computePort(int port, URI uri) { if (port > 0) { return port; } else if (uri.getPort() > 0) { return uri.getPort(); } else { return new Random().nextInt(MAX_PORT - MIN_PORT + 1) + MIN_PORT; } } @Override protected void startUp() throws Exception { RestLiConfig config = new RestLiConfig(); Set<String> resourceClassNames = Sets.newHashSet(); for (Class<? extends BaseResource> resClass : this.resources) { resourceClassNames.add(resClass.getName()); } config.addResourceClassNames(resourceClassNames); config.setServerNodeUri(this.serverUri); config.setDocumentationRequestHandler(new DefaultDocumentationRequestHandler()); config.addRequestFilter(new RestLiInputValidationFilter()); ResourceFactory factory = new GuiceInjectResourceFactory(this.injector); TransportDispatcher dispatcher = new DelegatingTransportDispatcher(new RestLiServer(config, factory)); FilterChain filterChain = FilterChains.create(new ServerCompressionFilter(new EncodingType[] { EncodingType.SNAPPY, EncodingType.GZIP })); this.httpServer = Optional.of(new HttpNettyServerFactory(filterChain).createServer(this.port, dispatcher)); this.log.info("Starting the {} embedded server at port {}.", this.name, this.port); this.httpServer.get().start(); } @Override protected void shutDown() throws Exception { if (this.httpServer.isPresent()) { this.log.info("Stopping the {} embedded server at port {}", this.name, this.port); this.httpServer.get().stop(); this.httpServer.get().waitForStop(); } } /** * Get the scheme and authority at which this server is listening. */ public URI getListeningURI() { try { return new URI(this.serverUri.getScheme(), this.serverUri.getUserInfo(), this.serverUri.getHost(), this.port, null, null, null); } catch (URISyntaxException use) { throw new RuntimeException("Invalid URI. This is an error in code.", use); } } /** * Get uri prefix that should be used to create a {@link com.linkedin.restli.client.RestClient}. */ public String getURIPrefix() { return getListeningURI().toString() + "/"; } }