From ead9c71464b6bcb0ffba9014c7e53fc63e635009 Mon Sep 17 00:00:00 2001 From: Jason House Date: Thu, 23 May 2019 15:56:42 -0400 Subject: [PATCH] Hardening code Adding better error handling Adding address/port support --- Dockerfile | 4 +- src/main/java/com/jasonhhouse/Gaps/Gaps.java | 20 +++- .../com/jasonhhouse/Gaps/GapsController.java | 65 ++++++++++++- .../com/jasonhhouse/Gaps/GapsSearchBean.java | 92 ++++++++++++------- src/main/resources/static/index.html | 24 ++++- src/main/resources/static/js/index.js | 43 +++++---- test_cases.txt | 42 +++++++++ 7 files changed, 225 insertions(+), 65 deletions(-) create mode 100644 test_cases.txt diff --git a/Dockerfile b/Dockerfile index f3d9537..7e6dde4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,11 @@ FROM openjdk:11.0.3-jre-slim +EXPOSE 32400 + RUN mkdir -p /usr/src/app WORKDIR /usr/src/app COPY target/Gaps-0.0.4.jar /usr/src/app/ #ENTRYPOINT ["java", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005", "-jar", "Gaps-0.0.4.jar"] -ENTRYPOINT ["java", "-jar", "Gaps-0.0.4.jar"] \ No newline at end of file +ENTRYPOINT ["java", "-jar", "Gaps-0.0.4.jar"] diff --git a/src/main/java/com/jasonhhouse/Gaps/Gaps.java b/src/main/java/com/jasonhhouse/Gaps/Gaps.java index ad292f7..96fc57c 100644 --- a/src/main/java/com/jasonhhouse/Gaps/Gaps.java +++ b/src/main/java/com/jasonhhouse/Gaps/Gaps.java @@ -21,10 +21,12 @@ public class Gaps { private List movieUrls; + private Boolean searchFromFolder; + public Gaps() { } - public Gaps(String movieDbApiKey, Boolean writeToFile, String movieDbListId, Boolean searchFromPlex, Integer connectTimeout, Integer writeTimeout, Integer readTimeout, List movieUrls) { + public Gaps(String movieDbApiKey, Boolean writeToFile, String movieDbListId, Boolean searchFromPlex, Integer connectTimeout, Integer writeTimeout, Integer readTimeout, List movieUrls, Boolean searchFromFolder) { this.movieDbApiKey = movieDbApiKey; this.writeToFile = writeToFile; this.movieDbListId = movieDbListId; @@ -33,6 +35,7 @@ public class Gaps { this.writeTimeout = writeTimeout; this.readTimeout = readTimeout; this.movieUrls = movieUrls; + this.searchFromFolder = searchFromFolder; } public String getMovieDbApiKey() { @@ -99,6 +102,14 @@ public class Gaps { this.movieUrls = movieUrls; } + public Boolean getSearchFromFolder() { + return searchFromFolder; + } + + public void setSearchFromFolder(Boolean searchFromFolder) { + this.searchFromFolder = searchFromFolder; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -111,12 +122,13 @@ public class Gaps { Objects.equals(connectTimeout, gaps.connectTimeout) && Objects.equals(writeTimeout, gaps.writeTimeout) && Objects.equals(readTimeout, gaps.readTimeout) && - Objects.equals(movieUrls, gaps.movieUrls); + Objects.equals(movieUrls, gaps.movieUrls) && + Objects.equals(searchFromFolder, gaps.searchFromFolder); } @Override public int hashCode() { - return Objects.hash(movieDbApiKey, writeToFile, movieDbListId, searchFromPlex, connectTimeout, writeTimeout, readTimeout, movieUrls); + return Objects.hash(movieDbApiKey, writeToFile, movieDbListId, searchFromPlex, connectTimeout, writeTimeout, readTimeout, movieUrls, searchFromFolder); } @Override @@ -130,6 +142,8 @@ public class Gaps { ", writeTimeout=" + writeTimeout + ", readTimeout=" + readTimeout + ", movieUrls=" + movieUrls + + ", searchFromFolder=" + searchFromFolder + '}'; } + } diff --git a/src/main/java/com/jasonhhouse/Gaps/GapsController.java b/src/main/java/com/jasonhhouse/Gaps/GapsController.java index c7f590b..1269858 100644 --- a/src/main/java/com/jasonhhouse/Gaps/GapsController.java +++ b/src/main/java/com/jasonhhouse/Gaps/GapsController.java @@ -1,8 +1,9 @@ package com.jasonhhouse.Gaps; import java.util.Set; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -10,12 +11,13 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; +import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.context.request.async.DeferredResult; +import org.springframework.web.server.ResponseStatusException; @Controller public class GapsController { @@ -33,8 +35,62 @@ public class GapsController { @ResponseStatus(value = HttpStatus.OK) public Future>> submit(@RequestBody Gaps gaps) { logger.info("submit()"); - Future future = runInOtherThread(gaps); - return future; + + //Error checking + if (StringUtils.isEmpty(gaps.getMovieDbApiKey())) { + String reason = "Missing Movie DB Api Key. This field is required for Gaps."; + logger.error(reason); + + Exception e = new IllegalArgumentException(); + throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, reason, e); + } + + if (BooleanUtils.isNotTrue(gaps.getSearchFromPlex()) && BooleanUtils.isNotTrue(gaps.getSearchFromFolder())) { + String reason = "Must search from Plex and/or folders. One or both of these fields is required for Gaps."; + logger.error(reason); + + Exception e = new IllegalArgumentException(); + throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, reason, e); + } + + if (BooleanUtils.isNotFalse(gaps.getSearchFromPlex())) { + if (CollectionUtils.isEmpty(gaps.getMovieUrls())) { + String reason = "Missing Plex movie collection urls. This field is required to search from Plex."; + logger.error(reason); + + Exception e = new IllegalArgumentException(); + throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, reason, e); + } else { + for(String url : gaps.getMovieUrls()) { + if(StringUtils.isEmpty(url)) { + String reason = "Found empty Plex movie collection url. This field is required to search from Plex."; + logger.error(reason); + + Exception e = new IllegalArgumentException(); + throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, reason, e); + } + } + } + } + + //Fill in default values if missing + if(gaps.getWriteTimeout() == null) { + logger.info("Missing write timeout. Setting default to 180 seconds."); + gaps.setWriteTimeout(180); + } + + if(gaps.getConnectTimeout() == null) { + logger.info("Missing connect timeout. Setting default to 180 seconds."); + gaps.setConnectTimeout(180); + } + + if(gaps.getReadTimeout() == null) { + logger.info("Missing read timeout. Setting default to 180 seconds."); + gaps.setReadTimeout(180); + } + + + return runInOtherThread(gaps); } @RequestMapping(value = "/status", method = RequestMethod.GET) @@ -66,4 +122,5 @@ public class GapsController { return gapsSearch.run(properties); } + } diff --git a/src/main/java/com/jasonhhouse/Gaps/GapsSearchBean.java b/src/main/java/com/jasonhhouse/Gaps/GapsSearchBean.java index 381b312..1ae4983 100644 --- a/src/main/java/com/jasonhhouse/Gaps/GapsSearchBean.java +++ b/src/main/java/com/jasonhhouse/Gaps/GapsSearchBean.java @@ -45,6 +45,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; +import org.springframework.web.server.ResponseStatusException; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; @@ -54,11 +55,17 @@ import org.xml.sax.SAXException; public class GapsSearchBean implements GapsSearch { private final Logger logger = LoggerFactory.getLogger(GapsSearchBean.class); + private final Set searched; + private final Set recommended; + private final Set ownedMovies; + private final AtomicInteger totalMovieCount; + private final AtomicInteger searchedMovieCount; + private Properties properties; public GapsSearchBean() { @@ -301,45 +308,60 @@ public class GapsSearchBean implements GapsSearch { } for (String url : urls) { - Request request = new Request.Builder() - .url(url) - .build(); + try { + Request request = new Request.Builder() + .url(url) + .build(); - try (Response response = client.newCall(request).execute()) { - String body = response.body() != null ? response.body().string() : null; + try (Response response = client.newCall(request).execute()) { + String body = response.body() != null ? response.body().string() : null; - if (body == null) { - logger.error("Body returned null from Plex"); - return; - } - - InputStream fileIS = new ByteArrayInputStream(body.getBytes()); - DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); - DocumentBuilder builder = builderFactory.newDocumentBuilder(); - Document xmlDocument = builder.parse(fileIS); - XPath xPath = XPathFactory.newInstance().newXPath(); - String expression = "/MediaContainer/Video"; - NodeList nodeList = (NodeList) xPath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET); - - for (int i = 0; i < nodeList.getLength() && i < 25; i++) { - Node node = nodeList.item(i); - //Files can't have : so need to remove to find matches correctly - String title = node.getAttributes().getNamedItem("title").getNodeValue().replaceAll(":", ""); - if (node.getAttributes().getNamedItem("year") == null) { - logger.warn("Year not found for " + title); - continue; + if (body == null) { + logger.error("Body returned null from Plex"); + return; } - String year = node.getAttributes().getNamedItem("year").getNodeValue(); - Movie movie = new Movie(-1, title, Integer.parseInt(year), ""); - ownedMovies.add(movie); - totalMovieCount.incrementAndGet(); - } - logger.info(ownedMovies.size() + " movies found in plex"); - } catch (IOException e) { - logger.error("Error connecting to Plex to get Movie list", e); - } catch (ParserConfigurationException | XPathExpressionException | SAXException e) { - logger.error("Error parsing XML from Plex", e); + InputStream fileIS = new ByteArrayInputStream(body.getBytes()); + DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = builderFactory.newDocumentBuilder(); + Document xmlDocument = builder.parse(fileIS); + XPath xPath = XPathFactory.newInstance().newXPath(); + String expression = "/MediaContainer/Video"; + NodeList nodeList = (NodeList) xPath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET); + + if(nodeList.getLength() == 0) { + String reason = "No movies found in url: " + url; + logger.warn(reason); + } + + for (int i = 0; i < nodeList.getLength(); i++) { + Node node = nodeList.item(i); + //Files can't have : so need to remove to find matches correctly + String title = node.getAttributes().getNamedItem("title").getNodeValue().replaceAll(":", ""); + if (node.getAttributes().getNamedItem("year") == null) { + logger.warn("Year not found for " + title); + continue; + } + String year = node.getAttributes().getNamedItem("year").getNodeValue(); + Movie movie = new Movie(-1, title, Integer.parseInt(year), ""); + ownedMovies.add(movie); + totalMovieCount.incrementAndGet(); + } + logger.info(ownedMovies.size() + " movies found in plex"); + + } catch (IOException e) { + String reason = "Error connecting to Plex to get Movie list: " + url; + logger.error(reason, e); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, reason, e); + } catch (ParserConfigurationException | XPathExpressionException | SAXException e) { + String reason = "Error parsing XML from Plex: " + url; + logger.error(reason, e); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, reason, e); + } + } catch (IllegalArgumentException e) { + String reason = "Error with plex Url: " + url; + logger.error(reason, e); + throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, reason, e); } } } diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html index 8d8896d..73068c3 100644 --- a/src/main/resources/static/index.html +++ b/src/main/resources/static/index.html @@ -43,10 +43,24 @@ recommend getting those movies, legally of course.

+

Sets address for Gaps backend. Only change if the container doesn't run in localhost.

+
+ + +
+ +

Sets port for Gaps backend. Only change if 8080 was already taken. Make sure to update docker to expose the same port.

+
+ + +
+

Go to The Movie DB and make an API Key

+ id="movie_db_api_key" type="text" class="validate">
@@ -86,12 +100,12 @@
+ id="plex_movie_urls" class="materialize-textarea">

Sets the default connect timeout for new connections. A value of 0 means no timeout, - otherwise values must be between 1 and {@link Integer#MAX_VALUE} when converted to milliseconds.

+ otherwise values must be between 1 and Integer.MAX_VALUE when converted to milliseconds.

@@ -99,7 +113,7 @@

Sets the default write timeout for new connections. A value of 0 means no timeout, - otherwise values must be between 1 and {@link Integer#MAX_VALUE} when converted to milliseconds.

+ otherwise values must be between 1 and Integer.MAX_VALUE when converted to milliseconds.

@@ -107,7 +121,7 @@

Sets the default read timeout for new connections. A value of 0 means no timeout, - otherwise values must be between 1 and {@link Integer#MAX_VALUE} when converted to milliseconds.

+ otherwise values must be between 1 and Integer.MAX_VALUE when converted to milliseconds.

diff --git a/src/main/resources/static/js/index.js b/src/main/resources/static/js/index.js index 239370d..fefc4eb 100644 --- a/src/main/resources/static/js/index.js +++ b/src/main/resources/static/js/index.js @@ -4,13 +4,13 @@ function start() { $('.modal').modal(); } -var keepChecking; +let keepChecking; function onSubmitGapsSearch() { keepChecking = true; $('#progressContainer').hide(); - var gaps = { + const gaps = { movieDbApiKey: $('#movie_db_api_key').val(), writeToFile: true, movieDbListId: $('#movie_db_list_id').val(), @@ -18,19 +18,19 @@ function onSubmitGapsSearch() { connectTimeout: $('#connect_timeout').val(), writeTimeout: $('#write_timeout').val(), readTimeout: $('#read_timeout').val(), - movieUrls: [$('#plex_movie_urls').val()] + movieUrls: $('#plex_movie_urls').val().split("\n") } $.ajax({ type: "POST", - url: "http://localhost:8080/submit", + url: "http://" + $('#address').val() + ":" + $('#port').val() + "/submit", data: JSON.stringify(gaps), contentType: "application/json", timeout: 10000000, success: function (movies) { keepChecking = false; - var movieHtml = ""; - movies.forEach(function(movie) { + let movieHtml = ""; + movies.forEach(function (movie) { movieHtml += buildMovieDiv(movie); }); @@ -39,12 +39,21 @@ function onSubmitGapsSearch() { $('#searchModelTitle').text(movies.length + ' movies to add to complete your collections'); }, error: function (err) { - alert(err.responseText); + let message = "Unknown error. Check docker Gaps log file."; + if (err) { + message = JSON.parse(err.responseText).message; + } + + $('#progressContainer').hide(); + $('#searchingBody').html(message); + $('#searchModelTitle').text("An error occurred..."); + + keepChecking = false; } }) $('#searchModal').modal('open'); - + polling(); } @@ -61,21 +70,21 @@ function buildMovie(movie) { } function polling() { - if(keepChecking) { + if (keepChecking) { $.ajax({ - type: "GET", - url: "http://localhost:8080/status", - contentType: "application/json", - success: function(data) { - if(keepChecking) { - var obj = JSON.parse(data); - if(!obj.searchedMovieCount && !obj.totalMovieCount && obj.totalMovieCount === 0) { + type: "GET", + url: "http://" + $('#address').val() + ":" + $('#port').val() + "/status", + contentType: "application/json", + success: function (data) { + if (keepChecking) { + const obj = JSON.parse(data); + if (!obj.searchedMovieCount && !obj.totalMovieCount && obj.totalMovieCount === 0) { $('#searchingBody').text("Searching for movies..."); } else { $('#progressContainer').show(); var percentage = Math.trunc(obj.searchedMovieCount / obj.totalMovieCount * 100); $('#searchingBody').text(obj.searchedMovieCount + ' of ' + obj.totalMovieCount + " movies searched. " + percentage + "% complete."); - $('#progressBar').css( "width", percentage + "%" ); + $('#progressBar').css("width", percentage + "%"); } setTimeout(polling, 2000); } diff --git a/test_cases.txt b/test_cases.txt new file mode 100644 index 0000000..62750b0 --- /dev/null +++ b/test_cases.txt @@ -0,0 +1,42 @@ +{} + +{ + "timestamp": "2019-05-23T18:16:17.402+0000", + "status": 422, + "error": "Unprocessable Entity", + "message": "Missing Movie DB Api Key. This field is required for Gaps.", + "path": "/submit" +} + + +#### + +{ + "movieDbApiKey":"test" +} + +{ + "timestamp": "2019-05-23T18:16:32.154+0000", + "status": 422, + "error": "Unprocessable Entity", + "message": "Must search from Plex and/or Folders. One or both of these fields is required for Gaps.", + "path": "/submit" +} + +#### + + +{ + "movieDbApiKey":"test", + "searchFromPlex": true +} + +{ + "timestamp": "2019-05-23T18:17:05.369+0000", + "status": 422, + "error": "Unprocessable Entity", + "message": "Missing Plex Movie Collection Urls. This field is required to search from Plex.", + "path": "/submit" +} + +####