From dfb34a1da9acc4470b0b1b116adb06209a422452 Mon Sep 17 00:00:00 2001 From: aeri Date: Wed, 15 Jan 2020 01:10:29 +0100 Subject: [PATCH] Last changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Víctor Peñasco Co-Authored-By: javiermixture17 --- README.md | 17 +- .../java/urlshortener/service/GetIPInfo.java | 9 +- .../web/UrlShortenerController.java | 652 +++++++++--------- src/main/resources/favicon.ico | Bin 12674 -> 108273 bytes 4 files changed, 321 insertions(+), 357 deletions(-) diff --git a/README.md b/README.md index 02eb3b1..fa29753 100755 --- a/README.md +++ b/README.md @@ -50,17 +50,22 @@ The following projects have been used to achieve this: * **PostDock** - *Dmitriy Paunin* - [postdock](https://github.com/paunin/PostDock) * **dockprom** - *Stefan Prodan* - [dockprom](https://github.com/stefanprodan/dockprom) +## Documentation + +The full documentation generated with Javadoc for this project is available at the following link: + +[Documentation](https://aeri.github.io/ulink/) + -## Contributors +## Creators -* [aeri](github.com/aeri) -* [vpec](github.com/vpec) -* [javiermixture17](github.com/javiermixture17) +* [aeri](https://github.com/aeri) +* [vpec](https://github.com/vpec) +* [javiermixture17](https://github.com/javiermixture17) ## License -Distributed under GNU General Public License v3.0. See `LICENSE` for more information. - +Distributed under GNU General Public License v3.0. See `LICENSE` for more information. \ No newline at end of file diff --git a/src/main/java/urlshortener/service/GetIPInfo.java b/src/main/java/urlshortener/service/GetIPInfo.java index 7d31c43..36457b0 100644 --- a/src/main/java/urlshortener/service/GetIPInfo.java +++ b/src/main/java/urlshortener/service/GetIPInfo.java @@ -7,7 +7,6 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.Environment; -import org.springframework.core.io.ResourceLoader; import org.springframework.stereotype.Service; import urlshortener.web.UrlShortenerController; @@ -26,10 +25,7 @@ public class GetIPInfo { @Autowired private Environment EN_US_PATH; - @Autowired - ResourceLoader resourceLoader; - - private File file; + private static File file; public static String token; public static String en_us; @@ -44,7 +40,7 @@ public class GetIPInfo { public void init() { token = IPINFO_TOKEN.getProperty("ipinfo.token"); en_us = EN_US_PATH.getProperty("path.en_us"); - file= new File(en_us); + file = new File(en_us); } @@ -57,7 +53,6 @@ public void init() { * @throws IOException */ public IPResponse getIpResponse(String addr) throws RateLimitedException, IOException { - log.info("BCD"); ipInfo = IPInfo.builder().setToken(token) .setCountryFile(file).build(); diff --git a/src/main/java/urlshortener/web/UrlShortenerController.java b/src/main/java/urlshortener/web/UrlShortenerController.java index cdc91c2..e2d4fe8 100644 --- a/src/main/java/urlshortener/web/UrlShortenerController.java +++ b/src/main/java/urlshortener/web/UrlShortenerController.java @@ -28,367 +28,331 @@ import java.sql.Timestamp; import java.time.Duration; - import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; -import java.util.regex.Pattern; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.core.env.Environment; @Controller public class UrlShortenerController { - private final ShortURLService shortUrlService; - - private final ClickService clickService; - private final PlatformService platformService = new PlatformService(); - private final BrowserService browserService = new BrowserService(); - private final GetIPInfo getIPInfo = new GetIPInfo(); - - - @Autowired - private Environment LATENCY_PERIOD; - - private static Pattern pDomainNameOnly; - private static final String DOMAIN_NAME_PATTERN = "^((?!-)[A-Za-z0-9-]{1,63}(? - * This method checks using Google Safe Browsing if original url - * is save. If it is, redirects the user. If it is not, redirects - * to a page that informs the using about the danger. If an error - * occurs, it returns a page showing the specific error. - * - * @param id shortened url provided - * @param request HttpServletRequest request - * @return ModelAndView containing redirection page or error - */ - @RequestMapping(value = "/{id:(?!link|index).*}", method = RequestMethod.GET) - public ModelAndView redirectTo(@PathVariable String id, HttpServletRequest request) { - long start = System.currentTimeMillis(); - log.info(id); - ShortURL l = shortUrlService.findByKey(id); - String countryName = null; - String countryCode = null; - String platform; - String browser; - try { - String userAgent = request.getHeader("User-Agent"); - platform = platformService.getPlatform(userAgent); - browser = browserService.getBrowser(userAgent); - } catch (NullPointerException e) { - platform = "Unknown"; - browser = "Unknown"; - } - if (l != null) { - try { - log.info("ABC"); - IPResponse response = getIPInfo.getIpResponse(request.getRemoteAddr()); - countryName = response.getCountryName(); - countryCode = response.getCountryCode(); - } catch (NullPointerException e) { - log.debug("Failed to retrieve IP file"); - ModelAndView model = new ModelAndView("error500"); - model.setStatus(HttpStatus.INTERNAL_SERVER_ERROR); - return model; - } - catch (RateLimitedException | IOException ex) { - log.debug("RateLimitedException"); - } - long end = System.currentTimeMillis(); - clickService.saveClick(id, extractIP(request), countryName, countryCode, platform, browser, end - start); - if (!l.getSafe().isEmpty()) { - ModelAndView modelAndView = new ModelAndView("warning"); - modelAndView.addObject("malware", l.getSafe()); - modelAndView.addObject("link", l.getTarget()); - return modelAndView; - } else { - request.setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, HttpStatus.TEMPORARY_REDIRECT); - return new ModelAndView("redirect:" + l.getTarget()); - } - } else { - ModelAndView model = new ModelAndView("error"); - model.setStatus(HttpStatus.NOT_FOUND); - return model; - } - } - - - /** - * Shorts the url provided - *

- * Checks if the provided url is reachable, if it is, - * returns the shortened url to the user. If it is not, - * informs the user about it so he/she can confirm that - * wants to shorten the url. It also checks if url is safe - * using Google Safe Browsing. If an error occurs, it - * returns a page showing the specific error. - * - * @param url link that is going to be shortened - * @param request HttpServletRequest request - * @return ResponseEntity containing ShortUrl object - */ - @RequestMapping(value = "/link", method = RequestMethod.POST) - @ResponseBody - public ResponseEntity shortener(@RequestParam(value = "url", required = true) String url, - HttpServletRequest request) { - - UrlValidator urlValidator = new UrlValidator(new String[] { "http", "https", "ftp" }); - HttpHeaders h = new HttpHeaders(); - if (!validateHTTP_URI(url)) { - url = "http://" + url; - } - if (urlValidator.isValid(url)) { - final String toU = url; - RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); - RestTemplate rest = restTemplateBuilder.setConnectTimeout(Duration.ofMillis(700)) - .setReadTimeout(Duration.ofMillis(700)).build(); - HttpEntity requestEntity = new HttpEntity("", h); - CompletableFuture> fResponse = CompletableFuture.supplyAsync(() -> { - try { - return getResponse(rest, toU, requestEntity); - } catch (SocketTimeoutException | HttpClientErrorException e) { - // e.printStackTrace(); - log.info(e.getMessage()); - log.info("Peticion incorrecta Timeout"); - return new ResponseEntity(h, HttpStatus.GATEWAY_TIMEOUT); - } - }); - CheckGSB checkGSB = new CheckGSB(); - String notSafe; - try { - log.debug(url); - // Check url in Google Safe Browsing - notSafe = checkGSB.checkSingleUrl(url); - } catch (GoogleJsonResponseException e) { - log.debug("Google Safe Browsing quota exceeded"); - notSafe = ""; - } catch (GeneralSecurityException | IOException e1) { - e1.printStackTrace(); - ShortURL su = new ShortURL(url, false); - return new ResponseEntity<>(su, h, HttpStatus.INTERNAL_SERVER_ERROR); - } - try { - ResponseEntity response; - response = fResponse.get(); - log.debug("Status code: " + response.getStatusCode()); - if (response.getStatusCode() == HttpStatus.GATEWAY_TIMEOUT) { - log.debug("Peticion incorrecta HttpClientErrorException"); - ShortURL su = new ShortURL(url, false); - return new ResponseEntity<>(su, h, HttpStatus.GATEWAY_TIMEOUT); - } else { - ShortURL su = shortUrlService.save(url, request.getRemoteAddr(), notSafe); - h.setLocation(su.getUri()); - log.debug("Peticion correcta"); - return new ResponseEntity<>(su, h, HttpStatus.CREATED); - } - } catch (ResourceAccessException e) { // Timeout OR Unknown host - log.debug(e.getMessage()); - log.debug("Peticion incorrecta Timeout"); - ShortURL su = new ShortURL(url, false); - return new ResponseEntity<>(su, h, HttpStatus.GATEWAY_TIMEOUT); - } catch (NullPointerException e) { - log.info(e.getMessage()); - log.info("Fallo al guardar url en la base"); - ShortURL su = new ShortURL(url, false); - return new ResponseEntity<>(su, h, HttpStatus.INTERNAL_SERVER_ERROR); - } catch (InterruptedException | ExecutionException e) { - log.info(e.getMessage()); - log.info("Error HTTP asincrono"); - ShortURL su = new ShortURL(url, false); - return new ResponseEntity<>(su, h, HttpStatus.GATEWAY_TIMEOUT); - } - } else { - ShortURL su = new ShortURL(url, false); - return new ResponseEntity<>(su, h, HttpStatus.BAD_REQUEST); - } - } - - /** - * Executes a GET HTTP request as a client a returns response - * - * @param rest RestTemplate object - * @param url server address - * @param requestEntity HttpEntity containing a string - * @return response from the server - * @throws SocketTimeoutException - */ - ResponseEntity getResponse(RestTemplate rest, String url, HttpEntity requestEntity) - throws SocketTimeoutException { - // Execute http get request as client - return rest.exchange(url, HttpMethod.GET, requestEntity, String.class); - } - - /** - * Shortens a url, despite it is reachable or not. - * - * @param url link that is going to be shortened - * @param request HttpServletRequest request - * @return ResponseEntity containing ShortUrl object - */ - @RequestMapping(value = "/linkConfirm", method = RequestMethod.POST) - @ResponseBody - public ResponseEntity shortenerConfirm(@RequestParam("url") String url, - HttpServletRequest request) { - log.debug("linkConfirm Request"); - UrlValidator urlValidator = new UrlValidator(new String[] { "http", "https", "ftp" }); - - if (isValidDomainName(url)) { - url = "http://" + url; - } - if (urlValidator.isValid(url)) { - HttpHeaders h = new HttpHeaders(); - CheckGSB checkGSB = new CheckGSB(); - String notSafe; - try { - // Check url in Google Safe Browsing - notSafe = checkGSB.checkSingleUrl(url); - } - catch (GoogleJsonResponseException e) { - log.debug("Google Safe Browsing quota exceeded"); - notSafe = ""; - } - catch (GeneralSecurityException | IOException e1) { - e1.printStackTrace(); - ShortURL su = new ShortURL(url, false); - return new ResponseEntity<>(su, h, HttpStatus.INTERNAL_SERVER_ERROR); - } - ShortURL su = shortUrlService.save(url, request.getRemoteAddr(), notSafe); - h.setLocation(su.getUri()); - return new ResponseEntity<>(su, h, HttpStatus.CREATED); - } else { - return new ResponseEntity<>(HttpStatus.BAD_REQUEST); - } - } - - /** - * Extracts IP address from a request - * - * @param request HttpServletRequest request - * @return IP extracted from the request - */ - private String extractIP(HttpServletRequest request) { - return request.getRemoteAddr(); - } - - - /** - * Returns global statistics and global information about the system - * - * @param request HttpServletRequest request - * @return ModelAndView containing global statistics and information - * @throws Throwable - */ - @GetMapping("/statistics") - public ModelAndView statistics(HttpServletRequest request) throws Throwable { - GetStats getStats = new GetStats(); - String minutes = LATENCY_PERIOD.getProperty("metric.latency.period"); - Timestamp since = new Timestamp( - System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(Long.parseLong(minutes))); + private final ShortURLService shortUrlService; + + private final ClickService clickService; + private final PlatformService platformService = new PlatformService(); + private final BrowserService browserService = new BrowserService(); + private final GetIPInfo getIPInfo = new GetIPInfo(); + + @Autowired + private Environment LATENCY_PERIOD; + + private static final Logger log = LoggerFactory.getLogger(UrlShortenerController.class); + + public UrlShortenerController(ShortURLService shortUrlService, ClickService clickService) { + this.shortUrlService = shortUrlService; + this.clickService = clickService; + } + + /** + * Checks if a provided URI is valid + * + * @param uri URI that is going to be validated + * @return true if URI is valid, false otherwise + */ + + public static boolean validateHTTP_URI(String uri) { + final URL url; + try { + url = new URL(uri); + } catch (Exception e1) { + return false; + } + return "http".equals(url.getProtocol()) || "https".equals(url.getProtocol()); + } + + /** + * Given a shortened url redirects the user to the original url + *

+ * This method checks using Google Safe Browsing if original url is save. If it + * is, redirects the user. If it is not, redirects to a page that informs the + * using about the danger. If an error occurs, it returns a page showing the + * specific error. + * + * @param id shortened url provided + * @param request HttpServletRequest request + * @return ModelAndView containing redirection page or error + */ + @RequestMapping(value = "/{id:(?!link|index).*}", method = RequestMethod.GET) + public ModelAndView redirectTo(@PathVariable String id, HttpServletRequest request) { + long start = System.currentTimeMillis(); + log.info(id); + ShortURL l = shortUrlService.findByKey(id); + String countryName = null; + String countryCode = null; + String platform; + String browser; + try { + String userAgent = request.getHeader("User-Agent"); + platform = platformService.getPlatform(userAgent); + browser = browserService.getBrowser(userAgent); + } catch (NullPointerException e) { + platform = "Unknown"; + browser = "Unknown"; + } + if (l != null) { + try { + IPResponse response = getIPInfo.getIpResponse(request.getRemoteAddr()); + countryName = response.getCountryName(); + countryCode = response.getCountryCode(); + } catch (NullPointerException e) { + log.debug("Failed to retrieve IP file"); + ModelAndView model = new ModelAndView("error500"); + model.setStatus(HttpStatus.INTERNAL_SERVER_ERROR); + return model; + } catch (RateLimitedException | IOException ex) { + log.debug("RateLimitedException"); + } + long end = System.currentTimeMillis(); + clickService.saveClick(id, extractIP(request), countryName, countryCode, platform, browser, end - start); + if (!l.getSafe().isEmpty()) { + ModelAndView modelAndView = new ModelAndView("warning"); + modelAndView.addObject("malware", l.getSafe()); + modelAndView.addObject("link", l.getTarget()); + return modelAndView; + } else { + request.setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, HttpStatus.TEMPORARY_REDIRECT); + return new ModelAndView("redirect:" + l.getTarget()); + } + } else { + ModelAndView model = new ModelAndView("error"); + model.setStatus(HttpStatus.NOT_FOUND); + return model; + } + } + + /** + * Shorts the url provided + *

+ * Checks if the provided url is reachable, if it is, returns the shortened url + * to the user. If it is not, informs the user about it so he/she can confirm + * that wants to shorten the url. It also checks if url is safe using Google + * Safe Browsing. If an error occurs, it returns a page showing the specific + * error. + * + * @param url link that is going to be shortened + * @param request HttpServletRequest request + * @return ResponseEntity containing ShortUrl object + */ + @RequestMapping(value = "/link", method = RequestMethod.POST) + @ResponseBody + public ResponseEntity shortener(@RequestParam(value = "url", required = true) String url, + HttpServletRequest request) { + + UrlValidator urlValidator = new UrlValidator(new String[] { "http", "https", "ftp" }); + HttpHeaders h = new HttpHeaders(); + if (!validateHTTP_URI(url)) { + url = "http://" + url; + } + if (urlValidator.isValid(url)) { + final String toU = url; + RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); + RestTemplate rest = restTemplateBuilder.setConnectTimeout(Duration.ofMillis(1500)) + .setReadTimeout(Duration.ofMillis(1500)).build(); + HttpEntity requestEntity = new HttpEntity("", h); + CompletableFuture> fResponse = CompletableFuture.supplyAsync(() -> { + try { + return getResponse(rest, toU, requestEntity); + } catch (SocketTimeoutException | HttpClientErrorException e) { + // e.printStackTrace(); + log.info(e.getMessage()); + log.info("Peticion incorrecta Timeout"); + return new ResponseEntity(h, HttpStatus.GATEWAY_TIMEOUT); + } + }); + CheckGSB checkGSB = new CheckGSB(); + String notSafe; + try { + log.debug(url); + // Check url in Google Safe Browsing + notSafe = checkGSB.checkSingleUrl(url); + } catch (GoogleJsonResponseException e) { + log.debug("Google Safe Browsing quota exceeded"); + notSafe = ""; + } catch (GeneralSecurityException | IOException e1) { + e1.printStackTrace(); + ShortURL su = new ShortURL(url, false); + return new ResponseEntity<>(su, h, HttpStatus.INTERNAL_SERVER_ERROR); + } + try { + ResponseEntity response; + response = fResponse.get(); + log.debug("Status code: " + response.getStatusCode()); + if (response.getStatusCode() == HttpStatus.GATEWAY_TIMEOUT) { + log.debug("Peticion incorrecta HttpClientErrorException"); + ShortURL su = new ShortURL(url, false); + return new ResponseEntity<>(su, h, HttpStatus.GATEWAY_TIMEOUT); + } else { + ShortURL su = shortUrlService.save(url, request.getRemoteAddr(), notSafe); + h.setLocation(su.getUri()); + log.debug("Peticion correcta"); + return new ResponseEntity<>(su, h, HttpStatus.CREATED); + } + } catch (ResourceAccessException e) { // Timeout OR Unknown host + log.debug(e.getMessage()); + log.debug("Peticion incorrecta Timeout"); + ShortURL su = new ShortURL(url, false); + return new ResponseEntity<>(su, h, HttpStatus.GATEWAY_TIMEOUT); + } catch (NullPointerException e) { + log.info(e.getMessage()); + log.info("Fallo al guardar url en la base"); + ShortURL su = new ShortURL(url, false); + return new ResponseEntity<>(su, h, HttpStatus.INTERNAL_SERVER_ERROR); + } catch (InterruptedException | ExecutionException e) { + log.info(e.getMessage()); + log.info("Error HTTP asincrono"); + ShortURL su = new ShortURL(url, false); + return new ResponseEntity<>(su, h, HttpStatus.GATEWAY_TIMEOUT); + } + } else { + ShortURL su = new ShortURL(url, false); + return new ResponseEntity<>(su, h, HttpStatus.BAD_REQUEST); + } + } + + /** + * Executes a GET HTTP request as a client a returns response + * + * @param rest RestTemplate object + * @param url server address + * @param requestEntity HttpEntity containing a string + * @return response from the server + * @throws SocketTimeoutException + */ + ResponseEntity getResponse(RestTemplate rest, String url, HttpEntity requestEntity) + throws SocketTimeoutException { + // Execute http get request as client + return rest.exchange(url, HttpMethod.GET, requestEntity, String.class); + } + + /** + * Shortens a url, despite it is reachable or not. + * + * @param url link that is going to be shortened + * @param request HttpServletRequest request + * @return ResponseEntity containing ShortUrl object + */ + @RequestMapping(value = "/linkConfirm", method = RequestMethod.POST) + @ResponseBody + public ResponseEntity shortenerConfirm(@RequestParam("url") String url, HttpServletRequest request) { + log.debug("linkConfirm Request"); + HttpHeaders h = new HttpHeaders(); + CheckGSB checkGSB = new CheckGSB(); + String notSafe; + try { + // Check url in Google Safe Browsing + notSafe = checkGSB.checkSingleUrl(url); + } catch (GoogleJsonResponseException e) { + log.debug("Google Safe Browsing quota exceeded"); + notSafe = ""; + } catch (GeneralSecurityException | IOException e1) { + e1.printStackTrace(); + ShortURL su = new ShortURL(url, false); + return new ResponseEntity<>(su, h, HttpStatus.INTERNAL_SERVER_ERROR); + } + ShortURL su = shortUrlService.save(url, request.getRemoteAddr(), notSafe); + h.setLocation(su.getUri()); + return new ResponseEntity<>(su, h, HttpStatus.CREATED); + + } + + /** + * Extracts IP address from a request + * + * @param request HttpServletRequest request + * @return IP extracted from the request + */ + private String extractIP(HttpServletRequest request) { + return request.getRemoteAddr(); + } + + /** + * Returns global statistics and global information about the system + * + * @param request HttpServletRequest request + * @return ModelAndView containing global statistics and information + * @throws Throwable + */ + @GetMapping("/statistics") + public ModelAndView statistics(HttpServletRequest request) throws Throwable { + GetStats getStats = new GetStats(); + String minutes = LATENCY_PERIOD.getProperty("metric.latency.period"); + Timestamp since = new Timestamp( + System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(Long.parseLong(minutes))); ModelAndView modelAndView = new ModelAndView("statistics"); modelAndView.addAllObjects(getStats.getGlobal(clickService, shortUrlService, since)); return modelAndView; - } - - /** - * Returns link statistics access page - * - * @param request HttpServletRequest request - * @return ModelAndView presenting link stats access page - */ - @GetMapping("/link-stats-access") - public ModelAndView linkStatsAccess(HttpServletRequest request) { - ModelAndView modelAndView; - modelAndView = new ModelAndView("link-stats-access"); - modelAndView.addObject("failedAccess", ""); - return modelAndView; - } - - /** - * Returns the user statistics about provided shortened url - * - * @param shortenedUrl shortened url which stats are going to be returned - * @param code access code for the shortened url - * @return ModelAndView containing link stats - * @throws Throwable - */ - @PostMapping("/linkStats") - public ModelAndView linkStats(@RequestParam("shortenedUrl") String shortenedUrl, @RequestParam("code") String code) - throws Throwable { - ModelAndView modelAndView; + } + + /** + * Returns link statistics access page + * + * @param request HttpServletRequest request + * @return ModelAndView presenting link stats access page + */ + @GetMapping("/link-stats-access") + public ModelAndView linkStatsAccess(HttpServletRequest request) { + ModelAndView modelAndView; + modelAndView = new ModelAndView("link-stats-access"); + modelAndView.addObject("failedAccess", ""); + return modelAndView; + } + + /** + * Returns the user statistics about provided shortened url + * + * @param shortenedUrl shortened url which stats are going to be returned + * @param code access code for the shortened url + * @return ModelAndView containing link stats + * @throws Throwable + */ + @PostMapping("/linkStats") + public ModelAndView linkStats(@RequestParam("shortenedUrl") String shortenedUrl, @RequestParam("code") String code) + throws Throwable { + ModelAndView modelAndView; String hashId = shortenedUrl.substring(shortenedUrl.lastIndexOf('/') + 1); ShortURL l = shortUrlService.findByKeyCode(hashId, code); - // Check authentication + // Check authentication if (l == null) { modelAndView = new ModelAndView("link-stats-access"); modelAndView.addObject("failedAccess", "Incorrect shortened URL or code"); modelAndView.setStatus(HttpStatus.FORBIDDEN); - } - else{ - modelAndView = new ModelAndView("link-stats"); - modelAndView.addObject("url", hashId); - GetStats getStats = new GetStats(); - modelAndView.addAllObjects(getStats.getLocal(clickService, shortUrlService, hashId)); - } - return modelAndView; - } - - /** - * Transforms a url into a QR image - * - * @param link shortened url to be transformed to qr - * @return response entity containing qr image as byte stream - */ - @ResponseBody - @RequestMapping(value = "/qr", method = RequestMethod.GET, produces = MediaType.IMAGE_PNG_VALUE) - public ResponseEntity qr(@RequestParam("link") String link) { - HttpHeaders h = new HttpHeaders(); - RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); - - RestTemplate rest = restTemplateBuilder.setConnectTimeout(Duration.ofMillis(700)) - .setReadTimeout(Duration.ofMillis(2000)).build(); - HttpEntity requestEntity = new HttpEntity("", h); - String url = "https://chart.googleapis.com/chart?cht=qr&chl=" + link + "&chs=160x160&chld=L|0"; - return rest.exchange(url, HttpMethod.GET, requestEntity, byte[].class); - } + } else { + modelAndView = new ModelAndView("link-stats"); + modelAndView.addObject("url", hashId); + GetStats getStats = new GetStats(); + modelAndView.addAllObjects(getStats.getLocal(clickService, shortUrlService, hashId)); + } + return modelAndView; + } + + /** + * Transforms a url into a QR image + * + * @param link shortened url to be transformed to qr + * @return response entity containing qr image as byte stream + */ + @ResponseBody + @RequestMapping(value = "/qr", method = RequestMethod.GET, produces = MediaType.IMAGE_PNG_VALUE) + public ResponseEntity qr(@RequestParam("link") String link) { + HttpHeaders h = new HttpHeaders(); + RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); + + RestTemplate rest = restTemplateBuilder.setConnectTimeout(Duration.ofMillis(700)) + .setReadTimeout(Duration.ofMillis(2000)).build(); + HttpEntity requestEntity = new HttpEntity("", h); + String url = "https://chart.googleapis.com/chart?cht=qr&chl=" + link + "&chs=160x160&chld=L|0"; + return rest.exchange(url, HttpMethod.GET, requestEntity, byte[].class); + } } diff --git a/src/main/resources/favicon.ico b/src/main/resources/favicon.ico index c89443c8e8aa13b471ebcf49323b06b080c4d868..70dc2833586eefcbf546294daf210f395b573e3f 100644 GIT binary patch literal 108273 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 literal 12674 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 Sy}*A_0Lt=j<*H@PKmH&32j-0c