diff --git a/src/main/java/com/scmspain/MsFcTechTestApplication.java b/src/main/java/com/scmspain/MsFcTechTestApplication.java index 28b3538..9038227 100644 --- a/src/main/java/com/scmspain/MsFcTechTestApplication.java +++ b/src/main/java/com/scmspain/MsFcTechTestApplication.java @@ -4,7 +4,6 @@ import com.scmspain.configuration.TweetConfiguration; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; diff --git a/src/main/java/com/scmspain/controller/TweetController.java b/src/main/java/com/scmspain/controller/TweetController.java index 55ce7cd..c314db9 100644 --- a/src/main/java/com/scmspain/controller/TweetController.java +++ b/src/main/java/com/scmspain/controller/TweetController.java @@ -1,5 +1,6 @@ package com.scmspain.controller; +import com.scmspain.controller.command.DiscardTweetCommand; import com.scmspain.controller.command.PublishTweetCommand; import com.scmspain.entities.Tweet; import com.scmspain.services.TweetService; @@ -29,6 +30,16 @@ public void publishTweet(@RequestBody PublishTweetCommand publishTweetCommand) { this.tweetService.publishTweet(publishTweetCommand.getPublisher(), publishTweetCommand.getTweet()); } + @PostMapping("/discarded") + public void discardTweet(@RequestBody DiscardTweetCommand discardTweetCommand) { + this.tweetService.discardTweet(discardTweetCommand.getTweet()); + } + + @GetMapping("/discarded") + public List listAllDiscardedTweets() { + return this.tweetService.listAllDiscardedTweets(); + } + @ExceptionHandler(IllegalArgumentException.class) @ResponseStatus(BAD_REQUEST) @ResponseBody diff --git a/src/main/java/com/scmspain/controller/command/DiscardTweetCommand.java b/src/main/java/com/scmspain/controller/command/DiscardTweetCommand.java new file mode 100644 index 0000000..3bf4e4c --- /dev/null +++ b/src/main/java/com/scmspain/controller/command/DiscardTweetCommand.java @@ -0,0 +1,14 @@ +package com.scmspain.controller.command; + +public class DiscardTweetCommand { + + private Long tweet; + + public Long getTweet() { + return tweet; + } + + public void setTweet(Long tweet) { + this.tweet = tweet; + } +} diff --git a/src/main/java/com/scmspain/entities/Tweet.java b/src/main/java/com/scmspain/entities/Tweet.java index 3616a94..26985b4 100644 --- a/src/main/java/com/scmspain/entities/Tweet.java +++ b/src/main/java/com/scmspain/entities/Tweet.java @@ -4,6 +4,8 @@ import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; +import java.sql.Timestamp; +import java.time.LocalDateTime; @Entity public class Tweet { @@ -16,6 +18,10 @@ public class Tweet { private String tweet; @Column (nullable=true) private Long pre2015MigrationStatus = 0L; + @Column(nullable = false) + private Boolean discarded = Boolean.FALSE; + @Column(nullable = true) + private Timestamp discardedDate; public Tweet() { } @@ -52,4 +58,16 @@ public void setPre2015MigrationStatus(Long pre2015MigrationStatus) { this.pre2015MigrationStatus = pre2015MigrationStatus; } + public Boolean isDiscarded() { + return discarded; + } + + public void setDiscarded(Boolean discarded) { + this.discarded = discarded; + this.discardedDate = Timestamp.valueOf(LocalDateTime.now()); + } + + public Timestamp getDiscardedDate() { + return discardedDate; + } } diff --git a/src/main/java/com/scmspain/services/TweetService.java b/src/main/java/com/scmspain/services/TweetService.java index d61bc9d..99be687 100644 --- a/src/main/java/com/scmspain/services/TweetService.java +++ b/src/main/java/com/scmspain/services/TweetService.java @@ -9,11 +9,16 @@ import javax.persistence.TypedQuery; import javax.transaction.Transactional; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; @Service @Transactional public class TweetService { + private static final String QUERY_LIST_ALL_TWEETS = "SELECT id FROM Tweet AS tweetId WHERE pre2015MigrationStatus<>99 AND discarded = FALSE ORDER BY id DESC"; + private static final String QUERY_LIST_ALL_DISCARDED_TWEETS = "SELECT id FROM Tweet AS tweetId WHERE discarded = TRUE ORDER BY discardedDate DESC"; + private EntityManager entityManager; private MetricWriter metricWriter; @@ -29,7 +34,10 @@ public TweetService(EntityManager entityManager, MetricWriter metricWriter) { Result - recovered Tweet */ public void publishTweet(String publisher, String text) { - if (publisher != null && publisher.length() > 0 && text != null && text.length() > 0 && text.length() < 140) { + + boolean publisherIsNotNullOrEmpty = publisher != null && publisher.length() > 0; + + if (publisherIsNotNullOrEmpty && tweetIsValid(text)) { Tweet tweet = new Tweet(); tweet.setTweet(text); tweet.setPublisher(publisher); @@ -51,18 +59,66 @@ public Tweet getTweet(Long id) { } /** - Recover tweet from repository - Parameter - id - id of the Tweet to retrieve - Result - retrieved Tweet + Recover tweets from repository + Result - retrieved Tweets */ public List listAllTweets() { List result = new ArrayList(); this.metricWriter.increment(new Delta("times-queried-tweets", 1)); - TypedQuery query = this.entityManager.createQuery("SELECT id FROM Tweet AS tweetId WHERE pre2015MigrationStatus<>99 ORDER BY id DESC", Long.class); + TypedQuery query = this.entityManager.createQuery(QUERY_LIST_ALL_TWEETS, Long.class); + List ids = query.getResultList(); + for (Long id : ids) { + result.add(getTweet(id)); + } + return result; + } + + /** + * Discard a tweet + * Parameter - id - id of the Tweet to discard + */ + public void discardTweet(Long id) { + Tweet tweetToDiscard = getTweet(id); + + if (tweetToDiscard != null) { + tweetToDiscard.setDiscarded(Boolean.TRUE); + + this.metricWriter.increment(new Delta("discarded-tweets", 1)); + this.entityManager.persist(tweetToDiscard); + } else { + throw new IllegalArgumentException("The selected tweet does not exists"); + } + } + + /** + Recover discarded tweets from repository + Result - retrieved Tweets + */ + public List listAllDiscardedTweets() { + List result = new ArrayList<>(); + this.metricWriter.increment(new Delta("times-queried-discarded-tweets", 1)); + TypedQuery query = this.entityManager.createQuery(QUERY_LIST_ALL_DISCARDED_TWEETS, Long.class); List ids = query.getResultList(); for (Long id : ids) { result.add(getTweet(id)); } return result; } + + private boolean tweetIsValid(String tweet) { + String linkRegex = "(.*)https?://(.*)"; + String space = " "; + + String tweetWithoutLink = tweet; + + if (tweet.matches(linkRegex)) { + tweetWithoutLink = Arrays.stream(tweet.split(space)) + .filter(word -> !word.matches(linkRegex)) + .collect(Collectors.joining(space)); + } + + boolean tweetIsNotNullOrEmpty = tweetWithoutLink != null && tweetWithoutLink.length() > 0; + + return tweetIsNotNullOrEmpty && tweetWithoutLink.length() < 140; + } } diff --git a/src/test/java/com/scmspain/controller/TweetControllerTest.java b/src/test/java/com/scmspain/controller/TweetControllerTest.java index 4368add..d86af60 100644 --- a/src/test/java/com/scmspain/controller/TweetControllerTest.java +++ b/src/test/java/com/scmspain/controller/TweetControllerTest.java @@ -1,7 +1,9 @@ package com.scmspain.controller; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.scmspain.configuration.TestConfiguration; +import com.scmspain.entities.Tweet; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -43,7 +45,9 @@ public void shouldReturn200WhenInsertingAValidTweet() throws Exception { @Test public void shouldReturn400WhenInsertingAnInvalidTweet() throws Exception { - mockMvc.perform(newTweet("Schibsted Spain", "We are Schibsted Spain (look at our home page http://www.schibsted.es/), we own Vibbo, InfoJobs, fotocasa, coches.net and milanuncios. Welcome!")) + mockMvc.perform(newTweet("Schibsted Spain", "We are Schibsted Spain (look at our home page, " + + "it's built on latest JS framework http://www.schibsted.es/), we own Vibbo, InfoJobs, fotocasa, " + + "coches.net and milanuncios. Welcome!")) .andExpect(status().is(400)); } @@ -60,10 +64,69 @@ public void shouldReturnAllPublishedTweets() throws Exception { assertThat(new ObjectMapper().readValue(content, List.class).size()).isEqualTo(1); } + @Test + @SuppressWarnings("unchecked") + public void shouldDiscardTweet() throws Exception { + mockMvc.perform(newTweet("Yo", "How are you?")); + + List tweets = getTweets(); + + int numberOfTweets = tweets.size(); + + assertThat(numberOfTweets).isGreaterThan(0); + + Long id = tweets.get(0).getId(); + + mockMvc.perform(discardTweet(id)) + .andExpect(status().is(200)); + + tweets = getTweets(); + assertThat(tweets.size()).isEqualTo(--numberOfTweets); + } + + @Test + public void shouldReturnAllDiscardedTweets() throws Exception { + mockMvc.perform(newTweet("1", "May be discarded")); + mockMvc.perform(newTweet("1", "May be discarded")); + mockMvc.perform(newTweet("1", "May be discarded")); + mockMvc.perform(newTweet("1", "May be discarded")); + + Long randomId = getTweets().stream() + .map(Tweet::getId) + .findAny() + .orElse(0L); + + mockMvc.perform(discardTweet(randomId)); + + MvcResult getResult = mockMvc.perform(get("/discarded")) + .andExpect(status().is(200)) + .andReturn(); + + String content = getResult.getResponse().getContentAsString(); + List discardedTweets = new ObjectMapper().readValue(content, new TypeReference>(){}); + + assertThat(discardedTweets.size()).isGreaterThanOrEqualTo(1); + } + private MockHttpServletRequestBuilder newTweet(String publisher, String tweet) { return post("/tweet") .contentType(MediaType.APPLICATION_JSON_UTF8) .content(format("{\"publisher\": \"%s\", \"tweet\": \"%s\"}", publisher, tweet)); } + private MockHttpServletRequestBuilder discardTweet(Long id) { + return post("/discarded") + .contentType(MediaType.APPLICATION_JSON_UTF8) + .content(format("{\"tweet\": %s}", id)); + } + + private List getTweets() throws Exception { + MvcResult getResult = mockMvc.perform(get("/tweet")) + .andExpect(status().is(200)) + .andReturn(); + + String content = getResult.getResponse().getContentAsString(); + return new ObjectMapper().readValue(content, new TypeReference>(){}); + } + } diff --git a/src/test/java/com/scmspain/services/TweetServiceTest.java b/src/test/java/com/scmspain/services/TweetServiceTest.java index ac88fe5..b70b4e8 100644 --- a/src/test/java/com/scmspain/services/TweetServiceTest.java +++ b/src/test/java/com/scmspain/services/TweetServiceTest.java @@ -35,4 +35,12 @@ public void shouldInsertANewTweet() throws Exception { public void shouldThrowAnExceptionWhenTweetLengthIsInvalid() throws Exception { tweetService.publishTweet("Pirate", "LeChuck? He's the guy that went to the Governor's for dinner and never wanted to leave. He fell for her in a big way, but she told him to drop dead. So he did. Then things really got ugly."); } + + @Test + public void shouldIgnoreLinksForCharacterLimit() { + tweetService.publishTweet("LeChuck", "Please visit my personal web page http://makelechuckgreatagain.com, plenty of eighties stuff " + + "like Alf pictures, Madonna music and so on. #nostalgia #backtothe80s"); + + verify(entityManager).persist(any(Tweet.class)); + } }