Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge pull request #21 from D-Roch/master

adding appcfg commands to the module
  • Loading branch information...
commit c499a4a2740adc04abfbd55a16608adca24ba092 2 parents ca0d66a + 0b49b63
@mandubian mandubian authored
View
194 commands.py
@@ -15,7 +15,17 @@
MODULE = "gae"
-COMMANDS = ["gae:deploy"]
+COMMANDS = ["gae:deploy", "gae:update_indexes", "gae:vacuum_indexes", "gae:update_queues", "gae:update_dos", "gae:update_cron", "gae:cron_info", "gae:request_logs"]
+HELP = {
+ 'gae:deploy': "Deploy to Google App Engine",
+ 'gae:update_indexes': "Updating Indexes",
+ 'gae:vacuum_indexes': "Deleting Unused Indexes",
+ 'gae:update_queues': "Managing Task Queues",
+ 'gae:update_dos': "Managing DoS Protection",
+ 'gae:update_cron': "Managing Scheduled Tasks : upload cron job specifications",
+ 'gae:cron_info': "Managing Scheduled Tasks : verify your cron configuration",
+ 'gae:request_logs': "Download logs from Google App Engine",
+}
def execute(**kargs):
command = kargs.get("command")
@@ -24,6 +34,8 @@ def execute(**kargs):
env = kargs.get("env")
gae_path = None
+ war_path = os.path.join(tempfile.gettempdir(), '%s.war' % os.path.basename(app.path))
+
try:
optlist, args2 = getopt.getopt(args, '', ['gae='])
for o, a in optlist:
@@ -55,50 +67,136 @@ def execute(**kargs):
if a.find('--gae') == 0:
args.remove(a)
- print '~'
- print '~ Compiling'
- print '~ ---------'
-
- remaining_args = []
- app.check()
- java_cmd = app.java_cmd(args)
- if os.path.exists(os.path.join(app.path, 'tmp')):
- shutil.rmtree(os.path.join(app.path, 'tmp'))
- if os.path.exists(os.path.join(app.path, 'precompiled')):
- shutil.rmtree(os.path.join(app.path, 'precompiled'))
- java_cmd.insert(2, '-Dprecompile=yes')
- try:
- result = subprocess.call(java_cmd, env=os.environ)
- if not result == 0:
- print "~"
- print "~ Precompilation has failed, stop deploying."
- print "~"
- sys.exit(-1)
-
- except OSError:
- print "Could not execute the java executable, please make sure the JAVA_HOME environment variable is set properly (the java executable should reside at JAVA_HOME/bin/java). "
- sys.exit(-1)
-
- if os.path.exists(os.path.join(app.path, 'tmp')):
- shutil.rmtree(os.path.join(app.path, 'tmp'))
-
- print '~'
- print '~ Packaging'
- print '~ ---------'
-
- war_path = os.path.join(tempfile.gettempdir(), '%s.war' % os.path.basename(app.path))
- package_as_war(app, env, war_path, None)
-
- print '~'
- print '~ Deploying'
- print '~ ---------'
-
- if os.name == 'nt':
- os.system('%s/bin/appcfg.cmd update %s' % (gae_path, war_path))
- else:
- os.system('%s/bin/appcfg.sh update %s' % (gae_path, war_path))
-
- print "~ "
- print "~ Done!"
- print "~ "
- sys.exit(-1)
+ if command == "gae:deploy":
+ print '~'
+ print '~ Compiling'
+ print '~ ---------'
+
+ remaining_args = []
+ app.check()
+ java_cmd = app.java_cmd(args)
+ if os.path.exists(os.path.join(app.path, 'tmp')):
+ shutil.rmtree(os.path.join(app.path, 'tmp'))
+ if os.path.exists(os.path.join(app.path, 'precompiled')):
+ shutil.rmtree(os.path.join(app.path, 'precompiled'))
+ java_cmd.insert(2, '-Dprecompile=yes')
+ try:
+ result = subprocess.call(java_cmd, env=os.environ)
+ if not result == 0:
+ print "~"
+ print "~ Precompilation has failed, stop deploying."
+ print "~"
+ sys.exit(-1)
+
+ except OSError:
+ print "Could not execute the java executable, please make sure the JAVA_HOME environment variable is set properly (the java executable should reside at JAVA_HOME/bin/java). "
+ sys.exit(-1)
+
+ if os.path.exists(os.path.join(app.path, 'tmp')):
+ shutil.rmtree(os.path.join(app.path, 'tmp'))
+
+ print '~'
+ print '~ Packaging'
+ print '~ ---------'
+
+
+ package_as_war(app, env, war_path, None)
+
+
+ print '~'
+ print '~ Deploying'
+ print '~ ---------'
+
+ if os.name == 'nt':
+ os.system('%s/bin/appcfg.cmd update %s' % (gae_path, war_path))
+ else:
+ os.system('%s/bin/appcfg.sh update %s' % (gae_path, war_path))
+
+ print "~ "
+ print "~ Done!"
+ print "~ "
+ sys.exit(-1)
+ if command == "gae:update_indexes":
+ print '~'
+ print '~ Updating indexes'
+ print '~ ---------'
+
+ if os.name == 'nt':
+ os.system('%s/bin/appcfg.cmd update_indexes %s' % (gae_path, war_path))
+ else:
+ os.system('%s/bin/appcfg.sh update_indexes %s' % (gae_path, war_path))
+
+ print "~ "
+ print "~ Done!"
+ print "~ "
+ sys.exit(-1)
+ if command == "gae:vacuum_indexes":
+ print '~'
+ print '~ Deleting Unused Indexes'
+ print '~ ---------'
+
+ if os.name == 'nt':
+ os.system('%s/bin/appcfg.cmd vacuum_indexes %s' % (gae_path, war_path))
+ else:
+ os.system('%s/bin/appcfg.sh vacuum_indexes %s' % (gae_path, war_path))
+
+ print "~ "
+ print "~ Done!"
+ print "~ "
+ sys.exit(-1)
+ if command == "gae:update_queues":
+ print '~'
+ print '~ Updating Task Queues'
+ print '~ ---------'
+
+ if os.name == 'nt':
+ os.system('%s/bin/appcfg.cmd update_queues %s' % (gae_path, war_path))
+ else:
+ os.system('%s/bin/appcfg.sh update_queues %s' % (gae_path, war_path))
+
+ print "~ "
+ print "~ Done!"
+ print "~ "
+ sys.exit(-1)
+ if command == "gae:update_dos":
+ print '~'
+ print '~ Updating DoS Protection'
+ print '~ ---------'
+
+ if os.name == 'nt':
+ os.system('%s/bin/appcfg.cmd update_dos %s' % (gae_path, war_path))
+ else:
+ os.system('%s/bin/appcfg.sh update_dos %s' % (gae_path, war_path))
+
+ print "~ "
+ print "~ Done!"
+ print "~ "
+ sys.exit(-1)
+ if command == "gae:update_cron":
+ print '~'
+ print '~ Updating cron job specifications'
+ print '~ ---------'
+
+ if os.name == 'nt':
+ os.system('%s/bin/appcfg.cmd update_cron %s' % (gae_path, war_path))
+ else:
+ os.system('%s/bin/appcfg.sh update_cron %s' % (gae_path, war_path))
+
+ print "~ "
+ print "~ Done!"
+ print "~ "
+ sys.exit(-1)
+ if command == "gae:request_logs":
+ print '~'
+ print '~ Downloading Logs'
+ print '~ ---------'
+
+ if os.name == 'nt':
+ os.system('%s/bin/appcfg.cmd request_logs %s ./logs/production.log' % (gae_path, war_path))
+ else:
+ os.system('%s/bin/appcfg.sh request_logs %s ./logs/production.log' % (gae_path, war_path))
+
+ print "~ "
+ print "~ Done!"
+ print "~ "
+ sys.exit(-1)
View
23 src/play/modules/gae/GAECache.java
@@ -21,31 +21,40 @@
public class GAECache implements CacheImpl {
static MemcacheService memcacheService = MemcacheServiceFactory.getMemcacheService();
-
+
+ private void put(String key, Object value, int expiration, MemcacheService.SetPolicy policy){
+ //I arbitrarly decided that MAX_VALUE stands for infinite amount of time
+ //I could have chosen 0 or a negative value.
+ if(expiration == Integer.MAX_VALUE){
+ memcacheService.put(key, wrap(value), null, policy);
+ } else {
+ memcacheService.put(key, wrap(value), Expiration.byDeltaSeconds(expiration), policy);
+ }
+ }
public void add(String key, Object value, int expiration) {
- memcacheService.put(key, wrap(value), Expiration.byDeltaSeconds(expiration), MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT);
+ put(key, value, expiration, MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT);
}
public boolean safeAdd(String key, Object value, int expiration) {
- memcacheService.put(key, wrap(value), Expiration.byDeltaSeconds(expiration), MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT);
+ put(key, value, expiration, MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT);
return true;
}
public void set(String key, Object value, int expiration) {
- memcacheService.put(key, wrap(value), Expiration.byDeltaSeconds(expiration), MemcacheService.SetPolicy.SET_ALWAYS);
+ put(key, value, expiration, MemcacheService.SetPolicy.SET_ALWAYS);
}
public boolean safeSet(String key, Object value, int expiration) {
- memcacheService.put(key, wrap(value), Expiration.byDeltaSeconds(expiration), MemcacheService.SetPolicy.SET_ALWAYS);
+ put(key, value, expiration, MemcacheService.SetPolicy.SET_ALWAYS);
return true;
}
public void replace(String key, Object value, int expiration) {
- memcacheService.put(key, wrap(value), Expiration.byDeltaSeconds(expiration), MemcacheService.SetPolicy.REPLACE_ONLY_IF_PRESENT);
+ put(key, value, expiration, MemcacheService.SetPolicy.REPLACE_ONLY_IF_PRESENT);
}
public boolean safeReplace(String key, Object value, int expiration) {
- memcacheService.put(key, wrap(value), Expiration.byDeltaSeconds(expiration), MemcacheService.SetPolicy.REPLACE_ONLY_IF_PRESENT);
+ put(key, value, expiration, MemcacheService.SetPolicy.REPLACE_ONLY_IF_PRESENT);
return true;
}
View
506 src/play/modules/gae/WS.java
@@ -0,0 +1,506 @@
+package play.modules.gae;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStreamWriter;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.w3c.dom.Document;
+import org.xml.sax.InputSource;
+
+import play.Logger;
+import play.libs.Codec;
+import play.libs.IO;
+import play.mvc.Http.Header;
+import play.utils.NoOpEntityResolver;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParser;
+
+/**
+ * Simple HTTP client to make webservices requests.
+ *
+ * <p/>
+ * Get latest BBC World news as a RSS content
+ * <pre>
+ * response = WS.GET("http://newsrss.bbc.co.uk/rss/newsonline_world_edition/front_page/rss.xml");
+ * Document xmldoc = response.getXml();
+ * // the real pain begins here...
+ * </pre>
+ * <p/>
+ *
+ * Search what Yahoo! thinks of google (starting from the 30th result).
+ * <pre>
+ * response = WS.GET("http://search.yahoo.com/search?p=<em>%s</em>&pstart=1&b=<em>%d</em>", "Google killed me", 30 );
+ * if( response.getStatus() == 200 ) {
+ * html = response.getString();
+ * }
+ * </pre>
+ */
+public class WS {
+
+ /**
+ * URL-encode an UTF-8 string to be used as a query string parameter.
+ * @param part string to encode
+ * @return url-encoded string
+ */
+ public static String encode(String part) {
+ try {
+ return URLEncoder.encode(part, "utf-8");
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Build a WebService Request with the given URL.
+ * This object support chaining style programming for adding params, file, headers to requests.
+ * @param url of the request
+ * @return a WSRequest on which you can add params, file headers using a chaining style programming.
+ */
+ public static WSRequest url(String url) {
+ return new WSRequest(url);
+ }
+
+ /**
+ * Build a WebService Request with the given URL.
+ * This constructor will format url using params passed in arguments.
+ * This object support chaining style programming for adding params, file, headers to requests.
+ * @param url to format using the given params.
+ * @param params the params passed to format the URL.
+ * @return a WSRequest on which you can add params, file headers using a chaining style programming.
+ */
+ public static WSRequest url(String url, String... params) {
+ Object[] encodedParams = new String[params.length];
+ for (int i = 0; i < params.length; i++) {
+ encodedParams[i] = encode(params[i]);
+ }
+ return new WSRequest(String.format(url, encodedParams));
+ }
+
+ public static class WSRequest {
+
+ public String url;
+ public String username;
+ public String password;
+ public String body;
+ public FileParam[] fileParams;
+ public Map<String, String> headers = new HashMap<String, String>();
+ public Map<String, Object> parameters = new HashMap<String, Object>();
+ public String mimeType;
+ public Integer timeout;
+
+ private WSRequest(String url) {
+ this.url = url;
+ }
+
+ /**
+ * Add a MimeType to the web service request.
+ * @param mimeType
+ * @return the WSRequest for chaining.
+ */
+ public WSRequest mimeType(String mimeType) {
+ this.mimeType = mimeType;
+ return this;
+ }
+
+ /**
+ * define client authentication for a server host
+ * provided credentials will be used during the request
+ * @param username
+ * @param password
+ */
+ public WSRequest authenticate(String username, String password) {
+ this.username = username;
+ this.password = password;
+ return this;
+ }
+
+ /**
+ * Add files to request. This will only work with POST or PUT.
+ * @param files
+ * @return the WSRequest for chaining.
+ */
+ public WSRequest files(File... files) {
+ this.fileParams = FileParam.getFileParams(files);
+ return this;
+ }
+
+ /**
+ * Add fileParams aka File and Name parameter to the request. This will only work with POST or PUT.
+ * @param fileParams
+ * @return the WSRequest for chaining.
+ */
+ public WSRequest files(FileParam... fileParams) {
+ this.fileParams = fileParams;
+ return this;
+ }
+
+ /**
+ * Add the given body to the request.
+ * @param body
+ * @return the WSRequest for chaining.
+ */
+ public WSRequest body(Object body) {
+ this.body = body == null ? "" : body.toString();
+ return this;
+ }
+
+ /**
+ * Add a header to the request
+ * @param name header name
+ * @param value header value
+ * @return the WSRequest for chaining.
+ */
+ public WSRequest setHeader(String name, String value) {
+ this.headers.put(name, value);
+ return this;
+ }
+
+ /**
+ * Add a parameter to the request
+ * @param name parameter name
+ * @param value parameter value
+ * @return the WSRequest for chaining.
+ */
+ public WSRequest setParameter(String name, String value) {
+ this.parameters.put(name, value);
+ return this;
+ }
+
+ public WSRequest setParameter(String name, Object value) {
+ this.parameters.put(name, value);
+ return this;
+ }
+
+ /**
+ * Use the provided headers when executing request.
+ * @param headers
+ * @return the WSRequest for chaining.
+ */
+ public WSRequest headers(Map<String, String> headers) {
+ this.headers = headers;
+ return this;
+ }
+
+ /**
+ * Add parameters to request.
+ * If POST or PUT, parameters are passed in body using x-www-form-urlencoded if alone, or form-data if there is files too.
+ * For any other method, those params are appended to the queryString.
+ * @return the WSRequest for chaining.
+ */
+ public WSRequest params(Map<String, Object> parameters) {
+ this.parameters = parameters;
+ return this;
+ }
+
+ /**
+ * Add parameters to request.
+ * If POST or PUT, parameters are passed in body using x-www-form-urlencoded if alone, or form-data if there is files too.
+ * For any other method, those params are appended to the queryString.
+ * @return the WSRequest for chaining.
+ */
+ public WSRequest setParameters(Map<String, String> parameters) {
+ this.parameters.putAll(parameters);
+ return this;
+ }
+
+ /** Execute a GET request synchronously. */
+ public HttpResponse get() {
+ try {
+ return new HttpResponse(prepare(new URL(url), "GET"));
+ } catch (Exception e) {
+ Logger.error(e.toString());
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** Execute a POST request.*/
+ public HttpResponse post() {
+ try {
+ return new HttpResponse(prepare(new URL(url), "POST"));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** Execute a PUT request.*/
+ public HttpResponse put() {
+ try {
+ return new HttpResponse(prepare(new URL(url), "PUT"));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** Execute a DELETE request.*/
+ public HttpResponse delete() {
+ try {
+ return new HttpResponse(prepare(new URL(url), "DELETE"));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** Execute a OPTIONS request.*/
+ public HttpResponse options() {
+ try {
+ return new HttpResponse(prepare(new URL(url), "OPTIONS"));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** Execute a HEAD request.*/
+ public HttpResponse head() {
+ try {
+ return new HttpResponse(prepare(new URL(url), "HEAD"));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** Execute a TRACE request.*/
+ public HttpResponse trace() {
+ try {
+ return new HttpResponse(prepare(new URL(url), "TRACE"));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private HttpURLConnection prepare(URL url, String method) {
+ if (this.username != null && this.password != null) {
+ this.headers.put("Authorization", "Basic " + Codec.encodeBASE64(this.username + ":" + this.password));
+ }
+ try {
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ checkFileBody(connection);
+ connection.setRequestMethod(method);
+ for (String key: this.headers.keySet()) {
+ connection.setRequestProperty(key, headers.get(key));
+ }
+ return connection;
+ } catch(Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void checkFileBody(HttpURLConnection connection) throws IOException {
+/* if (this.fileParams != null) {
+ //could be optimized, we know the size of this array.
+ for (int i = 0; i < this.fileParams.length; i++) {
+ builder.addBodyPart(new FilePart(this.fileParams[i].paramName,
+ this.fileParams[i].file,
+ MimeTypes.getMimeType(this.fileParams[i].file.getName()),
+ null));
+ }
+ if (this.parameters != null) {
+ for (String key : this.parameters.keySet()) {
+ Object value = this.parameters.get(key);
+ if (value instanceof Collection<?> || value.getClass().isArray()) {
+ Collection<?> values = value.getClass().isArray() ? Arrays.asList((Object[]) value) : (Collection<?>) value;
+ for (Object v : values) {
+ builder.addBodyPart(new StringPart(key, v.toString()));
+ }
+ } else {
+ builder.addBodyPart(new StringPart(key, value.toString()));
+ }
+ }
+ }
+ return;
+ }*/
+ if (this.parameters != null && !this.parameters.isEmpty()) {
+ connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
+ connection.setDoOutput(true);
+ OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream());
+ writer.write(createQueryString());
+ writer.close();
+ }
+ if (this.body != null) {
+ if (this.parameters != null && !this.parameters.isEmpty()) {
+ throw new RuntimeException("POST or PUT method with parameters AND body are not supported.");
+ }
+ connection.setDoOutput(true);
+ OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream());
+ writer.write(this.body);
+ writer.close();
+ if(this.mimeType != null) {
+ connection.setRequestProperty("Content-Type", this.mimeType);
+ }
+ }
+ }
+
+ private String createQueryString() {
+ StringBuilder sb = new StringBuilder();
+ for (String key : this.parameters.keySet()) {
+ if (sb.length() > 0) {
+ sb.append("&");
+ }
+ Object value = this.parameters.get(key);
+
+ if (value != null) {
+ if (value instanceof Collection<?> || value.getClass().isArray()) {
+ Collection<?> values = value.getClass().isArray() ? Arrays.asList((Object[]) value) : (Collection<?>) value;
+ boolean first = true;
+ for (Object v : values) {
+ if (!first) {
+ sb.append("&");
+ }
+ first = false;
+ sb.append(encode(key)).append("=").append(encode(v.toString()));
+ }
+ } else {
+ sb.append(encode(key)).append("=").append(encode(this.parameters.get(key).toString()));
+ }
+ }
+ }
+ return sb.toString();
+ }
+
+ }
+
+ public static class FileParam {
+ File file;
+ String paramName;
+
+ public FileParam(File file, String name) {
+ this.file = file;
+ this.paramName = name;
+ }
+
+ public static FileParam[] getFileParams(File[] files) {
+ FileParam[] filesp = new FileParam[files.length];
+ for (int i = 0; i < files.length; i++) {
+ filesp[i] = new FileParam(files[i], files[i].getName());
+ }
+ return filesp;
+ }
+ }
+
+ /**
+ * An HTTP response wrapper
+ */
+ public static class HttpResponse {
+
+ private HttpURLConnection connection;
+
+ /**
+ * you shouldnt have to create an HttpResponse yourself
+ * @param method
+ */
+ public HttpResponse(HttpURLConnection connection) {
+ this.connection = connection;
+ connection.setDoInput(true);
+ }
+
+ /**
+ * the HTTP status code
+ * @return the status code of the http response
+ */
+ public Integer getStatus() {
+ try {
+ return this.connection.getResponseCode();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * The http response content type
+ * @return the content type of the http response
+ */
+ public String getContentType() {
+ return getHeader("content-type");
+ }
+
+ public String getHeader(String key) {
+ return connection.getHeaderField(key);
+ }
+
+ public List<Header> getHeaders() {
+ Map<String, List<String>> hdrs = connection.getHeaderFields();
+ List<Header> result = new ArrayList<Header>();
+ for (String key: hdrs.keySet()) {
+ result.add(new Header(key, hdrs.get(key)));
+ }
+ return result;
+ }
+
+ /**
+ * Parse and get the response body as a {@link Document DOM document}
+ * @return a DOM document
+ */
+ public Document getXml() {
+ return getXml("UTF-8");
+ }
+
+ /**
+ * parse and get the response body as a {@link Document DOM document}
+ * @param encoding xml charset encoding
+ * @return a DOM document
+ */
+ public Document getXml(String encoding) {
+ try {
+ InputSource source = new InputSource(connection.getInputStream());
+ source.setEncoding(encoding);
+ DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
+ builder.setEntityResolver(new NoOpEntityResolver());
+ return builder.parse(source);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * get the response body as a string
+ * @return the body of the http response
+ */
+ public String getString() {
+ try {
+ return IO.readContentAsString(connection.getInputStream());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * get the response as a stream
+ * @return an inputstream
+ */
+ public InputStream getStream() {
+ try {
+ return connection.getInputStream();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * get the response body as a {@link com.google.gson.JSONObject}
+ * @return the json response
+ */
+ public JsonElement getJson() {
+ String json = "";
+ try {
+ json = getString();
+ return new JsonParser().parse(json);
+ } catch (Exception e) {
+ Logger.error("Bad JSON: \n%s", json);
+ throw new RuntimeException("Cannot parse JSON (check logs)", e);
+ }
+ }
+ }
+}
Please sign in to comment.
Something went wrong with that request. Please try again.