diff --git a/spring-geode-samples/caching/inline-async/spring-geode-samples-caching-inline-async.gradle b/spring-geode-samples/caching/inline-async/spring-geode-samples-caching-inline-async.gradle new file mode 100644 index 00000000..199b0c3e --- /dev/null +++ b/spring-geode-samples/caching/inline-async/spring-geode-samples-caching-inline-async.gradle @@ -0,0 +1,32 @@ +plugins { + id "io.freefair.lombok" version "5.3.0" +} + +apply plugin: 'io.spring.convention.spring-sample-boot' + +description = "Spring Geode Sample demonstrating Spring's Cache Abstraction using Apache Geode as the caching provider for Asynchronous Inline Caching." + +dependencies { + + compile project(":spring-geode-starter") + + compile "org.projectlombok:lombok" + compile "org.springframework.boot:spring-boot-starter-data-jpa" + compile "org.springframework.boot:spring-boot-starter-web" + + runtime "org.hsqldb:hsqldb" + + testCompile project(":spring-geode-starter-test") + + testCompile "org.awaitility:awaitility:$awaitilityVersion" + testCompile "org.springframework.boot:spring-boot-starter-test" + +} + +bootJar { + mainClassName = 'example.app.caching.inline.BootGeodeAsyncInlineCachingClientApplication' +} + +bootRun { + main = 'example.app.caching.inline.BootGeodeAsyncInlineClientCachingApplication' +} diff --git a/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/client/BootGeodeAsyncInlineCachingClientApplication.java b/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/client/BootGeodeAsyncInlineCachingClientApplication.java new file mode 100644 index 00000000..ab5b43b6 --- /dev/null +++ b/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/client/BootGeodeAsyncInlineCachingClientApplication.java @@ -0,0 +1,102 @@ +/* + * Copyright 2020 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 example.app.caching.inline.async.client; + +import org.apache.geode.cache.RegionShortcut; + +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Profile; +import org.springframework.data.gemfire.config.annotation.EnableCachingDefinedRegions; +import org.springframework.data.gemfire.config.annotation.PeerCacheApplication; +import org.springframework.geode.config.annotation.UseMemberName; +import org.springframework.scheduling.annotation.EnableScheduling; + +import example.app.caching.inline.async.client.model.GolfTournament; +import example.app.caching.inline.async.client.model.support.GolfCourseBuilder; +import example.app.caching.inline.async.client.model.support.GolferBuilder; +import example.app.caching.inline.async.client.service.GolfTournamentService; +import example.app.caching.inline.async.config.AsyncInlineCachingConfiguration; + +/** + * {@link SpringBootApplication} class simulating a golf tournament management application. + * + * @author John Blum + * @see org.springframework.boot.ApplicationRunner + * @see org.springframework.boot.SpringApplication + * @see org.springframework.boot.autoconfigure.SpringBootApplication + * @see org.springframework.context.annotation.Bean + * @see org.springframework.context.annotation.Configuration + * @see org.springframework.context.annotation.Import + * @see org.springframework.context.annotation.Profile + * @see org.springframework.data.gemfire.config.annotation.EnableCachingDefinedRegions + * @see org.springframework.data.gemfire.config.annotation.PeerCacheApplication + * @see org.springframework.geode.cache.AsyncInlineCachingRegionConfigurer + * @see org.springframework.geode.config.annotation.UseMemberName + * @see org.springframework.scheduling.annotation.EnableScheduling + * @see example.app.caching.inline.async.client.model.GolfCourse + * @see example.app.caching.inline.async.client.model.GolfTournament + * @see example.app.caching.inline.async.client.model.Golfer + * @see example.app.caching.inline.async.client.service.GolfTournamentService + * @see example.app.caching.inline.async.config.AsyncInlineCachingConfiguration + * @since 1.4.0 + */ +@SpringBootApplication +@SuppressWarnings("unused") +public class BootGeodeAsyncInlineCachingClientApplication { + + private static final String APPLICATION_NAME = "GolfClientApplication"; + + public static void main(String[] args) { + SpringApplication.run(BootGeodeAsyncInlineCachingClientApplication.class, args); + } + + @Configuration + @EnableScheduling + static class GolfApplicationConfiguration { + + @Bean + ApplicationRunner runGolfTournament(GolfTournamentService golfTournamentService) { + + return args -> { + + GolfTournament golfTournament = GolfTournament.newGolfTournament("Masters") + .at(GolfCourseBuilder.buildAugustaNational()) + .register(GolferBuilder.buildGolfers(GolferBuilder.FAVORITE_GOLFER_NAMES)) + .buildPairings() + .play(); + + golfTournamentService.manage(golfTournament); + + }; + } + } + + @Configuration + @UseMemberName(APPLICATION_NAME) + @EnableCachingDefinedRegions(serverRegionShortcut = RegionShortcut.LOCAL) + static class GeodeConfiguration { } + + @PeerCacheApplication + @Profile("peer-cache") + @Import(AsyncInlineCachingConfiguration.class) + static class PeerCacheApplicationConfiguration { } + +} diff --git a/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/client/model/GolfCourse.java b/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/client/model/GolfCourse.java new file mode 100644 index 00000000..9526c0ce --- /dev/null +++ b/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/client/model/GolfCourse.java @@ -0,0 +1,101 @@ +/* + * Copyright 2020 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 example.app.caching.inline.async.client.model; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.apache.shiro.util.CollectionUtils; + +import org.springframework.util.Assert; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +/** + * Abstract Data Type (ADT) modeling a golf course. + * + * @author John Blum + * @since 1.4.0 + */ +@Getter +@ToString(of = "name") +@EqualsAndHashCode(of = "name") +@RequiredArgsConstructor(staticName = "newGolfCourse") +@SuppressWarnings("unused") +public class GolfCourse { + + public static final int STANDARD_PAR_FOR_COURSE = 72; + + public static final Set VALID_PARS_FOR_HOLE = + Collections.unmodifiableSet(CollectionUtils.asSet(3, 4, 5)); + + @NonNull + private final String name; + + private final List parForHole = new ArrayList<>(18); + + public int getPar(int hole) { + + assertValidHoleNumber(hole); + + return this.parForHole.get(indexForHole(hole)); + } + + public int getParForCourse() { + + return this.parForHole.stream() + .reduce(Integer::sum) + .orElse(STANDARD_PAR_FOR_COURSE); + } + + public GolfCourse withHole(int holeNumber, int par) { + + assertValidHoleNumber(holeNumber); + assertValidParForHole(par, holeNumber); + + this.parForHole.set(indexForHole(holeNumber), par); + + return this; + } + + private void assertValidHoleNumber(int hole) { + Assert.isTrue(isValidHoleNumber(hole), + () -> String.format("Hole number [%d] must be 1 through 18", hole)); + } + + private void assertValidParForHole(int par, int hole) { + Assert.isTrue(isValidPar(par), + () -> String.format("Par [%1$d] for hole [%2$d] must be in [%3$s]", par, hole, VALID_PARS_FOR_HOLE)); + } + + private int indexForHole(int hole) { + return hole - 1; + } + + public boolean isValidHoleNumber(int hole) { + return hole >= 1 && hole <= 18; + } + + public boolean isValidPar(int par) { + return VALID_PARS_FOR_HOLE.contains(par); + } +} diff --git a/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/client/model/GolfTournament.java b/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/client/model/GolfTournament.java new file mode 100644 index 00000000..f597cb90 --- /dev/null +++ b/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/client/model/GolfTournament.java @@ -0,0 +1,157 @@ +/* + * Copyright 2020 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 example.app.caching.inline.async.client.model; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.StreamSupport; + +import org.springframework.data.gemfire.util.ArrayUtils; +import org.springframework.data.gemfire.util.CollectionUtils; +import org.springframework.util.Assert; + +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +/** + * Abstract Data Type (ADT) modeling a golf tournament. + * + * @author John Blum + * @since 1.4.0 + */ +@Getter +@ToString +@RequiredArgsConstructor(staticName = "newGolfTournament") +@SuppressWarnings("unused") +public class GolfTournament implements Iterable { + + @NonNull + private final String name; + + private GolfCourse golfCourse; + + private final List pairings = new ArrayList<>(); + + private final Set players = new HashSet<>(); + + @Override + public Iterator iterator() { + return Collections.unmodifiableList(this.pairings).iterator(); + } + + public boolean isFinished() { + + Set finishedPairings = new HashSet<>(this.pairings.size()); + + for (Pairing pairing : this) { + if (pairing.getHole() < 18) { + return false; + } + else { + finishedPairings.add(pairing); + } + } + + this.pairings.removeAll(finishedPairings); + + return this.pairings.isEmpty(); + } + + public GolfTournament at(GolfCourse golfCourse) { + + Assert.notNull(golfCourse, "Golf Course must not be null"); + + this.golfCourse = golfCourse; + + return this; + } + + public GolfTournament buildPairings() { + + Assert.notEmpty(this.players, + () -> String.format("No players are registered for this golf tournament [%s]", getName())); + + Assert.isTrue(this.players.size() % 2 == 0, + () -> String.format("An even number of players must register to play this golf tournament [%s]; currently at [%d]", + getName(), this.players.size())); + + List playersToPair = new ArrayList<>(this.players); + + Collections.shuffle(playersToPair); + + for (int index = 0, size = playersToPair.size(); index < size; index += 2) { + this.pairings.add(Pairing.of(playersToPair.get(index), playersToPair.get(index + 1))); + } + + return this; + } + + public GolfTournament play() { + + Assert.state(this.golfCourse != null, "No golf course was declared"); + Assert.state(!this.players.isEmpty(), "Golfers must register to play before the golf tournament is played"); + Assert.state(!this.pairings.isEmpty(), "Pairings must be formed before the golf tournament is played"); + Assert.state(!isFinished(), () -> String.format("Golf tournament [%s] has already been played", getName())); + + return this; + } + + public GolfTournament register(Golfer... players) { + return register(Arrays.asList(ArrayUtils.nullSafeArray(players, Golfer.class))); + } + + public GolfTournament register(Iterable players) { + + StreamSupport.stream(CollectionUtils.nullSafeIterable(players).spliterator(), false) + .filter(Objects::nonNull) + .forEach(this.players::add); + + return this; + } + + @Getter + @ToString + @RequiredArgsConstructor(staticName = "of") + public static class Pairing { + + @NonNull + private final Golfer playerOne; + + @NonNull + private final Golfer playerTwo; + + public int getHole() { + return getPlayerOne().getHole(); + } + + public void setHole(int hole) { + this.playerOne.setHole(hole); + this.playerTwo.setHole(hole); + } + + public int playNextHole() { + return getHole() + 1; + } + } +} diff --git a/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/client/model/Golfer.java b/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/client/model/Golfer.java new file mode 100644 index 00000000..8b78d7a7 --- /dev/null +++ b/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/client/model/Golfer.java @@ -0,0 +1,89 @@ +/* + * Copyright 2020 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 example.app.caching.inline.async.client.model; + +import javax.persistence.Entity; +import javax.persistence.Table; + +import org.springframework.data.annotation.Id; +import org.springframework.data.gemfire.mapping.annotation.Region; +import org.springframework.util.Assert; + +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +/** + * Abstract Data Type (ADT) modeling a person who plays golf. + * + * In addition to the {@link Golfer Golfer's} {@link String name}, this class tracks the current {@link Integer hole} + * and {@link Integer score} of the {@link Golfer} when s/he competes/plays in a golf tournament. + * + * @author John Blum + * @see javax.persistence.Entity + * @see javax.persistence.Table + * @see org.springframework.data.annotation.Id + * @see org.springframework.data.gemfire.mapping.annotation.Region + * @since 1.4.0 + */ +@Entity +@Getter +@ToString(of = "name") +@Table(name = "golfers") +@Region(name = "Golfers") +@EqualsAndHashCode(of = "name") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@RequiredArgsConstructor(staticName = "newGolfer") +@SuppressWarnings("unused") +public class Golfer implements Comparable { + + @javax.persistence.Id @Id @NonNull + private String name; + + @Setter + private Integer hole = 0; + + @Setter + private Integer score = 0; + + @Override + public int compareTo(Golfer other) { + return this.getName().compareTo(other.getName()); + } + + private boolean isValidHole(int hole) { + return hole >= 1 && hole <= 18; + } + + public Golfer on(int hole) { + + Assert.isTrue(isValidHole(hole), () -> String.format("Hole [%d] must be 1 through 18", hole)); + + this.hole = hole; + + return this; + } + + public Golfer shot(int score) { + this.score = score; + return this; + } +} diff --git a/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/client/model/support/GolfCourseBuilder.java b/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/client/model/support/GolfCourseBuilder.java new file mode 100644 index 00000000..2949bef0 --- /dev/null +++ b/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/client/model/support/GolfCourseBuilder.java @@ -0,0 +1,57 @@ +/* + * Copyright 2020 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 example.app.caching.inline.async.client.model.support; + +import example.app.caching.inline.async.client.model.GolfCourse; + +/** + * A {@literal Builder} class used to build {@link GolfCourse golf courses}. + * + * @author John Blum + * @see example.app.caching.inline.async.client.model.GolfCourse + * @since 1.4.0 + */ +public abstract class GolfCourseBuilder { + + /** + * Builds the {@literal Augusta National} {@link GolfCourse}, home of the {@literal Masters} major golf tournament. + * + * @return a new instance of {@link GolfCourse} modeling {@literal Augusta National} in Augusta, GA; USA. + * @see example.app.caching.inline.async.client.model.GolfCourse + */ + public static GolfCourse buildAugustaNational() { + + return GolfCourse.newGolfCourse("Augusta National") + .withHole(1, 4) + .withHole(2, 5) + .withHole(3, 4) + .withHole(4, 3) + .withHole(5, 4) + .withHole(6, 3) + .withHole(7, 4) + .withHole(8, 5) + .withHole(9, 4) + .withHole(10, 4) + .withHole(11, 4) + .withHole(12, 3) + .withHole(13, 5) + .withHole(14, 4) + .withHole(15, 5) + .withHole(16, 3) + .withHole(17, 4) + .withHole(18, 4); + } +} diff --git a/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/client/model/support/GolferBuilder.java b/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/client/model/support/GolferBuilder.java new file mode 100644 index 00000000..8b21016c --- /dev/null +++ b/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/client/model/support/GolferBuilder.java @@ -0,0 +1,58 @@ +/* + * Copyright 2020 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 example.app.caching.inline.async.client.model.support; + +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.data.gemfire.util.ArrayUtils; +import org.springframework.util.StringUtils; + +import example.app.caching.inline.async.client.model.Golfer; + +/** + * A {@literal Builder} class used to build {@link Golfer Golfers}. + * + * @author John Blum + * @see example.app.caching.inline.async.client.model.Golfer + * @since 1.4.0 + */ +public abstract class GolferBuilder { + + public static final String[] FAVORITE_GOLFER_NAMES = { + "Arnold Palmer", + "Ben Hogan", + "Bobby Jones", + "Tiger Woods", + "Rory McIlroy", + "Dustin Johnson", + "Jason Day", + "Justin Thomas", + "John Rahm", + "Jordan Spieth", + "Phil Michelson", + "Ricky Fowler" + }; + + public static Set buildGolfers(String... names) { + + return Arrays.stream(ArrayUtils.nullSafeArray(names, String.class)) + .filter(StringUtils::hasText) + .map(Golfer::newGolfer) + .collect(Collectors.toSet()); + } +} diff --git a/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/client/repo/GolferRepository.java b/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/client/repo/GolferRepository.java new file mode 100644 index 00000000..225f940d --- /dev/null +++ b/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/client/repo/GolferRepository.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020 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 example.app.caching.inline.async.client.repo; + +import org.springframework.data.jpa.repository.JpaRepository; + +import example.app.caching.inline.async.client.model.Golfer; + +/** + * Spring Data {@link JpaRepository} and Data Access Object (DAO) used to perform basic CRUD and simple SQL query + * data access operations on {@link Golfer Golfers} stored in an RDBMS (database) with JPA. + * + * @author John Blum + * @see org.springframework.data.jpa.repository.JpaRepository + * @see example.app.caching.inline.async.client.model.Golfer + * @since 1.4.0 + */ +public interface GolferRepository extends JpaRepository { + +} diff --git a/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/client/service/GolfTournamentService.java b/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/client/service/GolfTournamentService.java new file mode 100644 index 00000000..c6e324cf --- /dev/null +++ b/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/client/service/GolfTournamentService.java @@ -0,0 +1,163 @@ +/* + * Copyright 2020 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 example.app.caching.inline.async.client.service; + +import java.io.Closeable; +import java.util.HashSet; +import java.util.Optional; +import java.util.Random; +import java.util.Set; +import java.util.function.Function; + +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +import example.app.caching.inline.async.client.model.GolfCourse; +import example.app.caching.inline.async.client.model.GolfTournament; +import example.app.caching.inline.async.client.model.Golfer; + +/** + * Spring {@link Service} class used to manage golf tournaments. + * + * @author John Blum + * @see java.io.Closeable + * @see org.springframework.scheduling.annotation.Scheduled + * @see org.springframework.stereotype.Service + * @see example.app.caching.inline.async.client.model.GolfTournament + * @see example.app.caching.inline.async.client.model.Golfer + * @since 1.4.0 + */ +@Service +@SuppressWarnings("unused") +public class GolfTournamentService implements Closeable { + + protected static final int SCORE_DELTA_BOUND = 2; + + private final GolferService golferService; + + private volatile GolfTournament golfTournament; + + private final Random random = new Random(System.currentTimeMillis()); + + public GolfTournamentService(GolferService golferService) { + + Assert.notNull(golferService, "GolferService must not be null"); + + this.golferService = golferService; + } + + public Optional getGolfTournament() { + return Optional.ofNullable(this.golfTournament); + } + + @Override + public void close() { + this.golfTournament = null; + } + + public GolfTournamentService manage(GolfTournament golfTournament) { + + GolfTournament currentGolfTournament = this.golfTournament; + + Assert.state(currentGolfTournament == null, + () -> String.format("Can only manage 1 golf tournament at a time; currently managing [%s]", + currentGolfTournament)); + + this.golfTournament = golfTournament; + + return this; + } + + @SuppressWarnings("unused") + @Scheduled(fixedRate = 2500L) + public void play() { + + GolfTournament golfTournament = this.golfTournament; + + if (golfTournament != null) { + playHole(golfTournament); + finish(golfTournament); + } + } + + private void playHole(@NonNull GolfTournament golfTournament) { + + GolfCourse golfCourse = golfTournament.getGolfCourse(); + + Set occupiedHoles = new HashSet<>(); + + for (GolfTournament.Pairing pairing : golfTournament) { + + int hole = pairing.playNextHole(); + + if (!occupiedHoles.contains(hole)) { + if (golfCourse.isValidHoleNumber(hole)) { + occupiedHoles.add(hole); + pairing.setHole(hole); + updateScore(this::calculateRunningScore, pairing.getPlayerOne()); + updateScore(this::calculateRunningScore, pairing.getPlayerTwo()); + } + } + } + } + + private Golfer updateScore(@NonNull Function scoreFunction, @NonNull Golfer player) { + + player.setScore(scoreFunction.apply(player.getScore())); + + this.golferService.update(player); + + return player; + } + + private int calculateFinalScore(@Nullable Integer scoreRelativeToPar) { + + int finalScore = scoreRelativeToPar != null ? scoreRelativeToPar : 0; + + int parForCourse = getGolfTournament() + .map(GolfTournament::getGolfCourse) + .map(GolfCourse::getParForCourse) + .orElse(GolfCourse.STANDARD_PAR_FOR_COURSE); + + return parForCourse + finalScore; + } + + private int calculateRunningScore(@Nullable Integer currentScore) { + + int runningScore = currentScore != null ? currentScore : 0; + int scoreDelta = this.random.nextInt(SCORE_DELTA_BOUND); + + scoreDelta *= this.random.nextBoolean() ? -1 : 1; + + return runningScore + scoreDelta; + } + + private void finish(@NonNull GolfTournament golfTournament) { + + if (golfTournament.isFinished()) { + + GolfCourse golfCourse = golfTournament.getGolfCourse(); + + for (GolfTournament.Pairing pairing : golfTournament) { + updateScore(this::calculateFinalScore, pairing.getPlayerOne()); + updateScore(this::calculateFinalScore, pairing.getPlayerTwo()); + } + } + } +} diff --git a/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/client/service/GolferService.java b/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/client/service/GolferService.java new file mode 100644 index 00000000..84361ce7 --- /dev/null +++ b/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/client/service/GolferService.java @@ -0,0 +1,93 @@ +/* + * Copyright 2020 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 example.app.caching.inline.async.client.service; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.geode.cache.Region; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.cache.annotation.CachePut; +import org.springframework.data.gemfire.GemfireTemplate; +import org.springframework.data.gemfire.util.RegionUtils; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +import example.app.caching.inline.async.client.model.Golfer; +import example.app.caching.inline.async.client.repo.GolferRepository; + +/** + * Spring {@link Service} class used to manage {@link Golfer Golfers}. + * + * @author John Blum + * @see org.springframework.stereotype.Service + * @see org.springframework.data.gemfire.GemfireTemplate + * @see example.app.caching.inline.async.client.model.Golfer + * @see example.app.caching.inline.async.client.repo.GolferRepository + * @since 1.4.0 + */ +@Service +@SuppressWarnings("unused") +public class GolferService { + + private final GemfireTemplate golfersTemplate; + + private final GolferRepository golferRepository; + + public GolferService(@Qualifier("golfersTemplate") GemfireTemplate golfersTemplate, + GolferRepository golferRepository) { + + Assert.notNull(golfersTemplate, "GolfersTemplate must not be null"); + Assert.notNull(golferRepository, "GolferRepository must not be null"); + + this.golfersTemplate = golfersTemplate; + this.golferRepository = golferRepository; + } + + @CachePut(cacheNames = "Golfers", key = "#golfer.name") + public Golfer update(Golfer golfer) { + return golfer; + } + + public List getAllGolfersFromCache() { + + Map golferMap = + nullSafeMap(this.golfersTemplate.getAll(resolveKeys(this.golfersTemplate.getRegion()))); + + return sort(new ArrayList<>(golferMap.values())); + } + + public List getAllGolfersFromDatabase() { + return sort(this.golferRepository.findAll()); + } + + private Map nullSafeMap(Map map) { + return map != null ? map : Collections.emptyMap(); + } + + private Set resolveKeys(Region region) { + return RegionUtils.isClient(region) ? region.keySetOnServer() : region.keySet(); + } + + private > List sort(List list) { + Collections.sort(list); + return list; + } +} diff --git a/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/config/AsyncInlineCachingConfiguration.java b/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/config/AsyncInlineCachingConfiguration.java new file mode 100644 index 00000000..117f3dc5 --- /dev/null +++ b/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/config/AsyncInlineCachingConfiguration.java @@ -0,0 +1,71 @@ +/* + * Copyright 2020 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 example.app.caching.inline.async.config; + +import java.time.Duration; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.geode.cache.AsyncInlineCachingRegionConfigurer; + +import example.app.caching.inline.async.client.model.Golfer; +import example.app.caching.inline.async.client.repo.GolferRepository; + +/** + * Spring {@link Configuration} class used to configure {@literal Async Inline Caching}. + * + * @author John Blum + * @see java.time.Duration + * @see org.springframework.context.annotation.Bean + * @see org.springframework.context.annotation.Configuration + * @see org.springframework.context.annotation.Profile + * @see org.springframework.geode.cache.AsyncInlineCachingRegionConfigurer + * @see example.app.caching.inline.async.client.model.Golfer + * @see example.app.caching.inline.async.client.repo.GolferRepository + * @since 1.4.0 + */ +@Configuration +@SuppressWarnings("unused") +public class AsyncInlineCachingConfiguration { + + private static final String GOLFERS_REGION_NAME = "Golfers"; + + @Bean + @Profile("queue-batch-size") + AsyncInlineCachingRegionConfigurer batchSizeAsyncInlineCachingConfigurer( + @Value("${spring.geode.sample.async-inline-caching.queue.batch-size:4}") int queueBatchSize, + GolferRepository golferRepository) { + + return AsyncInlineCachingRegionConfigurer.create(golferRepository, GOLFERS_REGION_NAME) + .withQueueBatchSize(queueBatchSize) + .withQueueBatchTimeInterval(Duration.ofMinutes(60)) + .withQueueDispatcherThreadCount(1); + } + + @Bean + @Profile("queue-batch-time-interval") + AsyncInlineCachingRegionConfigurer batchTimeIntervalAsyncInlineCachingConfigurer( + @Value("${spring.geode.sample.async-inline-caching.queue.batch-time-interval-ms:5000}") int queueBatchTimeIntervalMilliseconds, + GolferRepository golferRepository) { + + return AsyncInlineCachingRegionConfigurer.create(golferRepository, GOLFERS_REGION_NAME) + .withQueueBatchTimeInterval(Duration.ofMillis(queueBatchTimeIntervalMilliseconds)) + .withQueueBatchSize(1000000) + .withQueueDispatcherThreadCount(1); + } +} diff --git a/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/server/BootGeodeAsyncInlineCachingServerApplication.java b/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/server/BootGeodeAsyncInlineCachingServerApplication.java new file mode 100644 index 00000000..e6fc208d --- /dev/null +++ b/spring-geode-samples/caching/inline-async/src/main/java/example/app/caching/inline/async/server/BootGeodeAsyncInlineCachingServerApplication.java @@ -0,0 +1,69 @@ +/* + * Copyright 2020 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 example.app.caching.inline.async.server; + +import org.apache.geode.cache.RegionShortcut; + +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Profile; +import org.springframework.data.gemfire.config.annotation.CacheServerApplication; +import org.springframework.data.gemfire.config.annotation.EnableEntityDefinedRegions; + +import example.app.caching.inline.async.client.model.Golfer; +import example.app.caching.inline.async.config.AsyncInlineCachingConfiguration; + +/** + * {@link SpringBootApplication} class implementing the server-side of the golf tournament management application. + * + * @author John Blum + * @see java.time.Duration + * @see org.springframework.boot.autoconfigure.SpringBootApplication + * @see org.springframework.boot.builder.SpringApplicationBuilder + * @see org.springframework.context.annotation.Configuration + * @see org.springframework.context.annotation.Import + * @see org.springframework.context.annotation.Profile + * @see org.springframework.data.gemfire.config.annotation.CacheServerApplication + * @see org.springframework.data.gemfire.config.annotation.EnableEntityDefinedRegions + * @see example.app.caching.inline.async.config.AsyncInlineCachingConfiguration + * @see example.app.caching.inline.async.client.model.Golfer + * @since 1.4.0 + */ +@SpringBootApplication +@Profile("server") +public class BootGeodeAsyncInlineCachingServerApplication { + + private static final String APPLICATION_NAME = "GolfServerApplication"; + + public static void main(String[] args) { + + new SpringApplicationBuilder(BootGeodeAsyncInlineCachingServerApplication.class) + .web(WebApplicationType.NONE) + .build() + .run(args); + } + + @Configuration + @CacheServerApplication(name = APPLICATION_NAME) + @EnableEntityDefinedRegions(basePackageClasses = Golfer.class, serverRegionShortcut = RegionShortcut.LOCAL) + @Import(AsyncInlineCachingConfiguration.class) + @SuppressWarnings("unused") + static class GeodeConfiguration { } + +} diff --git a/spring-geode-samples/caching/inline-async/src/main/resources/application-client.properties b/spring-geode-samples/caching/inline-async/src/main/resources/application-client.properties new file mode 100644 index 00000000..5b5b1e10 --- /dev/null +++ b/spring-geode-samples/caching/inline-async/src/main/resources/application-client.properties @@ -0,0 +1,3 @@ +# Spring Boot application.properties configuration for the golf client application + +spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration diff --git a/spring-geode-samples/caching/inline-async/src/main/resources/application-server.properties b/spring-geode-samples/caching/inline-async/src/main/resources/application-server.properties new file mode 100644 index 00000000..1ee7d8ef --- /dev/null +++ b/spring-geode-samples/caching/inline-async/src/main/resources/application-server.properties @@ -0,0 +1,5 @@ +# Spring Boot application.properties configuration for the golfer server application. +# RBMS (Database) configuration properties. + +spring.jpa.show-sql=true +spring.jpa.hibernate.ddl-auto=none diff --git a/spring-geode-samples/caching/inline-async/src/main/resources/schema.sql b/spring-geode-samples/caching/inline-async/src/main/resources/schema.sql new file mode 100644 index 00000000..f850b179 --- /dev/null +++ b/spring-geode-samples/caching/inline-async/src/main/resources/schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS golfers ( + name VARCHAR(256) PRIMARY KEY, + hole NUMERIC(10), + score NUMERIC(10) +);