package demo; import java.net.URI; import java.net.URISyntaxException; import java.util.Collection; import java.util.Collections; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.cloud.netflix.hystrix.EnableHystrix; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.core.ParameterizedTypeReference; import org.springframework.hateoas.MediaTypes; import org.springframework.hateoas.Resources; import org.springframework.hateoas.client.Traverson; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; import reactor.core.Environment; import reactor.rx.Stream; import reactor.rx.spec.Streams; import com.netflix.discovery.DiscoveryClient; import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import demo.StoreDetails.Recommendation; @Configuration @ComponentScan @EnableAutoConfiguration @EnableEurekaClient @EnableHystrix @RestController public class RecommendationApplication { @Autowired private StoreService stores; @RequestMapping("/{customerId}") public DeferredResult<List<StoreDetails>> recommend(@PathVariable String customerId) throws Exception { return toDeferredResult(fetch(customerId)); } public static void main(String[] args) { SpringApplication.run(RecommendationApplication.class, args); } private Stream<StoreDetails> fetch(String customerId) { Stream<StoreDetails> details = stores.nearbyStores(customerId).flatMap( store -> { StoreDetails result = new StoreDetails(store); Stream<Recommendation> recommendations = stores .recommendationsForStore(store); return recommendations.map(recommendation -> { if (recommendation != null) { result.getRecommendations().add(recommendation); } return result; }); }); return details; } private static <T> DeferredResult<List<T>> toDeferredResult(Stream<T> publisher) { DeferredResult<List<T>> deferred = new DeferredResult<List<T>>(); publisher.buffer().consume((List<T> result) -> deferred.setResult(result)); return deferred; } } @Component class StoreService { private final DiscoveryClient client; private Environment environment; @Autowired public StoreService(Environment environment, DiscoveryClient client) { this.environment = environment; this.client = client; } @HystrixCommand(fallbackMethod = "emptyStream") public Stream<Recommendation> recommendationsForStore(Store store) { return Streams.defer(environment, recommendations(store)); } @HystrixCommand(fallbackMethod = "emptyStream") public Stream<Store> nearbyStores(String customerId) { // TODO: iterate over pages return Streams.defer(environment, stores(customerId)); } protected <T> Stream<T> emptyStream(String id) { return Streams.defer(environment, Collections.<T>singleton(null)); } protected <T> Stream<T> emptyStream(Store input) { return Streams.defer(environment, Collections.<T>singleton(null)); } private Collection<Store> stores(String customerId) { String url = client.getNextServerFromEureka("CUSTOMERS", false).getHomePageUrl(); URI base; try { base = new URI(url + "customers/" + customerId); } catch (URISyntaxException e) { throw new IllegalStateException("Could not create base URI", e); } final Collection<Store> stores = new Traverson(base, MediaTypes.HAL_JSON) .follow("stores-nearby") .toObject(new ParameterizedTypeReference<Resources<Store>>() { }).getContent(); return stores; } private Collection<Recommendation> recommendations(Store store) { throw new UnsupportedOperationException("No recommendations available"); } }