<|start_filename|>4-reactive/src/test/java/com/greglturnquist/hackingspringboot/reactive/HomeControllerSliceTest.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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 com.greglturnquist.hackingspringboot.reactive;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.reactive.server.WebTestClient;

/**
 * @author <NAME>
 */
// tag::code[]
@WebFluxTest(HomeController.class) // <1>
public class HomeControllerSliceTest {

	@Autowired // <2>
	private WebTestClient client;

	@MockBean // <3>
	InventoryService inventoryService;

	@Test
	void homePage() {
		when(inventoryService.getInventory()).thenReturn(Flux.just( //
				new Item("id1", "name1", "desc1", 1.99), //
				new Item("id2", "name2", "desc2", 9.99) //
		));
		when(inventoryService.getCart("My Cart")) //
				.thenReturn(Mono.just(new Cart("My Cart")));

		client.get().uri("/").exchange() //
				.expectStatus().isOk() //
				.expectBody(String.class) //
				.consumeWith(exchangeResult -> {
					assertThat( //
							exchangeResult.getResponseBody()).contains("action=\"/add/id1\"");
					assertThat( //
							exchangeResult.getResponseBody()).contains("action=\"/add/id2\"");
				});
	}
}
// end::code[]


<|start_filename|>5-reactive/src/main/java/com/greglturnquist/hackingspringboot/reactive/SpringDataHttpTraceRepository.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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 com.greglturnquist.hackingspringboot.reactive;

import org.springframework.boot.actuate.trace.http.HttpTrace;
import org.springframework.boot.actuate.trace.http.HttpTraceRepository;

import java.util.List;
import java.util.stream.Collectors;

/**
 * @author <NAME>
 */
// tag::code[]
public class SpringDataHttpTraceRepository implements HttpTraceRepository {

	private final HttpTraceWrapperRepository repository;

	public SpringDataHttpTraceRepository(HttpTraceWrapperRepository repository) {
		this.repository = repository; // <1>
	}

	@Override
	public List<HttpTrace> findAll() {
		return repository.findAll() //
				.map(HttpTraceWrapper::getHttpTrace) // <2>
				.collect(Collectors.toList());
	}

	@Override
	public void add(HttpTrace trace) {
		repository.save(new HttpTraceWrapper(trace)); // <3>
	}
}
// end::code[]


<|start_filename|>8-reactive-server/src/main/java/com/greglturnquist/hackingspringboot/reactive/server/RSocketService.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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 com.greglturnquist.hackingspringboot.reactive.server;

import reactor.core.publisher.EmitterProcessor;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxProcessor;
import reactor.core.publisher.FluxSink;
import reactor.core.publisher.Mono;

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.stereotype.Controller;

/**
 * @author <NAME>
 */
// tag::code[]
@Controller // <1>
public class RSocketService {

	private final ItemRepository repository;
	// end::code[]
	private final EmitterProcessor<Item> itemProcessor;
	private final FluxSink<Item> itemSink;

	// tag::code2[]
	public RSocketService(ItemRepository repository) {
		this.repository = repository; // <2>
		// end::code2[]
		// tag::code3[]
		this.itemProcessor = EmitterProcessor.create(); // <1>
		this.itemSink = this.itemProcessor.sink(); // <2>
	}
	// end::code3[]

	// tag::request-response[]
	@MessageMapping("newItems.request-response") // <1>
	public Mono<Item> processNewItemsViaRSocketRequestResponse(Item item) { // <2>
		return this.repository.save(item) // <3>
				.doOnNext(savedItem -> this.itemSink.next(savedItem)); // <4>
	}
	// end::request-response[]

	// tag::fire-and-forget[]
	@MessageMapping("newItems.fire-and-forget")
	public Mono<Void> processNewItemsViaRSocketFireAndForget(Item item) {
		return this.repository.save(item) //
				.doOnNext(savedItem -> this.itemSink.next(savedItem)) //
				.then();
	}
	// end::fire-and-forget[]

	// tag::monitor[]
	@MessageMapping("newItems.monitor") // <1>
	public Flux<Item> monitorNewItems() { // <2>
		return this.itemProcessor; // <3>
	}
	// end::monitor[]
}


<|start_filename|>9-reactive-oauth/src/main/java/com/greglturnquist/hackingspringboot/reactive/HomeController.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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 com.greglturnquist.hackingspringboot.reactive;

import reactor.core.publisher.Mono;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.reactive.result.view.Rendering;

/**
 * @author <NAME>
 */
@Controller
public class HomeController {

	private final InventoryService inventoryService;

	public HomeController(InventoryService inventoryService) {
		this.inventoryService = inventoryService;
	}

	// tag::user-cart[]
	@GetMapping
	Mono<Rendering> home( //
			@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient,
			@AuthenticationPrincipal OAuth2User oauth2User) { // <1>
		return Mono.just(Rendering.view("home.html") //
				.modelAttribute("items", this.inventoryService.getInventory()) //
				.modelAttribute("cart", this.inventoryService.getCart(cartName(oauth2User)) // <2>
						.defaultIfEmpty(new Cart(cartName(oauth2User)))) //

				// Fetching authentication details is a little more complex
				.modelAttribute("userName", oauth2User.getName()) //
				.modelAttribute("authorities", oauth2User.getAuthorities()) //
				.modelAttribute("clientName", //
						authorizedClient.getClientRegistration().getClientName()) //
				.modelAttribute("userAttributes", oauth2User.getAttributes()) //
				.build());
	}
	// end::user-cart[]

	// tag::adjust-cart[]
	@PostMapping("/add/{id}")
	Mono<String> addToCart(@AuthenticationPrincipal OAuth2User oauth2User, @PathVariable String id) {
		return this.inventoryService.addItemToCart(cartName(oauth2User), id) //
				.thenReturn("redirect:/");
	}

	@DeleteMapping("/remove/{id}")
	Mono<String> removeFromCart(@AuthenticationPrincipal OAuth2User oauth2User, @PathVariable String id) {
		return this.inventoryService.removeOneFromCart(cartName(oauth2User), id) //
				.thenReturn("redirect:/");
	}
	// end::adjust-cart[]

	// tag::inventory[]
	@PostMapping
	@ResponseBody
	Mono<Item> createItem(@RequestBody Item newItem) {
		return this.inventoryService.saveItem(newItem);
	}

	@DeleteMapping("/{id}")
	@ResponseBody
	Mono<Void> deleteItem(@PathVariable String id) {
		return this.inventoryService.deleteItem(id);
	}
	// end::inventory[]

	// tag::cartName[]
	private static String cartName(OAuth2User oAuth2User) {
		return oAuth2User.getName() + "'s Cart";
	}
	// end::cartName[]
}


<|start_filename|>4-reactive/src/test/java/com/greglturnquist/hackingspringboot/reactive/ItemUnitTest.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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 com.greglturnquist.hackingspringboot.reactive;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

import com.greglturnquist.hackingspringboot.reactive.Item;

/**
 * @author <NAME>
 */
// tag::code[]
class ItemUnitTest {

	@Test // <1>
	void itemBasicsShouldWork() {
		Item sampleItem = new Item("item1", "TV tray", "Alf TV tray", 19.99); // <2>

		// Test various aspects using AssertJ <3>
		assertThat(sampleItem.getId()).isEqualTo("item1");
		assertThat(sampleItem.getName()).isEqualTo("TV tray");
		assertThat(sampleItem.getDescription()).isEqualTo("Alf TV tray");
		assertThat(sampleItem.getPrice()).isEqualTo(19.99);

		assertThat(sampleItem.toString()).isEqualTo( //
				"Item{id='item1', name='TV tray', description='Alf TV tray', price=19.99}");

		Item sampleItem2 = new Item("item1", "TV tray", "Alf TV tray", 19.99);
		assertThat(sampleItem).isEqualTo(sampleItem2);
	}
}
// end::code[]


<|start_filename|>6-reactive/src/main/java/com/greglturnquist/hackingspringboot/reactive/ApiItemController.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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 com.greglturnquist.hackingspringboot.reactive;

import java.net.URI;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author <NAME>
 */
// tag::intro[]
@RestController // <1>
public class ApiItemController {

	private final ItemRepository repository; // <2>

	public ApiItemController(ItemRepository repository) {
		this.repository = repository; // <3>
	}
	// end::intro[]

	// tag::all-items[]
	@GetMapping("/api/items") // <1>
	Flux<Item> findAll() { // <2>
		return this.repository.findAll(); // <3>
	}
	// end::all-items[]

	// tag::one-item[]
	@GetMapping("/api/items/{id}") // <1>
	Mono<Item> findOne(@PathVariable String id) { // <2>
		return this.repository.findById(id); // <3>
	}
	// end::one-item[]

	// tag::new-item[]
	@PostMapping("/api/items") // <1>
	Mono<ResponseEntity<?>> addNewItem(@RequestBody Mono<Item> item) { // <2>

		return item.flatMap(s -> this.repository.save(s)) // <3>
				.map(savedItem -> ResponseEntity //
						.created(URI.create("/api/items/" + //
								savedItem.getId())) // <4>
						.body(savedItem)); // <5>
	}
	// end::new-item[]

	// tag::replace-item[]
	@PutMapping("/api/items/{id}") // <1>
	public Mono<ResponseEntity<?>> updateItem( //
			@RequestBody Mono<Item> item, // <2>
			@PathVariable String id) { // <3>

		return item //
				.map(content -> new Item(id, content.getName(), content.getDescription(), //
						content.getPrice())) // <4>
				.flatMap(this.repository::save) // <5>
				.thenReturn(ResponseEntity.created( // <6>
						URI.create("/api/items/" + id)).build());
	}
	// end::replace-item[]
}


<|start_filename|>9-reactive-repository/src/main/java/com/greglturnquist/hackingspringboot/reactive/HackingSpringBootApplication.java<|end_filename|>
package com.greglturnquist.hackingspringboot.reactive;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class HackingSpringBootApplication {

	public static void main(String[] args) {
		SpringApplication.run(HackingSpringBootApplication.class, args);
	}
}


<|start_filename|>2b-reactive/src/test/java/com/greglturnquist/hackingspringboot/reactive/FluentService.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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 com.greglturnquist.hackingspringboot.reactive;

import org.springframework.data.mongodb.core.ReactiveFluentMongoOperations;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;

import static org.springframework.data.mongodb.core.query.Criteria.where;
import static org.springframework.data.mongodb.core.query.Query.query;

import com.greglturnquist.hackingspringboot.reactive.Item;

/**
 * @author <NAME>
 */
@Service
public class FluentService {

	private final ReactiveFluentMongoOperations fluentMongoOperations;

	public FluentService(ReactiveFluentMongoOperations fluentMongoOperations) {
		this.fluentMongoOperations = fluentMongoOperations;
	}

	Flux<Item> searchFluently(String name, String description) {
		return fluentMongoOperations.query(Item.class) //
				.matching(query(where("name").is(name).and("description").is(description))) //
				.all();
	}
}


<|start_filename|>4-reactive/src/test/java/com/greglturnquist/hackingspringboot/reactive/BlockHoundIntegrationTest.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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 com.greglturnquist.hackingspringboot.reactive;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import reactor.test.StepVerifier;

import java.time.Duration;
import java.util.Collections;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;

import com.greglturnquist.hackingspringboot.reactive.AltInventoryService;
import com.greglturnquist.hackingspringboot.reactive.Cart;
import com.greglturnquist.hackingspringboot.reactive.CartItem;
import com.greglturnquist.hackingspringboot.reactive.CartRepository;
import com.greglturnquist.hackingspringboot.reactive.Item;
import com.greglturnquist.hackingspringboot.reactive.ItemRepository;

/**
 * @author <NAME>
 */
// tag::1[]
@ExtendWith(SpringExtension.class) // <1>
public class BlockHoundIntegrationTest {

	AltInventoryService inventoryService; // <2>

	@MockBean ItemRepository itemRepository; // <3>
	@MockBean CartRepository cartRepository;
	// end::1[]

	// tag::2[]
	@BeforeEach
	void setUp() {
		// Define test data <1>

		Item sampleItem = new Item("item1", "TV tray", "Alf TV tray", 19.99);
		CartItem sampleCartItem = new CartItem(sampleItem);
		Cart sampleCart = new Cart("My Cart", Collections.singletonList(sampleCartItem));

		// Define mock interactions provided
		// by your collaborators <2>

		when(cartRepository.findById(anyString())) //
				.thenReturn(Mono.<Cart> empty().hide()); // <3>

		when(itemRepository.findById(anyString())).thenReturn(Mono.just(sampleItem));
		when(cartRepository.save(any(Cart.class))).thenReturn(Mono.just(sampleCart));

		inventoryService = new AltInventoryService(itemRepository, cartRepository);
	}
	// end::2[]

	// tag::3[]
	@Test
	void blockHoundShouldTrapBlockingCall() { //
		Mono.delay(Duration.ofSeconds(1)) // <1>
				.flatMap(tick -> inventoryService.addItemToCart("My Cart", "item1")) // <2>
				.as(StepVerifier::create) // <3>
				.verifyErrorSatisfies(throwable -> { // <4>
					assertThat(throwable).hasMessageContaining( //
							"block()/blockFirst()/blockLast() are blocking");
				});
	}
	// end::3[]
}
// end::code[]


<|start_filename|>9-reactive-repository/src/main/java/com/greglturnquist/hackingspringboot/reactive/HomeController.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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 com.greglturnquist.hackingspringboot.reactive;

import org.springframework.web.bind.annotation.DeleteMapping;
import reactor.core.publisher.Mono;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.reactive.result.view.Rendering;

/**
 * @author <NAME>
 */
@Controller
public class HomeController {

	private final InventoryService inventoryService;

	public HomeController(InventoryService inventoryService) {
		this.inventoryService = inventoryService;
	}

	@GetMapping
	Mono<Rendering> home() {
		return Mono.just(Rendering.view("home.html") // <2>
			.modelAttribute("items", this.inventoryService.getInventory()) // <3>
			.modelAttribute("cart", this.inventoryService.getCart("My Cart") // <4>
				.defaultIfEmpty(new Cart("My Cart")))
			.build());
	}

	@PostMapping("/add/{id}")
	Mono<String> addToCart(@PathVariable String id) {
		return this.inventoryService.addItemToCart("My Cart", id)
			.thenReturn("redirect:/");
	}

	@DeleteMapping("/remove/{id}")
	Mono<String> removeFromCart(@PathVariable String id) {
		return this.inventoryService.removeOneFromCart("My Cart", id)
			.thenReturn("redirect:/");
	}

	@PostMapping
	Mono<String> createItem(@ModelAttribute Item newItem) {
		return this.inventoryService.saveItem(newItem) //
			.thenReturn("redirect:/");
	}

	@DeleteMapping("/delete/{id}")
	Mono<String> deleteItem(@PathVariable String id) {
		return this.inventoryService.deleteItem(id) //
			.thenReturn("redirect:/");
	}
}


<|start_filename|>9-reactive-repository/src/main/java/com/greglturnquist/hackingspringboot/reactive/SecurityConfig.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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 com.greglturnquist.hackingspringboot.reactive;

import java.util.Arrays;

import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;

/**
 * @author <NAME>
 */
// tag::code[]
@Configuration
public class SecurityConfig {
	// end::code[]

	// tag::reactive-user-details[]
	@Bean
	public ReactiveUserDetailsService userDetailsService(UserRepository repository) { // <1>
		return username -> repository.findByName(username) // <2>
				.map(user -> User.withDefaultPasswordEncoder() // <3>
						.username(user.getName()) //
						.password(<PASSWORD>()) //
						.authorities(user.getRoles().toArray(new String[0])) //
						.build()); // <4>
	}
	// end::reactive-user-details[]

	// tag::users[]
	@Bean
	CommandLineRunner userLoader(MongoOperations operations) {
		return args -> {
			operations.save(new com.greglturnquist.hackingspringboot.reactive.User( //
					"greg", "password", Arrays.asList("ROLE_USER")));
		};
	}
	// end::users[]
}


<|start_filename|>6-reactive/src/test/java/com/greglturnquist/hackingspringboot/reactive/ApiItemControllerDocumentationTest.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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 com.greglturnquist.hackingspringboot.reactive;

import static org.mockito.Mockito.*;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.*;

import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.reactive.server.WebTestClient;

/**
 * @author <NAME>
 */
// tag::intro[]
@WebFluxTest(controllers = ApiItemController.class) // <1>
@AutoConfigureRestDocs // <2>
public class ApiItemControllerDocumentationTest {

	@Autowired private WebTestClient webTestClient; // <3>

	@MockBean InventoryService service; // <4>

	@MockBean ItemRepository repository; // <5>
	// end::intro[]

	// tag::test1[]
	@Test
	void findingAllItems() {
		when(repository.findAll()).thenReturn( // <1>
				Flux.just(new Item("item-1", "Alf alarm clock", //
						"nothing I really need", 19.99)));

		this.webTestClient.get().uri("/api/items") //
				.exchange() //
				.expectStatus().isOk() //
				.expectBody() //
				.consumeWith(document("findAll", preprocessResponse(prettyPrint()))); // <2>
	}
	// end::test1[]

	// tag::test2[]
	@Test
	void postNewItem() {
		when(repository.save(any())).thenReturn( //
				Mono.just(new Item("1", "Alf alarm clock", "nothing important", 19.99)));

		this.webTestClient.post().uri("/api/items") // <1>
				.bodyValue(new Item("Alf alarm clock", "nothing important", 19.99)) // <2>
				.exchange() //
				.expectStatus().isCreated() // <3>
				.expectBody() //
				.consumeWith(document("post-new-item", preprocessResponse(prettyPrint()))); // <4>
	}
	// end::test2[]

	// tag::test3[]
	@Test
	void findOneItem() {
		when(repository.findById("item-1")).thenReturn( //
				Mono.just(new Item("item-1", "Alf alarm clock", "nothing I really need", 19.99))); // <1>

		this.webTestClient.get().uri("/api/items/item-1") //
				.exchange() //
				.expectStatus().isOk() //
				.expectBody() //
				.consumeWith(document("findOne", preprocessResponse(prettyPrint()))); // <2>
	}
	// end::test3[]

}


<|start_filename|>8-reactive-client/src/main/java/com/greglturnquist/hackingspringboot/reactive/client/RSocketController.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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 com.greglturnquist.hackingspringboot.reactive.client;

import static io.rsocket.metadata.WellKnownMimeType.*;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.http.MediaType.*;

import java.net.URI;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.http.ResponseEntity;
import org.springframework.messaging.rsocket.RSocketRequester;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author <NAME>
 */
// tag::code[]
@RestController // <1>
public class RSocketController {

	private final Mono<RSocketRequester> requester; // <2>

	public RSocketController(RSocketRequester.Builder builder) { // <3>
		this.requester = builder //
				.dataMimeType(APPLICATION_JSON) // <4>
				.metadataMimeType(parseMediaType(MESSAGE_RSOCKET_ROUTING.toString())) // <5>
				.connectTcp("localhost", 7000) // <6>
				.retry(5) // <7>
				.cache(); // <8>
	}
	// end::code[]

	// tag::request-response[]
	@PostMapping("/items/request-response") // <1>
	Mono<ResponseEntity<?>> addNewItemUsingRSocketRequestResponse(@RequestBody Item item) {
		return this.requester //
				.flatMap(rSocketRequester -> rSocketRequester //
						.route("newItems.request-response") // <2>
						.data(item) // <3>
						.retrieveMono(Item.class)) // <4>
				.map(savedItem -> ResponseEntity.created( // <5>
						URI.create("/items/request-response")).body(savedItem));
	}
	// end::request-response[]

	// tag::fire-and-forget[]
	@PostMapping("/items/fire-and-forget")
	Mono<ResponseEntity<?>> addNewItemUsingRSocketFireAndForget(@RequestBody Item item) {
		return this.requester //
				.flatMap(rSocketRequester -> rSocketRequester //
						.route("newItems.fire-and-forget") // <1>
						.data(item) //
						.send()) // <2>
				.then( // <3>
						Mono.just( //
								ResponseEntity.created( //
										URI.create("/items/fire-and-forget")).build()));
	}
	// end::fire-and-forget[]

	// tag::request-stream[]
	@GetMapping(value = "/items", produces = TEXT_EVENT_STREAM_VALUE) // <1>
	Flux<Item> liveUpdates() {
		return this.requester //
				.flatMapMany(rSocketRequester -> rSocketRequester //
						.route("newItems.monitor") // <2>
						.retrieveFlux(Item.class)); // <3>
	}
	// end::request-stream[]
}


<|start_filename|>5-reactive/src/main/java/com/greglturnquist/hackingspringboot/reactive/AltInventoryService.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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 com.greglturnquist.hackingspringboot.reactive;

import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.stream.Collectors;

/**
 * @author <NAME>
 */
// tag::code[]
@Service
class AltInventoryService {

    private ItemRepository itemRepository;

    private CartRepository cartRepository;

    AltInventoryService(ItemRepository repository,
                        CartRepository cartRepository) {
        this.itemRepository = repository;
        this.cartRepository = cartRepository;
    }

    public Mono<Cart> getCart(String cartId) {
        return this.cartRepository.findById(cartId);
    }

    public Flux<Item> getInventory() {
        return this.itemRepository.findAll();
    }

    Mono<Item> saveItem(Item newItem) {
        return this.itemRepository.save(newItem);
    }

    Mono<Void> deleteItem(String id) {
        return this.itemRepository.deleteById(id);
    }

    Mono<Cart> addItemToCart(String cartId, String itemId) {
        Cart myCart = this.cartRepository.findById(cartId)
            .defaultIfEmpty(new Cart(cartId))
            .block();

        return Mono.just(myCart) //
            .flatMap(cart -> cart.getCartItems().stream()
                .filter(cartItem -> cartItem.getItem().getId().equals(itemId))
                .findAny() //
                .map(cartItem -> {
                    cartItem.increment();
                    return Mono.just(cart);
                }) //
                .orElseGet(() -> {
                    return this.itemRepository.findById(itemId) //
                        .map(item -> new CartItem(item)) //
                        .map(cartItem -> {
                            cart.getCartItems().add(cartItem);
                            return cart;
                        });
                }))
            .flatMap(cart -> this.cartRepository.save(cart));
    }

    Mono<Cart> removeOneFromCart(String cartId, String itemId) {
        return this.cartRepository.findById(cartId)
            .defaultIfEmpty(new Cart(cartId))
            .flatMap(cart -> cart.getCartItems().stream()
                .filter(cartItem -> cartItem.getItem().getId().equals(itemId))
                .findAny()
                .map(cartItem -> {
                    cartItem.decrement();
                    return Mono.just(cart);
                }) //
                .orElse(Mono.empty()))
            .map(cart -> new Cart(cart.getId(), cart.getCartItems().stream()
                .filter(cartItem -> cartItem.getQuantity() > 0)
                .collect(Collectors.toList())))
            .flatMap(cart -> this.cartRepository.save(cart));
    }
}
// end::code[]


<|start_filename|>5-reactive/src/main/java/com/greglturnquist/hackingspringboot/reactive/HackingSpringBootApplication.java<|end_filename|>
package com.greglturnquist.hackingspringboot.reactive;

import org.bson.Document;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.actuate.trace.http.HttpTrace;
import org.springframework.boot.actuate.trace.http.HttpTraceRepository;
import org.springframework.boot.actuate.trace.http.InMemoryHttpTraceRepository;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;

import java.net.URI;
import java.util.Collections;
import java.util.Map;

@SpringBootApplication
public class HackingSpringBootApplication {

	public static void main(String[] args) {
		SpringApplication.run(HackingSpringBootApplication.class, args);
	}

	// tag::http-trace[]
	HttpTraceRepository traceRepository() { // <2>
		return new InMemoryHttpTraceRepository(); // <3>
	}
	// end::http-trace[]

	// tag::spring-data-trace[]
	@Bean
	HttpTraceRepository springDataTraceRepository(HttpTraceWrapperRepository repository) {
		return new SpringDataHttpTraceRepository(repository);
	}
	// end::spring-data-trace[]

	// tag::custom-1[]
	static Converter<Document, HttpTraceWrapper> CONVERTER = //
			new Converter<Document, HttpTraceWrapper>() { //
				@Override
				public HttpTraceWrapper convert(Document document) {
					Document httpTrace = document.get("httpTrace", Document.class);
					Document request = httpTrace.get("request", Document.class);
					Document response = httpTrace.get("response", Document.class);

					return new HttpTraceWrapper(new HttpTrace( //
							new HttpTrace.Request( //
									request.getString("method"), //
									URI.create(request.getString("uri")), //
									request.get("headers", Map.class), //
									null),
							new HttpTrace.Response( //
									response.getInteger("status"), //
									response.get("headers", Map.class)),
							httpTrace.getDate("timestamp").toInstant(), //
							null, //
							null, //
							httpTrace.getLong("timeTaken")));
				}
			};
	// end::custom-1[]

	// tag::custom-2[]
	@Bean
	public MappingMongoConverter mappingMongoConverter(MongoMappingContext context) {

		MappingMongoConverter mappingConverter = //
				new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, context); // <1>

		mappingConverter.setCustomConversions( // <2>
				new MongoCustomConversions(Collections.singletonList(CONVERTER))); // <3>

		return mappingConverter;
	}
	// end::custom-2[]

}


<|start_filename|>8-reactive-server/src/main/java/com/greglturnquist/hackingspringboot/reactive/server/HackingSpringBootRSocketServerApplication.java<|end_filename|>
package com.greglturnquist.hackingspringboot.reactive.server;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class HackingSpringBootRSocketServerApplication {

	public static void main(String[] args) {
		SpringApplication.run(HackingSpringBootRSocketServerApplication.class, args);
	}
}


<|start_filename|>6-reactive/src/test/java/com/greglturnquist/hackingspringboot/reactive/AffordancesItemControllerDocumentationTest.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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 com.greglturnquist.hackingspringboot.reactive;

import static org.mockito.Mockito.*;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.*;

import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.hateoas.MediaTypes;
import org.springframework.test.web.reactive.server.WebTestClient;

import com.greglturnquist.hackingspringboot.reactive.AffordancesItemController;
import com.greglturnquist.hackingspringboot.reactive.InventoryService;
import com.greglturnquist.hackingspringboot.reactive.Item;
import com.greglturnquist.hackingspringboot.reactive.ItemRepository;

/**
 * @author <NAME>
 */
// tag::intro[]
@WebFluxTest(controllers = AffordancesItemController.class) // <1>
@AutoConfigureRestDocs // <2>
public class AffordancesItemControllerDocumentationTest {

	@Autowired private WebTestClient webTestClient; // <3>

	@MockBean InventoryService service; // <4>

	@MockBean ItemRepository repository; // <5>
	// end::intro[]

	// tag::affordances[]
	@Test
	void findSingleItemAffordances() {
		when(repository.findById("item-1")).thenReturn(Mono.just( //
				new Item("item-1", "Alf alarm clock", "nothing I really need", 19.99)));

		this.webTestClient.get().uri("/affordances/items/item-1") // <1>
				.accept(MediaTypes.HAL_FORMS_JSON) // <2>
				.exchange() //
				.expectStatus().isOk() //
				.expectBody() //
				.consumeWith(document("single-item-affordances", //
						preprocessResponse(prettyPrint()))); // <3>
	}
	// end::affordances[]

	// tag::affordances2[]
	@Test
	void findAggregateRootItemAffordances() {
		when(repository.findAll()).thenReturn(Flux.just( //
				new Item("Alf alarm clock", "nothing I really need", 19.99)));
		when(repository.findById((String) null)).thenReturn(Mono.just( //
				new Item("item-1", "Alf alarm clock", "nothing I really need", 19.99)));

		this.webTestClient.get().uri("/affordances/items") // <1>
				.accept(MediaTypes.HAL_FORMS_JSON) // <2>
				.exchange() //
				.expectStatus().isOk() //
				.expectBody() //
				.consumeWith(document("aggregate-root-affordances", preprocessResponse(prettyPrint()))); // <3>
	}
	// end::affordances2[]

}


<|start_filename|>6-reactive/src/main/java/com/greglturnquist/hackingspringboot/reactive/ItemRepository.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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 com.greglturnquist.hackingspringboot.reactive;

import org.springframework.data.mongodb.repository.Query;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Flux;

// tag::code[]
public interface ItemRepository extends ReactiveCrudRepository<Item, String> {

    Flux<Item> findByNameContaining(String partialName);
    // end::code[]

    // tag::code-2[]
    @Query("{ 'name' : ?0, 'age' :  }")
    Flux<Item> findItemsForCustomerMonthlyReport();

    @Query(sort = "{ 'age' : -1", value = "{ 'name' : 'TV tray', 'age' : }")
    Flux<Item> findSortedStuffForWeeklyReport();
    // end::code-2[]

    // tag::code-3[]
    // search by name
    Flux<Item> findByNameContainingIgnoreCase(String partialName);

    // search by description
    Flux<Item> findByDescriptionContainingIgnoreCase(String partialName);

    // search by name AND description
    Flux<Item> findByNameContainingAndDescriptionContainingAllIgnoreCase(String partialName, String partialDesc);

    // search by name OR description
    Flux<Item> findByNameContainingOrDescriptionContainingAllIgnoreCase(String partialName, String partialDesc);
    // end::code-3[]
}


<|start_filename|>9-reactive-repository/src/main/java/com/greglturnquist/hackingspringboot/reactive/Cart.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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 com.greglturnquist.hackingspringboot.reactive;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import org.springframework.data.annotation.Id;

/**
 * @author <NAME>
 */
// tag::code[]
class Cart {

	private @Id String id;
	private List<CartItem> cartItems;

	private Cart() {}

	public Cart(String id) {
		this(id, new ArrayList<>());
	}

	public Cart(String id, List<CartItem> cartItems) {
		this.id = id;
		this.cartItems = cartItems;
	}
	// end::code[]

	public String getId() {
		return id;
	}

	public void setId(String id) {
		this.id = id;
	}

	public List<CartItem> getCartItems() {
		return cartItems;
	}

	public void setCartItems(List<CartItem> cartItems) {
		this.cartItems = cartItems;
	}

	@Override
	public boolean equals(Object o) {
		if (this == o)
			return true;
		if (o == null || getClass() != o.getClass())
			return false;
		Cart cart = (Cart) o;
		return Objects.equals(id, cart.id) && Objects.equals(cartItems, cart.cartItems);
	}

	@Override
	public int hashCode() {
		return Objects.hash(id, cartItems);
	}

	@Override
	public String toString() {
		return "Cart{" + "id='" + id + '\'' + ", cartItems=" + cartItems + '}';
	}
}


<|start_filename|>6-reactive/src/main/java/com/greglturnquist/hackingspringboot/reactive/AffordancesItemController.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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 com.greglturnquist.hackingspringboot.reactive;

import static org.springframework.hateoas.mediatype.alps.Alps.*;
import static org.springframework.hateoas.server.reactive.WebFluxLinkBuilder.*;

import java.util.Arrays;
import java.util.Collections;
import java.util.stream.Collectors;

import reactor.core.publisher.Mono;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Links;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.RepresentationModel;
import org.springframework.hateoas.mediatype.alps.Alps;
import org.springframework.hateoas.mediatype.alps.Type;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author <NAME>
 */
// tag::intro[]
@RestController
public class AffordancesItemController {

	private final ItemRepository repository;

	public AffordancesItemController(ItemRepository repository) {
		this.repository = repository;
	}
	// end::intro[]

	// tag::root[]
	@GetMapping("/affordances")
	Mono<RepresentationModel<?>> root() {
		AffordancesItemController controller = methodOn(AffordancesItemController.class);

		Mono<Link> selfLink = linkTo(controller.root()) //
				.withSelfRel() //
				.toMono();

		Mono<Link> itemsAggregateLink = linkTo(controller.findAll()) //
				.withRel(IanaLinkRelations.ITEM) //
				.toMono();

		return selfLink.zipWith(itemsAggregateLink) //
				.map(links -> Links.of(links.getT1(), links.getT2())) //
				.map(links -> new RepresentationModel<>(links.toList()));
	}
	// end::root[]

	// tag::find-all[]
	@GetMapping("/affordances/items")
	Mono<CollectionModel<EntityModel<Item>>> findAll() {
		AffordancesItemController controller = methodOn(AffordancesItemController.class);

		Mono<Link> aggregateRoot = linkTo(controller.findAll()) //
				.withSelfRel() //
				.andAffordance(controller.addNewItem(null)) // <1>
				.toMono();

		return this.repository.findAll() // <2>
				.flatMap(item -> findOne(item.getId())) // <3>
				.collectList() // <4>
				.flatMap(models -> aggregateRoot //
						.map(selfLink -> CollectionModel.of( //
								models, selfLink))); // <5>
	}
	// end::find-all[]

	// tag::find-one[]
	@GetMapping("/affordances/items/{id}") // <1>
	Mono<EntityModel<Item>> findOne(@PathVariable String id) {
		AffordancesItemController controller = methodOn(AffordancesItemController.class); // <2>

		Mono<Link> selfLink = linkTo(controller.findOne(id)) //
				.withSelfRel() //
				.andAffordance(controller.updateItem(null, id)) // <3>
				.toMono();

		Mono<Link> aggregateLink = linkTo(controller.findAll()) //
				.withRel(IanaLinkRelations.ITEM) //
				.toMono();

		return Mono.zip(repository.findById(id), selfLink, aggregateLink) //
				.map(o -> EntityModel.of(o.getT1(), Links.of(o.getT2(), o.getT3())));
	}
	// end::find-one[]

	// tag::add-new-item[]
	@PostMapping("/affordances/items") // <1>
	Mono<ResponseEntity<?>> addNewItem(@RequestBody Mono<EntityModel<Item>> item) { // <2>
		return item //
				.map(EntityModel::getContent) // <3>
				.flatMap(this.repository::save) // <4>
				.map(Item::getId) // <5>
				.flatMap(this::findOne) // <6>
				.map(newModel -> ResponseEntity.created(newModel // <7>
						.getRequiredLink(IanaLinkRelations.SELF) //
						.toUri()).body(newModel.getContent()));
	}
	// end::add-new-item[]

	// tag::update-item[]
	@PutMapping("/affordances/items/{id}") // <1>
	public Mono<ResponseEntity<?>> updateItem(@RequestBody Mono<EntityModel<Item>> item, // <2>
			@PathVariable String id) {
		return item //
				.map(EntityModel::getContent) //
				.map(content -> new Item(id, content.getName(), // <3>
						content.getDescription(), content.getPrice())) //
				.flatMap(this.repository::save) // <4>
				.then(findOne(id)) // <5>
				.map(model -> ResponseEntity.noContent() // <6>
						.location(model.getRequiredLink(IanaLinkRelations.SELF).toUri()).build());
	}
	// end::update-item[]

	// tag::profile[]
	@GetMapping(value = "/affordances/items/profile", produces = MediaTypes.ALPS_JSON_VALUE)
	public Alps profile() {
		return alps() //
				.descriptor(Collections.singletonList(descriptor() //
						.id(Item.class.getSimpleName() + "-representation") //
						.descriptor( //
								Arrays.stream(Item.class.getDeclaredFields()) //
										.map(field -> descriptor() //
												.name(field.getName()) //
												.type(Type.SEMANTIC) //
												.build()) //
										.collect(Collectors.toList())) //
						.build())) //
				.build();
	}
	// end::profile[]
}


<|start_filename|>1-reactive/src/test/java/com/greglturnquist/hackingspringboot/reactive/DishMetaphor.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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 com.greglturnquist.hackingspringboot.reactive;

import reactor.core.publisher.Flux;

/**
 * @author <NAME>
 */
public class DishMetaphor {

	static
	// tag::service[]
	class KitchenService {

		Flux<Dish> getDishes() {
			// You could model a ChefService, but let's just
			// hard code some tasty dishes.
			return Flux.just( //
					new Dish("Sesame chicken"), //
					new Dish("Lo mein noodles, plain"), //
					new Dish("Sweet & sour beef"));
		}
	}
	// end::service[]

	// tag::simple-server[]
	class SimpleServer {

		private final KitchenService kitchen;

		SimpleServer(KitchenService kitchen) {
			this.kitchen = kitchen;
		}

		Flux<Dish> doingMyJob() {
			return this.kitchen.getDishes() //
					.map(dish -> Dish.deliver(dish));
		}

	}
	// end::simple-server[]

	static
	// tag::polite-server[]
	class PoliteServer {

		private final KitchenService kitchen;

		PoliteServer(KitchenService kitchen) {
			this.kitchen = kitchen;
		}

		Flux<Dish> doingMyJob() {
			return this.kitchen.getDishes() //
					.doOnNext(dish -> System.out.println("Thank you for " + dish + "!")) //
					.doOnError(error -> System.out.println("So sorry about " //
							+ error.getMessage())) //
					.doOnComplete(() -> System.out.println("Thanks for all your hard work!")) //
					.map(Dish::deliver);
		}

	}
	// end::polite-server[]

	class BusyServer {

		private final KitchenService kitchen;

		BusyServer(KitchenService kitchen) {
			this.kitchen = kitchen;
		}

		Flux<Dish> doingMyJob() {
			// tag::multiple-side-effects[]
			return this.kitchen.getDishes() //
					.doOnNext(dish -> {
						System.out.println("Thank you for " + dish + "!");
						System.out.println("Marking the ticket as done.");
						System.out.println("Grabbing some silverware.");
					}) //
					.map(this::deliver);
			// end::multiple-side-effects[]
		}

		Dish deliver(Dish dish) {
			return Dish.deliver(dish);
		}
	}

	class BusyServer2 {

		private final KitchenService kitchen;

		BusyServer2(KitchenService kitchen) {
			this.kitchen = kitchen;
		}

		Flux<Dish> doingMyJob() {
			// tag::multiple-side-effects2[]
			return this.kitchen.getDishes() //
					.doOnNext( //
							dish -> System.out.println("Thank you for " + dish + "!")) //
					.doOnNext( //
							dish -> System.out.println("Marking the ticket as done.")) //
					.doOnNext(dish -> System.out.println("Grabbing some silverware.")) //
					.map(this::deliver);
			// end::multiple-side-effects2[]
		}

		Dish deliver(Dish dish) {
			return Dish.deliver(dish);
		}
	}

	static
	// tag::dish[]
	class Dish {
		private String description;
		private boolean delivered = false;

		public static Dish deliver(Dish dish) {
			Dish deliveredDish = new Dish(dish.description);
			deliveredDish.delivered = true;
			return deliveredDish;
		}

		Dish(String description) {
			this.description = description;
		}

		public boolean isDelivered() {
			return delivered;
		}

		@Override
		public String toString() {
			return "Dish{" + //
					"description='" + description + '\'' + //
					", delivered=" + delivered + '}';
		}
	}
	// end::dish[]

	static
	// tag::example[]
	class PoliteRestaurant {

		public static void main(String... args) {
			PoliteServer server = //
					new PoliteServer(new KitchenService());

			server.doingMyJob().subscribe( //
					dish -> System.out.println("Consuming " + dish), //
					throwable -> System.err.println(throwable));
		}
	}
	// end::example[]

}


<|start_filename|>6-reactive/src/test/java/com/greglturnquist/hackingspringboot/reactive/HypermediaItemControllerDocumentationTest.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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 com.greglturnquist.hackingspringboot.reactive;

import static org.mockito.Mockito.*;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.*;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.*;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.reactive.server.WebTestClient;

/**
 * @author <NAME>
 */
// tag::intro[]
@WebFluxTest(controllers = HypermediaItemController.class)
@AutoConfigureRestDocs
public class HypermediaItemControllerDocumentationTest {

	@Autowired private WebTestClient webTestClient;

	@MockBean InventoryService service;

	@MockBean ItemRepository repository;
	// end::intro[]

	// tag::test1[]
	@Test
	void findingAllItems() {
		when(repository.findAll()) //
				.thenReturn(Flux.just( //
						new Item("Alf alarm clock", //
								"nothing I really need", 19.99)));
		when(repository.findById((String) null)) //
				.thenReturn(Mono.just( //
						new Item("item-1", "Alf alarm clock", //
								"nothing I really need", 19.99)));

		this.webTestClient.get().uri("/hypermedia/items") //
				.exchange() //
				.expectStatus().isOk() //
				.expectBody() //
				.consumeWith(document("findAll-hypermedia", //
						preprocessResponse(prettyPrint()))); //
	}
	// end::test1[]

	// tag::test2[]
	// @Test
	void postNewItem() {
		this.webTestClient.post().uri("/hypermedia/items") //
				.body(Mono.just( //
						new Item("item-1", "Alf alarm clock", //
								"nothing I really need", 19.99)),
						Item.class) //
				.exchange() //
				.expectStatus().isCreated() //
				.expectBody().isEmpty();
	}
	// end::test2[]

	// tag::test3[]
	@Test
	void findOneItem() {
		when(repository.findById("item-1")).thenReturn(Mono.just( //
				new Item("item-1", "Alf alarm clock", "nothing I really need", 19.99)));

		this.webTestClient.get().uri("/hypermedia/items/item-1") //
				.exchange() //
				.expectStatus().isOk() //
				.expectBody() //
				.consumeWith(document("findOne-hypermedia", preprocessResponse(prettyPrint()), //
						links( // <1>
								linkWithRel("self").description("Canonical link to this `Item`"), // <2>
								linkWithRel("item").description("Link back to the aggregate root")))); // <3>
	}
	// end::test3[]

	@Test
	void findProfile() {
		this.webTestClient.get().uri("/hypermedia/items/profile") //
				.exchange() //
				.expectStatus().isOk() //
				.expectBody() //
				.consumeWith(document("profile", //
						preprocessResponse(prettyPrint())));
	}
}


<|start_filename|>7-reactive/src/main/java/com/greglturnquist/hackingspringboot/reactive/SpringAmqpItemService.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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 com.greglturnquist.hackingspringboot.reactive;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;

/**
 * @author <NAME>
 */
// tag::code[]
@Service // <1>
public class SpringAmqpItemService {

	private static final Logger log = //
			LoggerFactory.getLogger(SpringAmqpItemService.class);

	private final ItemRepository repository; // <2>

	public SpringAmqpItemService(ItemRepository repository) {
		this.repository = repository;
	}
	// end::code[]

	// tag::listener[]
	@RabbitListener( // <1>
			ackMode = "MANUAL", //
			bindings = @QueueBinding( // <2>
					value = @Queue, // <3>
					exchange = @Exchange("hacking-spring-boot"), // <4>
					key = "new-items-spring-amqp")) // <5>
	public Mono<Void> processNewItemsViaSpringAmqp(Item item) { // <6>
		log.debug("Consuming => " + item);
		return this.repository.save(item).then(); // <7>
	}
	// end::listener[]
}


<|start_filename|>8-reactive-client/src/main/java/com/greglturnquist/hackingspringboot/reactive/client/Item.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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 com.greglturnquist.hackingspringboot.reactive.client;

import java.util.Objects;

import org.springframework.data.annotation.Id;

// tag::code[]
public class Item {

	private @Id String id;
	private String name;
	private String description;
	private double price;
	// end::code[]

	private Item() {}

	Item(String name, String description, double price) {
		this.name = name;
		this.description = description;
		this.price = price;
	}

	Item(String id, String name, String description, double price) {
		this(name, description, price);
		this.id = id;
	}

	public String getId() {
		return id;
	}

	public void setId(String id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getDescription() {
		return description;
	}

	public void setDescription(String description) {
		this.description = description;
	}

	public double getPrice() {
		return price;
	}

	public void setPrice(double price) {
		this.price = price;
	}

	@Override
	public boolean equals(Object o) {
		if (this == o) {
			return true;
		}
		if (o == null || getClass() != o.getClass()) {
			return false;
		}
		Item item = (Item) o;
		return Double.compare(item.price, price) == 0 && Objects.equals(id, item.id) && Objects.equals(name, item.name)
				&& Objects.equals(description, item.description);
	}

	@Override
	public int hashCode() {
		return Objects.hash(id, name, description, price);
	}

	@Override
	public String toString() {
		return "Item{" + "id='" + id + '\'' + ", name='" + name + '\'' + ", description='" + description + '\'' + ", price="
				+ price + '}';
	}

}


<|start_filename|>7-reactive/src/main/java/com/greglturnquist/hackingspringboot/reactive/SpringAmqpItemController.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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 com.greglturnquist.hackingspringboot.reactive;

import java.net.URI;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author <NAME>
 */
// tag::code[]
@RestController // <1>
public class SpringAmqpItemController {

	private static final Logger log = //
			LoggerFactory.getLogger(SpringAmqpItemController.class);

	private final AmqpTemplate template; // <2>

	public SpringAmqpItemController(AmqpTemplate template) {
		this.template = template;
	}
	// end::code[]

	// tag::post[]
	@PostMapping("/items") // <1>
	Mono<ResponseEntity<?>> addNewItemUsingSpringAmqp(@RequestBody Mono<Item> item) { // <2>
		return item //
				.publishOn(Schedulers.boundedElastic())// <3>
				.flatMap(content -> { //
					return Mono //
							.fromCallable(() -> { // <4>
								this.template.convertAndSend( // <5>
										"hacking-spring-boot", "new-items-spring-amqp", content);
								return ResponseEntity.created(URI.create("/items")).build(); // <6>
							});
				});
	}
	// end::post[]
}


<|start_filename|>3-reactive/src/test/java/com/greglturnquist/hackingspringboot/reactive/LoggingReactorFlows.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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 com.greglturnquist.hackingspringboot.reactive;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;

/**
 * @author <NAME>
 */
class LoggingReactorFlows {

    private static final Logger log = LoggerFactory.getLogger(LoggingReactorFlows.class);

    private ItemRepository itemRepository;

    LoggingReactorFlows(ItemRepository itemRepository) {
        this.itemRepository = itemRepository;
    }

    Mono<Double> findPriceByMethodReference(String id) {
        // tag::method-handle[]
        return itemRepository.findById(id)
            .map(Item::getPrice);
        // end::method-handle[]
    }

    Mono<Double> findPriceWithLogging(String id) {
        // tag::injected-logger[]
        return itemRepository.findById(id)
            .map(item -> {
                log.debug("Found item");
                return item.getPrice();
            });
        // end::injected-logger[]
    }

    Mono<Double> findPriceWithReactorLogging(String id) {
        // tag::reactor-logging[]
        return itemRepository.findById(id)
            .log("Found item")
            .map(Item::getPrice);
        // end::reactor-logging[]
    }
}


<|start_filename|>4-reactive/src/test/java/com/greglturnquist/hackingspringboot/reactive/MongoDbSliceTest.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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 com.greglturnquist.hackingspringboot.reactive;

import static org.assertj.core.api.Assertions.*;

import org.junit.jupiter.api.Test;
import reactor.test.StepVerifier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest;

/**
 * @author <NAME>
 */
// tag::code[]
@DataMongoTest // <1>
public class MongoDbSliceTest {

	@Autowired ItemRepository repository; // <2>

	@Test // <3>
	void itemRepositorySavesItems() {
		Item sampleItem = new Item( //
				"name", "description", 1.99);

		repository.save(sampleItem) //
				.as(StepVerifier::create) //
				.expectNextMatches(item -> {
					assertThat(item.getId()).isNotNull();
					assertThat(item.getName()).isEqualTo("name");
					assertThat(item.getDescription()).isEqualTo("description");
					assertThat(item.getPrice()).isEqualTo(1.99);

					return true;
				}) //
				.verifyComplete();
	}
}
// end::code[]


<|start_filename|>4-reactive/src/test/java/com/greglturnquist/hackingspringboot/reactive/BlockHoundUnitTest.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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 com.greglturnquist.hackingspringboot.reactive;

import static org.assertj.core.api.Assertions.*;

import java.time.Duration;

import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

/**
 * @author <NAME>
 */
class BlockHoundUnitTest {

	// tag::obvious-failure[]
	@Test
	void threadSleepIsABlockingCall() {
		Mono.delay(Duration.ofSeconds(1)) // <1>
				.flatMap(tick -> {
					try {
						Thread.sleep(10); // <2>
						return Mono.just(true);
					} catch (InterruptedException e) {
						return Mono.error(e);
					}
				}) //
				.as(StepVerifier::create) //
				.verifyErrorMatches(throwable -> {
					assertThat(throwable.getMessage()) //
							.contains("Blocking call! java.lang.Thread.sleep");
					return true;
				});
	}
	// end::obvious-failure[]

}


<|start_filename|>2b-reactive/src/main/java/com/greglturnquist/hackingspringboot/reactive/HomeController.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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 com.greglturnquist.hackingspringboot.reactive;

import org.springframework.web.bind.annotation.DeleteMapping;
import reactor.core.publisher.Mono;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.reactive.result.view.Rendering;

/**
 * @author <NAME>
 */
// tag::1[]
@Controller // <1>
public class HomeController {

	private ItemRepository itemRepository;
	private CartRepository cartRepository;
	private InventoryService inventoryService;

	public HomeController(ItemRepository itemRepository, CartRepository cartRepository,
			InventoryService inventoryService) {
		this.itemRepository = itemRepository;
		this.cartRepository = cartRepository;
		this.inventoryService = inventoryService;
	}
	// end::1[]

	// tag::2[]
	@GetMapping
	Mono<Rendering> home() { // <1>
		return Mono.just(Rendering.view("home.html") // <2>
				.modelAttribute("items", //
						this.itemRepository.findAll()) // <3>
				.modelAttribute("cart", //
						this.cartRepository.findById("My Cart") // <4>
								.defaultIfEmpty(new Cart("My Cart")))
				.build());
	}
	// end::2[]

	// tag::3[]
	@PostMapping("/add/{id}")
	// <1>
	Mono<String> addToCart(@PathVariable String id) { // <2>
		return this.cartRepository.findById("My Cart") // <3>
				.defaultIfEmpty(new Cart("My Cart")) //
				.flatMap(cart -> cart.getCartItems().stream() // <4>
						.filter(cartItem -> cartItem.getItem() //
								.getId().equals(id)) //
						.findAny() //
						.map(cartItem -> {
							cartItem.increment();
							return Mono.just(cart);
						}).orElseGet(() -> { // <5>
							return this.itemRepository.findById(id) //
									.map(item -> new CartItem(item)) //
									.map(cartItem -> {
										cart.getCartItems().add(cartItem);
										return cart;
									});
						}))
				.flatMap(cart -> this.cartRepository.save(cart)) // <6>
				.thenReturn("redirect:/"); // <7>
	}
	// end::3[]

	@PostMapping
	Mono<String> createItem(@ModelAttribute Item newItem) {
		return this.itemRepository.save(newItem) //
				.thenReturn("redirect:/");
	}

	@DeleteMapping("/delete/{id}")
	Mono<String> deleteItem(@PathVariable String id) {
		return this.itemRepository.deleteById(id) //
				.thenReturn("redirect:/");
	}

	// tag::search[]
	@GetMapping("/search") // <1>
	Mono<Rendering> search( //
			@RequestParam(required = false) String name, // <2>
			@RequestParam(required = false) String description, //
			@RequestParam boolean useAnd) {
		return Mono.just(Rendering.view("home.html") // <3>
				.modelAttribute("results", //
						inventoryService.searchByExample(name, description, useAnd)) // <4>
				.build());
	}
	// end::search[]
}


<|start_filename|>4-reactive/src/test/java/com/greglturnquist/hackingspringboot/reactive/InventoryServiceUnitTest.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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 com.greglturnquist.hackingspringboot.reactive;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

import java.util.Collections;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;

import com.greglturnquist.hackingspringboot.reactive.Cart;
import com.greglturnquist.hackingspringboot.reactive.CartItem;
import com.greglturnquist.hackingspringboot.reactive.CartRepository;
import com.greglturnquist.hackingspringboot.reactive.InventoryService;
import com.greglturnquist.hackingspringboot.reactive.Item;
import com.greglturnquist.hackingspringboot.reactive.ItemRepository;

/**
 * @author <NAME>
 */
// tag::extend[]
@ExtendWith(SpringExtension.class) // <1>
class InventoryServiceUnitTest { // <2>
	// end::extend[]

	// tag::class-under-test[]
	InventoryService inventoryService; // <1>

	@MockBean private ItemRepository itemRepository; // <2>

	@MockBean private CartRepository cartRepository; // <2>
	// end::class-under-test[]

	// tag::before[]
	@BeforeEach // <1>
	void setUp() {
		// Define test data <2>
		Item sampleItem = new Item("item1", "TV tray", "Alf TV tray", 19.99);
		CartItem sampleCartItem = new CartItem(sampleItem);
		Cart sampleCart = new Cart("My Cart", Collections.singletonList(sampleCartItem));

		// Define mock interactions provided
		// by your collaborators <3>
		when(cartRepository.findById(anyString())).thenReturn(Mono.empty());
		when(itemRepository.findById(anyString())).thenReturn(Mono.just(sampleItem));
		when(cartRepository.save(any(Cart.class))).thenReturn(Mono.just(sampleCart));

		inventoryService = new InventoryService(itemRepository, cartRepository); // <4>
	}
	// end::before[]

	// tag::test[]
	@Test
	void addItemToEmptyCartShouldProduceOneCartItem() { // <1>
		inventoryService.addItemToCart("My Cart", "item1") // <2>
				.as(StepVerifier::create) // <3>
				.expectNextMatches(cart -> { // <4>
					assertThat(cart.getCartItems()).extracting(CartItem::getQuantity) //
							.containsExactlyInAnyOrder(1); // <5>

					assertThat(cart.getCartItems()).extracting(CartItem::getItem) //
							.containsExactly(new Item("item1", "TV tray", "Alf TV tray", 19.99)); // <6>

					return true; // <7>
				}) //
				.verifyComplete(); // <8>
	}
	// end::test[]

	// tag::test2[]
	@Test
	void alternativeWayToTest() { // <1>
		StepVerifier.create( //
				inventoryService.addItemToCart("My Cart", "item1")) //
				.expectNextMatches(cart -> { // <4>
					assertThat(cart.getCartItems()).extracting(CartItem::getQuantity) //
							.containsExactlyInAnyOrder(1); // <5>

					assertThat(cart.getCartItems()).extracting(CartItem::getItem) //
							.containsExactly(new Item("item1", "TV tray", "Alf TV tray", 19.99)); // <6>

					return true; // <7>
				}) //
				.verifyComplete(); // <8>
	}
	// end::test2[]

}


<|start_filename|>7-reactive/src/main/java/com/greglturnquist/hackingspringboot/reactive/HackingSpringBootApplication.java<|end_filename|>
package com.greglturnquist.hackingspringboot.reactive;

import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class HackingSpringBootApplication {

	public static void main(String[] args) {
		SpringApplication.run(HackingSpringBootApplication.class, args);
	}

	// tag::jackson[]
	@Bean
	Jackson2JsonMessageConverter jackson2JsonMessageConverter() {
		return new Jackson2JsonMessageConverter();
	}
	// end::jackson[]
}


<|start_filename|>3-reactive/src/main/java/com/greglturnquist/hackingspringboot/reactive/HackingSpringBootApplicationPlainBlockHound.java<|end_filename|>
package com.greglturnquist.hackingspringboot.reactive;

import reactor.blockhound.BlockHound;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class HackingSpringBootApplicationPlainBlockHound {

	// tag::blockhound[]
	public static void main(String[] args) {
		BlockHound.install();

		SpringApplication.run(HackingSpringBootApplicationPlainBlockHound.class, args);
	}
	// end::blockhound[]
}


<|start_filename|>9-reactive-custom-config/src/main/java/com/greglturnquist/hackingspringboot/reactive/HomeController.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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 com.greglturnquist.hackingspringboot.reactive;

import reactor.core.publisher.Mono;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.reactive.result.view.Rendering;

/**
 * @author <NAME>
 */
@Controller
public class HomeController {

	private final InventoryService inventoryService;

	public HomeController(InventoryService inventoryService) {
		this.inventoryService = inventoryService;
	}

	// tag::user-cart[]
	@GetMapping
	Mono<Rendering> home(Authentication auth) { // <1>
		return Mono.just(Rendering.view("home.html") //
				.modelAttribute("items", this.inventoryService.getInventory()) //
				.modelAttribute("cart", this.inventoryService.getCart(cartName(auth)) // <2>
						.defaultIfEmpty(new Cart(cartName(auth)))) //
				.modelAttribute("auth", auth) // <3>
				.build());
	}
	// end::user-cart[]

	// tag::adjust-cart[]
	@PostMapping("/add/{id}")
	Mono<String> addToCart(Authentication auth, @PathVariable String id) {
		return this.inventoryService.addItemToCart(cartName(auth), id) //
				.thenReturn("redirect:/");
	}

	@DeleteMapping("/remove/{id}")
	Mono<String> removeFromCart(Authentication auth, @PathVariable String id) {
		return this.inventoryService.removeOneFromCart(cartName(auth), id) //
				.thenReturn("redirect:/");
	}
	// end::adjust-cart[]

	// tag::inventory[]
	@PostMapping
	@ResponseBody
	Mono<Item> createItem(@RequestBody Item newItem) {
		return this.inventoryService.saveItem(newItem);
	}

	@DeleteMapping("/{id}")
	@ResponseBody
	Mono<Void> deleteItem(@PathVariable String id) {
		return this.inventoryService.deleteItem(id);
	}
	// end::inventory[]

	// tag::cartName[]
	private static String cartName(Authentication auth) {
		return auth.getName() + "'s Cart";
	}
	// end::cartName[]
}


<|start_filename|>9-reactive-repository/src/test/java/com/greglturnquist/hackingspringboot/reactive/HomeControllerTest.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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 com.greglturnquist.hackingspringboot.reactive;

import static org.mockito.Mockito.*;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.reactive.server.WebTestClient;

/**
 * @author <NAME>
 */
@WebFluxTest(controllers = HomeController.class)
public class HomeControllerTest {

	@Autowired private WebTestClient webTestClient;

	@MockBean InventoryService service;

	@Test
	void verifyLoginPageBlocksAccess() {
		this.webTestClient.get().uri("/") //
				.exchange() //
				.expectStatus().isUnauthorized();
	}

	@Test
	@WithMockUser(username = "ada")
	void verifyLoginPageWorks() {
		when(this.service.getInventory()).thenReturn(Flux.just( //
				new Item("1", "Alf alarm clock", "kids clock", 19.99), //
				new Item("2", "Smurf TV tray", "kids TV tray", 24.99)));
		
		when(this.service.getCart(any())).thenReturn(Mono.just(new Cart("Test Cart")));

		this.webTestClient.get().uri("/") //
				.exchange() //
				.expectStatus().isOk();
	}
}


<|start_filename|>7-reactive/src/test/java/com/greglturnquist/hackingspringboot/reactive/RabbitTest.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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 com.greglturnquist.hackingspringboot.reactive;

import static org.assertj.core.api.Assertions.*;

import reactor.test.StepVerifier;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.testcontainers.containers.RabbitMQContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

/**
 * @author <NAME>
 */
// tag::setup[]
@SpringBootTest // <1>
@AutoConfigureWebTestClient // <2>
@Testcontainers // <3>
@ContextConfiguration // <4>
public class RabbitTest {

	@Container static RabbitMQContainer container = new RabbitMQContainer(); // <5>

	@Autowired WebTestClient webTestClient; // <6>

	@Autowired ItemRepository repository; // <7>

	@DynamicPropertySource // <8>
	static void configure(DynamicPropertyRegistry registry) {
		registry.add("spring.rabbitmq.host", container::getContainerIpAddress);
		registry.add("spring.rabbitmq.port", container::getAmqpPort);
	}
	// end::setup[]

	// tag::spring-amqp-test[]
	@Test
	void verifyMessagingThroughAmqp() throws InterruptedException {
		this.webTestClient.post().uri("/items") // <1>
				.bodyValue(new Item("Alf alarm clock", "nothing important", 19.99)) //
				.exchange() //
				.expectStatus().isCreated() //
				.expectBody();

		Thread.sleep(1500L); // <2>

		this.webTestClient.post().uri("/items") // <3>
				.bodyValue(new Item("Smurf TV tray", "nothing important", 29.99)) //
				.exchange() //
				.expectStatus().isCreated() //
				.expectBody();

		Thread.sleep(2000L); // <4>

		this.repository.findAll() // <5>
				.as(StepVerifier::create) //
				.expectNextMatches(item -> {
					assertThat(item.getName()).isEqualTo("Alf alarm clock");
					assertThat(item.getDescription()).isEqualTo("nothing important");
					assertThat(item.getPrice()).isEqualTo(19.99);
					return true;
				}) //
				.expectNextMatches(item -> {
					assertThat(item.getName()).isEqualTo("Smurf TV tray");
					assertThat(item.getDescription()).isEqualTo("nothing important");
					assertThat(item.getPrice()).isEqualTo(29.99);
					return true;
				}) //
				.verifyComplete();
	}
	// end::spring-amqp-test[]

}


<|start_filename|>5-reactive/src/main/java/com/greglturnquist/hackingspringboot/reactive/HttpTraceWrapper.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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 com.greglturnquist.hackingspringboot.reactive;

import org.springframework.boot.actuate.trace.http.HttpTrace;
import org.springframework.data.annotation.Id;

/**
 * @author <NAME>
 */
// tag::code[]
public class HttpTraceWrapper {

    private @Id String id; // <1>
    
    private HttpTrace httpTrace; // <2>

    public HttpTraceWrapper(HttpTrace httpTrace) { // <3>
        this.httpTrace = httpTrace;
    }

    public HttpTrace getHttpTrace() { // <4>
        return httpTrace;
    }
}
// end::code[]


<|start_filename|>3-reactive/src/main/java/com/greglturnquist/hackingspringboot/reactive/HackingSpringBootApplicationBlockHoundCustomized.java<|end_filename|>
package com.greglturnquist.hackingspringboot.reactive;

import org.thymeleaf.TemplateEngine;
import reactor.blockhound.BlockHound;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.util.JdkIdGenerator;
import org.springframework.web.filter.reactive.HiddenHttpMethodFilter;

@SpringBootApplication
public class HackingSpringBootApplicationBlockHoundCustomized {

	// tag::blockhound[]
	public static void main(String[] args) {
		BlockHound.builder() // <1>
				.allowBlockingCallsInside( //
						TemplateEngine.class.getCanonicalName(), "process") // <2>
				.install(); // <3>

		SpringApplication.run(HackingSpringBootApplicationBlockHoundCustomized.class, args);
	}
	// end::blockhound[]
}


<|start_filename|>3-reactive/src/test/java/com/greglturnquist/hackingspringboot/reactive/DebuggingReactorFlows.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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 com.greglturnquist.hackingspringboot.reactive;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Hooks;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
 * @author <NAME>
 */
public class DebuggingReactorFlows {

	// tag::simple-example[]
	static class SimpleExample {
		public static void main(String[] args) {
			ExecutorService executor = Executors.newSingleThreadScheduledExecutor();

			List<Integer> source;
			if (new Random().nextBoolean()) {
				source = IntStream.range(1, 11).boxed() //
						.collect(Collectors.toList());
			} else {
				source = Arrays.asList(1, 2, 3, 4);
			}

			try {
				executor.submit(() -> source.get(5)).get(); // line 52
			} catch (InterruptedException | ExecutionException e) {
				e.printStackTrace();
			} finally {
				executor.shutdown();
			}
		}
	}
	// end::simple-example[]

	// tag::reactor-example[]
	static class ReactorExample {
		public static void main(String[] args) {
			Mono<Integer> source;
			if (new Random().nextBoolean()) {
				source = Flux.range(1, 10).elementAt(5);
			} else {
				source = Flux.just(1, 2, 3, 4).elementAt(5);
			}

			source //
					.subscribeOn(Schedulers.parallel()) //
					.block(); // line 74
		}
	}
	// end::reactor-example[]

	// tag::reactor-example2[]
	static class ReactorDebuggingExample {
		public static void main(String[] args) {

			Hooks.onOperatorDebug();

			Mono<Integer> source;
			if (new Random().nextBoolean()) {
				source = Flux.range(1, 10).elementAt(5);
			} else {
				source = Flux.just(1, 2, 3, 4).elementAt(5); // line 89
			}
			source //
					.subscribeOn(Schedulers.parallel()) //
					.block(); // line 93
		}
	}
	// end::reactor-example2[]

}


<|start_filename|>2b-reactive/src/main/java/com/greglturnquist/hackingspringboot/reactive/CartItem.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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 com.greglturnquist.hackingspringboot.reactive;

import java.util.Objects;

/**
 * @author <NAME>
 */
// tag::code[]
class CartItem {

	private Item item;
	private int quantity;

	private CartItem() {}

	CartItem(Item item) {
		this.item = item;
		this.quantity = 1;
	}
	// end::code[]

	public void increment() {
		this.quantity++;
	}

	public Item getItem() {
		return item;
	}

	public void setItem(Item item) {
		this.item = item;
	}

	public int getQuantity() {
		return quantity;
	}

	public void setQuantity(int quantity) {
		this.quantity = quantity;
	}

	@Override
	public boolean equals(Object o) {
		if (this == o)
			return true;
		if (o == null || getClass() != o.getClass())
			return false;
		CartItem cartItem = (CartItem) o;
		return quantity == cartItem.quantity && Objects.equals(item, cartItem.item);
	}

	@Override
	public int hashCode() {
		return Objects.hash(item, quantity);
	}

	@Override
	public String toString() {
		return "CartItem{" + "item=" + item + ", quantity=" + quantity + '}';
	}
}


<|start_filename|>2-reactive/src/test/java/com/greglturnquist/hackingspringboot/reactive/HomeController2.java<|end_filename|>
/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed 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 com.greglturnquist.hackingspringboot.reactive;

import org.springframework.web.bind.annotation.PostMapping;
import reactor.core.publisher.Mono;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

/**
 * @author <NAME>
 */
@Controller
public class HomeController2 {

    private final CartService cartService;

    public HomeController2(CartService cartService) {
        this.cartService = cartService;
    }

    // tag::4[]
    @PostMapping("/add/{id}")
    Mono<String> addToCart(@PathVariable String id) {
        return this.cartService.addToCart("My Cart", id) //
            .thenReturn("redirect:/");
    }
    // end::4[]


}
