Skip to content
This repository has been archived by the owner on Dec 11, 2018. It is now read-only.

Feature/solution #28

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
17 changes: 15 additions & 2 deletions src/main/java/com/scmspain/configuration/TweetConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.scmspain.controller.TweetController;
import com.scmspain.services.TweetService;
import com.scmspain.validators.TweetValidator;
import com.scmspain.validators.UrlRemover;
import org.springframework.boot.actuate.metrics.writer.MetricWriter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -10,9 +12,20 @@

@Configuration
public class TweetConfiguration {

@Bean
public UrlRemover getUrlRemover() {
return new UrlRemover();
}

@Bean
public TweetValidator getTweetValidator(UrlRemover urlRemover) {
return new TweetValidator(urlRemover);
}

@Bean
public TweetService getTweetService(EntityManager entityManager, MetricWriter metricWriter) {
return new TweetService(entityManager, metricWriter);
public TweetService getTweetService(EntityManager entityManager, MetricWriter metricWriter, TweetValidator tweetValidator) {
return new TweetService(entityManager, metricWriter, tweetValidator);
}

@Bean
Expand Down
29 changes: 26 additions & 3 deletions src/main/java/com/scmspain/controller/TweetController.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
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;
import javassist.NotFoundException;
import org.springframework.web.bind.annotation.*;

import java.util.List;

import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.CREATED;
import static org.springframework.http.HttpStatus.*;

@RestController
public class TweetController {
Expand All @@ -26,7 +27,29 @@ public List<Tweet> listAllTweets() {
@PostMapping("/tweet")
@ResponseStatus(CREATED)
public void publishTweet(@RequestBody PublishTweetCommand publishTweetCommand) {
this.tweetService.publishTweet(publishTweetCommand.getPublisher(), publishTweetCommand.getTweet());
this.tweetService.publishTweet(new Tweet(publishTweetCommand.getPublisher(), publishTweetCommand.getTweet()));
}

@PostMapping("/discarded")
@ResponseStatus(OK)
public void discardTweet(@RequestBody DiscardTweetCommand discardTweetCommand) throws NotFoundException {
this.tweetService.discardTweet(discardTweetCommand.getTweet());
}

@GetMapping("/discarded")
@ResponseStatus(OK)
public List<Tweet> listAllDiscardedTweets() {
return this.tweetService.listAllDiscardedTweets();
}

@ExceptionHandler(NotFoundException.class)
@ResponseStatus(NOT_FOUND)
@ResponseBody
public Object notFoundException(NotFoundException ex) {
return new Object() {
public String message = ex.getMessage();
public String exceptionClass = ex.getClass().getSimpleName();
};
}

@ExceptionHandler(IllegalArgumentException.class)
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
41 changes: 39 additions & 2 deletions src/main/java/com/scmspain/entities/Tweet.java
Original file line number Diff line number Diff line change
@@ -1,23 +1,43 @@
package com.scmspain.entities;

import com.fasterxml.jackson.annotation.JsonIgnore;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.util.Date;

@Entity
public class Tweet {
public final class Tweet {
@Id
@GeneratedValue
private Long id;
@Column(nullable = false)
private String publisher;
@Column(nullable = false, length = 140)
/**
Todo for future versions define the limits for tweet's length and number of urls it can have

We can assume that a message including a url is not longer than 1000. This should be rethought.
It's more complicated than that on the real Twitter.
Leaving this number forever as 1000 gives opportunities for hackers trying to execute
time complexity attacks (see the `UrlRemover` class)
*/
@Column(nullable = false, length = 1000)
private String tweet;
@Column (nullable=true)
private Long pre2015MigrationStatus = 0L;
@JsonIgnore
@Column (nullable=true)
private Date discardDate;

public Tweet() {

}

public Tweet(String publisher, String tweet) {
this.publisher = publisher;
this.tweet = tweet;
}

public Long getId() {
Expand Down Expand Up @@ -52,4 +72,21 @@ public void setPre2015MigrationStatus(Long pre2015MigrationStatus) {
this.pre2015MigrationStatus = pre2015MigrationStatus;
}

public Date getDiscardDate() {
return discardDate;
}

public void setDiscardDate(Date discardDate) {
this.discardDate = discardDate;
}

@Override
public String toString() {
return "Tweet(id=" + id +
", publisher=" + publisher +
", tweet=" + tweet +
", discardDate=" + discardDate +
")";
}

}
43 changes: 33 additions & 10 deletions src/main/java/com/scmspain/services/TweetService.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.scmspain.services;

import com.scmspain.entities.Tweet;
import com.scmspain.validators.TweetValidator;
import javassist.NotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.metrics.writer.Delta;
import org.springframework.boot.actuate.metrics.writer.MetricWriter;
import org.springframework.stereotype.Service;
Expand All @@ -9,17 +12,21 @@
import javax.persistence.TypedQuery;
import javax.transaction.Transactional;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

@Service
@Transactional
public class TweetService {
private EntityManager entityManager;
private MetricWriter metricWriter;
private TweetValidator tweetValidator;

public TweetService(EntityManager entityManager, MetricWriter metricWriter) {
public TweetService(EntityManager entityManager, MetricWriter metricWriter, TweetValidator tweetValidator) {
this.entityManager = entityManager;
this.metricWriter = metricWriter;
this.tweetValidator = tweetValidator;
}

/**
Expand All @@ -28,19 +35,23 @@ public TweetService(EntityManager entityManager, MetricWriter metricWriter) {
Parameter - text - Content of the Tweet
Result - recovered Tweet
*/
public void publishTweet(String publisher, String text) {
if (publisher != null && publisher.length() > 0 && text != null && text.length() > 0 && text.length() < 140) {
Tweet tweet = new Tweet();
tweet.setTweet(text);
tweet.setPublisher(publisher);

public void publishTweet(Tweet tweet) {
if (tweetValidator.isValid(tweet)) {
this.metricWriter.increment(new Delta<Number>("published-tweets", 1));
this.entityManager.persist(tweet);
} else {
throw new IllegalArgumentException("Tweet must not be greater than 140 characters");
}
}

public void discardTweet(Long tweetId) throws NotFoundException {
Tweet tweet = getTweet(tweetId);
if (tweet == null)
throw new NotFoundException("The tweet id does not match any tweet");
tweet.setDiscardDate(new Date());
this.metricWriter.increment(new Delta<Number>("discarded-tweets", 1));
this.entityManager.persist(tweet);
}


/**
Recover tweet from repository
Parameter - id - id of the Tweet to retrieve
Expand All @@ -58,11 +69,23 @@ public Tweet getTweet(Long id) {
public List<Tweet> listAllTweets() {
List<Tweet> result = new ArrayList<Tweet>();
this.metricWriter.increment(new Delta<Number>("times-queried-tweets", 1));
TypedQuery<Long> query = this.entityManager.createQuery("SELECT id FROM Tweet AS tweetId WHERE pre2015MigrationStatus<>99 ORDER BY id DESC", Long.class);
TypedQuery<Long> query = this.entityManager.createQuery("SELECT id FROM Tweet AS tweetId WHERE discardDate is null and pre2015MigrationStatus<>99 ORDER BY id DESC", Long.class);
List<Long> ids = query.getResultList();
for (Long id : ids) {
result.add(getTweet(id));
}
return result;
}

public List<Tweet> listAllDiscardedTweets() {
List<Tweet> result = new ArrayList<>();
this.metricWriter.increment(new Delta<Number>("times-queried-discarded-tweets", 1));
TypedQuery<Long> query = this.entityManager.createQuery("SELECT id FROM Tweet AS tweetId WHERE discardDate is not null and pre2015MigrationStatus<>99 ORDER BY discardDate DESC", Long.class);
List<Long> ids = query.getResultList();
for (Long id : ids) {
Tweet t = getTweet(id);
result.add(t);
}
return result;
}
}
27 changes: 27 additions & 0 deletions src/main/java/com/scmspain/validators/TweetValidator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.scmspain.validators;

import com.scmspain.entities.Tweet;
import org.springframework.stereotype.Component;

@Component
public class TweetValidator {

private UrlRemover urlRemover;

public TweetValidator(UrlRemover urlRemover) {
this.urlRemover = urlRemover;
}

public boolean isValid(Tweet t) {
String tweetText = t.getTweet();
if (tweetText == null || tweetText.length() == 0)
throw new IllegalArgumentException("Tweet should not be empty.");
String textWithoutUrls = urlRemover.getTextWithoutUrls(tweetText);
if (140 < textWithoutUrls.length())
throw new IllegalArgumentException("Tweet should have at most 140 characters.");
else if (t.getPublisher() == null || t.getPublisher().length() == 0)
throw new IllegalArgumentException("Publisher should not be empty.");
else
return true;
}
}
24 changes: 24 additions & 0 deletions src/main/java/com/scmspain/validators/UrlRemover.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.scmspain.validators;

import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class UrlRemover {

public String getTextWithoutUrls(String text) {
String urlRegex = "((https?)://+[\\w\\d:#@%/;$()~_?\\+-=\\\\\\.&]*)";
String result = text.replaceAll(urlRegex, "");
return result;
}


// Todo make sure hackers don't send messages with hundreds of short links
String removeUrls(String s, List<String> urls) {
String result = s;
for (String url: urls)
result = result.replace(url, "");
return result;
}
}
Loading