diff --git a/persistence-modules/fauna/.gitignore b/persistence-modules/fauna/.gitignore new file mode 100644 index 0000000000..c37fa0c4b3 --- /dev/null +++ b/persistence-modules/fauna/.gitignore @@ -0,0 +1 @@ +/application.properties diff --git a/persistence-modules/fauna/pom.xml b/persistence-modules/fauna/pom.xml new file mode 100644 index 0000000000..67aabb7501 --- /dev/null +++ b/persistence-modules/fauna/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.6.2 + + + com.baeldung + fauna-blog + 0.0.1-SNAPSHOT + fauna-blog + Blogging Service built with FaunaDB + + 17 + + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-web + + + com.faunadb + faunadb-java + 4.2.0 + compile + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/FaunaBlogApplication.java b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/FaunaBlogApplication.java new file mode 100644 index 0000000000..12739342bf --- /dev/null +++ b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/FaunaBlogApplication.java @@ -0,0 +1,13 @@ +package com.baeldung.faunablog; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class FaunaBlogApplication { + + public static void main(String[] args) { + SpringApplication.run(FaunaBlogApplication.class, args); + } + +} diff --git a/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/FaunaConfiguration.java b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/FaunaConfiguration.java new file mode 100644 index 0000000000..9964431475 --- /dev/null +++ b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/FaunaConfiguration.java @@ -0,0 +1,25 @@ +package com.baeldung.faunablog; + +import com.faunadb.client.FaunaClient; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.net.MalformedURLException; + +@Configuration +class FaunaConfiguration { + @Value("https://db.${fauna.region}.fauna.com/") + private String faunaUrl; + + @Value("${fauna.secret}") + private String faunaSecret; + + @Bean + FaunaClient getFaunaClient() throws MalformedURLException { + return FaunaClient.builder() + .withEndpoint(faunaUrl) + .withSecret(faunaSecret) + .build(); + } +} diff --git a/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/WebSecurityConfiguration.java b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/WebSecurityConfiguration.java new file mode 100644 index 0000000000..da99b7578e --- /dev/null +++ b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/WebSecurityConfiguration.java @@ -0,0 +1,35 @@ +package com.baeldung.faunablog; + +import com.baeldung.faunablog.users.FaunaUserDetailsService; +import com.faunadb.client.FaunaClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.userdetails.UserDetailsService; + +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { + + @Autowired + private FaunaClient faunaClient; + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.csrf().disable(); + http.authorizeRequests() + .antMatchers("/**").permitAll() + .and().httpBasic(); + } + + @Bean + @Override + public UserDetailsService userDetailsService() { + return new FaunaUserDetailsService(faunaClient); + } +} diff --git a/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/posts/Author.java b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/posts/Author.java new file mode 100644 index 0000000000..ec4854621d --- /dev/null +++ b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/posts/Author.java @@ -0,0 +1,3 @@ +package com.baeldung.faunablog.posts; + +public record Author(String username, String name) {} diff --git a/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/posts/Post.java b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/posts/Post.java new file mode 100644 index 0000000000..62b6558a37 --- /dev/null +++ b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/posts/Post.java @@ -0,0 +1,5 @@ +package com.baeldung.faunablog.posts; + +import java.time.Instant; + +public record Post(String id, String title, String content, Author author, Instant created, Long version) {} diff --git a/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/posts/PostsController.java b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/posts/PostsController.java new file mode 100644 index 0000000000..e8e6316ea8 --- /dev/null +++ b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/posts/PostsController.java @@ -0,0 +1,50 @@ +package com.baeldung.faunablog.posts; + +import com.faunadb.client.errors.NotFoundException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; +import java.util.concurrent.ExecutionException; + +@RestController +@RequestMapping("/posts") +public class PostsController { + @Autowired + private PostsService postsService; + + @GetMapping + public List listPosts(@RequestParam(value = "author", required = false) String author) throws ExecutionException, InterruptedException { + return author == null ? postsService.getAllPosts() : postsService.getAuthorPosts("graham"); + } + + @GetMapping("/{id}") + public Post getPost(@PathVariable("id") String id, @RequestParam(value = "before", required = false) Long before) + throws ExecutionException, InterruptedException { + return postsService.getPost(id, before); + } + + @PostMapping + @ResponseStatus(HttpStatus.NO_CONTENT) + @PreAuthorize("isAuthenticated()") + public void createPost(@RequestBody UpdatedPost post) throws ExecutionException, InterruptedException { + String name = SecurityContextHolder.getContext().getAuthentication().getName(); + postsService.createPost(name, post.title(), post.content()); + } + + @PutMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + @PreAuthorize("isAuthenticated()") + public void updatePost(@PathVariable("id") String id, @RequestBody UpdatedPost post) + throws ExecutionException, InterruptedException { + postsService.updatePost(id, post.title(), post.content()); + } + + @ExceptionHandler(NotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public void postNotFound() {} +} diff --git a/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/posts/PostsService.java b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/posts/PostsService.java new file mode 100644 index 0000000000..5143a24b28 --- /dev/null +++ b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/posts/PostsService.java @@ -0,0 +1,124 @@ +package com.baeldung.faunablog.posts; + +import com.faunadb.client.FaunaClient; +import com.faunadb.client.types.Value; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +import static com.faunadb.client.query.Language.*; + +@Component +public class PostsService { + @Autowired + private FaunaClient faunaClient; + + Post getPost(String id, Long before) throws ExecutionException, InterruptedException { + var query = Get(Ref(Collection("posts"), id)); + if (before != null) { + query = At(Value(before - 1), query); + } + + var postResult= faunaClient.query( + Let( + "post", query + ).in( + Obj( + "post", Var("post"), + "author", Get(Select(Arr(Value("data"), Value("authorRef")), Var("post"))) + ) + )).get(); + + return parsePost(postResult); + } + + List getAllPosts() throws ExecutionException, InterruptedException { + var postsResult = faunaClient.query(Map( + Paginate( + Join( + Documents(Collection("posts")), + Index("posts_sort_by_created_desc") + ) + ), + Lambda( + Arr(Value("extra"), Value("ref")), + Obj( + "post", Get(Var("ref")), + "author", Get(Select(Arr(Value("data"), Value("authorRef")), Get(Var("ref")))) + ) + ) + )).get(); + + var posts = postsResult.at("data").asCollectionOf(Value.class).get(); + return posts.stream().map(this::parsePost).collect(Collectors.toList()); + } + + List getAuthorPosts(String author) throws ExecutionException, InterruptedException { + var postsResult = faunaClient.query(Map( + Paginate( + Join( + Match(Index("posts_by_author"), Select(Value("ref"), Get(Match(Index("users_by_username"), Value(author))))), + Index("posts_sort_by_created_desc") + ) + ), + Lambda( + Arr(Value("extra"), Value("ref")), + Obj( + "post", Get(Var("ref")), + "author", Get(Select(Arr(Value("data"), Value("authorRef")), Get(Var("ref")))) + ) + ) + )).get(); + + var posts = postsResult.at("data").asCollectionOf(Value.class).get(); + return posts.stream().map(this::parsePost).collect(Collectors.toList()); + } + + public void createPost(String author, String title, String contents) throws ExecutionException, InterruptedException { + faunaClient.query( + Create(Collection("posts"), + Obj( + "data", Obj( + "title", Value(title), + "contents", Value(contents), + "created", Now(), + "authorRef", Select(Value("ref"), Get(Match(Index("users_by_username"), Value(author))))) + ) + ) + ).get(); + } + + public void updatePost(String id, String title, String contents) throws ExecutionException, InterruptedException { + faunaClient.query( + Update(Ref(Collection("posts"), id), + Obj( + "data", Obj( + "title", Value(title), + "contents", Value(contents)) + ) + ) + ).get(); + } + + private Post parsePost(Value entry) { + var author = entry.at("author"); + var post = entry.at("post"); + + return new Post( + post.at("ref").to(Value.RefV.class).get().getId(), + post.at("data", "title").to(String.class).get(), + post.at("data", "contents").to(String.class).get(), + new Author( + author.at("data", "username").to(String.class).get(), + author.at("data", "name").to(String.class).get() + ), + post.at("data", "created").to(Instant.class).get(), + post.at("ts").to(Long.class).get() + ); + } +} diff --git a/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/posts/UpdatedPost.java b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/posts/UpdatedPost.java new file mode 100644 index 0000000000..9850cd5927 --- /dev/null +++ b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/posts/UpdatedPost.java @@ -0,0 +1,3 @@ +package com.baeldung.faunablog.posts; + +public record UpdatedPost(String title, String content) {} diff --git a/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/users/FaunaUserDetailsService.java b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/users/FaunaUserDetailsService.java new file mode 100644 index 0000000000..2e88aaa477 --- /dev/null +++ b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/users/FaunaUserDetailsService.java @@ -0,0 +1,44 @@ +package com.baeldung.faunablog.users; + +import com.faunadb.client.FaunaClient; +import com.faunadb.client.types.Value; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +import java.util.concurrent.ExecutionException; + +import static com.faunadb.client.query.Language.*; + +public class FaunaUserDetailsService implements UserDetailsService { + private FaunaClient faunaClient; + + public FaunaUserDetailsService(FaunaClient faunaClient) { + this.faunaClient = faunaClient; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + try { + Value user = faunaClient.query(Map( + Paginate(Match(Index("users_by_username"), Value(username))), + Lambda(Value("user"), Get(Var("user"))))) + .get(); + + Value userData = user.at("data").at(0).orNull(); + if (userData == null) { + throw new UsernameNotFoundException("User not found"); + } + + return User.withDefaultPasswordEncoder() + .username(userData.at("data", "username").to(String.class).orNull()) + .password(userData.at("data", "password").to(String.class).orNull()) + .roles("USER") + .build(); + } catch (ExecutionException | InterruptedException e) { + throw new RuntimeException(e); + } + } +} + diff --git a/persistence-modules/fauna/src/main/resources/application.properties b/persistence-modules/fauna/src/main/resources/application.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pom.xml b/pom.xml index 1243eb4522..197e946974 100644 --- a/pom.xml +++ b/pom.xml @@ -1347,6 +1347,7 @@ spring-boot-modules/spring-boot-cassandre spring-boot-modules/spring-boot-camel testing-modules/testing-assertions + persistence-modules/fauna @@ -1409,6 +1410,7 @@ spring-boot-modules/spring-boot-cassandre spring-boot-modules/spring-boot-camel testing-modules/testing-assertions + persistence-modules/fauna