diff --git a/README.md b/README.md index d265d0c..f918aee 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,13 @@ Latest version as of 27-10-2018 is 2.0.1.RELEASE [comment]: # (REPLACE ME: Add a Getting Started guide) -Enterprise YAML validator is better than [http://www.yamllint.com] http://www.yamllint.com +Enterprise YAML validator is better than http://www.yamllint.com. Enterprise YAML Validator follows YAML spec, support multiple doc feature which is not supported my YAML Lint. Also, now you dont have to use https://www.base64decode.org/, This app also supports Decode from and to Base64 format. Here's the working version of the Application hosted in Microsoft Azure Cloud: -[http://varkeys-rhel-jenkins.westus.cloudapp.azure.com:8090/validator/forEditor] (http://varkeys-rhel-jenkins.westus.cloudapp.azure.com:8090/validator/forEditor) +http://varkeys-rhel-jenkins.westus.cloudapp.azure.com:9090/validator/editor ![Alt text](enterprise-yaml-validator-image.PNG?raw=true "Enterprise YAML Validator") @@ -24,14 +24,14 @@ Here's the working version of the Application hosted in Microsoft Azure Cloud: * spring-boot-starter-freemarker * springfox-swagger2, springfox-swagger-ui * snakeyaml, lombok, jackson-databind, spring-boot-test -[comment]: # (REPLACE ME: Add Initial requirements like JDK version, IDE, etc) + ### How to? [comment]: # (REPLACE ME: Add your confluence page in below format) -Click on "BASE 64Encode" to encode to Base64 format. -Click on "BASE 64Encode" to decode from Base64 format. -Click on "Validate YAML" to validate YAML data. -Click on "Share YAML" to share the YAML data through Outlook Mail. +* Click on "BASE 64Encode" to encode to Base64 format. +* Click on "BASE 64Encode" to decode from Base64 format. +* Click on "Validate YAML" to validate YAML data. +* Click on "Share YAML" to share the YAML data through Outlook Mail. ### Jira diff --git a/pom.xml b/pom.xml index a418aa8..d0df479 100644 --- a/pom.xml +++ b/pom.xml @@ -43,14 +43,19 @@ springfox-swagger-ui 2.9.2 + + org.projectlombok + lombok + org.yaml snakeyaml 1.23 - org.projectlombok - lombok + org.json + json + 20180813 diff --git a/src/main/java/io/exnihilo/validator/ValidatorApplication.java b/src/main/java/io/exnihilo/validator/ValidatorApplication.java index 19afc77..358222e 100644 --- a/src/main/java/io/exnihilo/validator/ValidatorApplication.java +++ b/src/main/java/io/exnihilo/validator/ValidatorApplication.java @@ -17,7 +17,6 @@ public class ValidatorApplication { public static void main(String[] args) { - SpringApplicationBuilder app = new SpringApplicationBuilder(ValidatorApplication.class); app.build().addListeners(new ApplicationPidFileWriter()); app.run(); diff --git a/src/main/java/io/exnihilo/validator/config/Config.java b/src/main/java/io/exnihilo/validator/config/Config.java new file mode 100644 index 0000000..a81f969 --- /dev/null +++ b/src/main/java/io/exnihilo/validator/config/Config.java @@ -0,0 +1,35 @@ +package io.exnihilo.validator.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.Contact; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; + +import java.util.Collections; + +@Configuration +public class Config { + + @Bean + public Docket api() { + return new Docket(DocumentationType.SWAGGER_2) + .select() + .apis(RequestHandlerSelectors.any()) + .paths(PathSelectors.any()) + .build().apiInfo(apiInfo()); + } + + private ApiInfo apiInfo() { + return new ApiInfo( + "This REST API", + "Validates yaml, json and xml files. Hi!!", + "API TOS", + "Terms of service", + new Contact("Anand Varkey Philips", "about.me/anandvarkeyphilips", "anandvarkey.philips@gmail.com"), + "License of API", "API license URL", Collections.emptyList()); + } +} diff --git a/src/main/java/io/exnihilo/validator/controller/EditorController.java b/src/main/java/io/exnihilo/validator/controller/EditorController.java new file mode 100644 index 0000000..49a77ad --- /dev/null +++ b/src/main/java/io/exnihilo/validator/controller/EditorController.java @@ -0,0 +1,81 @@ +package io.exnihilo.validator.controller; + +import io.exnihilo.validator.entity.ValidationEntity; +import io.exnihilo.validator.service.ValidatorService; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.ModelAndView; + +@Slf4j +@RestController +@ApiResponses(value = { + @ApiResponse(code = 200, message = "Successfully connected and validated with API Validator"), + @ApiResponse(code = 401, message = "You are not authenticated properly to view the resource!"), + @ApiResponse(code = 403, message = "Accessing the resource you were trying to reach is forbidden!"), + @ApiResponse(code = 404, message = "Validator Service not available right now!"), +}) +public class EditorController { + + @Autowired + private ValidatorService validatorService; + + @RequestMapping("/editor") + public ModelAndView editor() { + log.info("Inside editor"); + return new ModelAndView("editor"); + } + + /** + * A pre-configured sample REST endpoint to demonstrate the use of Request Parameter. + * + * @param validationEntity + * @return validation result + */ + @PostMapping("/yaml") + @ApiOperation( + value = "API for Validating the YAML Data", + notes = "This API validates YAML data input.The API is in beta phase..") + public ResponseEntity validateYamlController(@RequestBody ValidationEntity validationEntity) { + log.debug("Calling validateYamlController..."); + return new ResponseEntity(validatorService.validateYamlService(validationEntity.getValidationMessage()), HttpStatus.OK); + } + + /** + * A pre-configured sample REST endpoint to demonstrate the use of Request Parameter. + * + * @param validationEntity + * @return validation result + */ + @PostMapping("/json") + @ApiOperation( + value = "API for Validating the JSON Data", + notes = "This API validates JSON data input.The API is in beta phase..") + public ResponseEntity validateJsonController(@RequestBody ValidationEntity validationEntity) { + log.debug("Calling validateJsonController..."); + return new ResponseEntity(validatorService.validateJsonService(validationEntity.getValidationMessage()), HttpStatus.OK); + } + + /** + * A pre-configured sample REST endpoint to demonstrate the use of Request Parameter. + * + * @param validationEntity + * @return validation result + */ + @PostMapping("/formatJson") + @ApiOperation( + value = "API for formatting the JSON Data", + notes = "This API formats JSON data input.The API is in beta phase..") + public ResponseEntity formatJsonController(@RequestBody ValidationEntity validationEntity) { + log.debug("Calling formatJsonController..."); + return new ResponseEntity(validatorService.formatJsonService(validationEntity.getValidationMessage()), HttpStatus.OK); + } +} diff --git a/src/main/java/io/exnihilo/validator/controller/ValidatorController.java b/src/main/java/io/exnihilo/validator/controller/ValidatorController.java deleted file mode 100644 index f74869c..0000000 --- a/src/main/java/io/exnihilo/validator/controller/ValidatorController.java +++ /dev/null @@ -1,82 +0,0 @@ -package io.exnihilo.validator.controller; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import io.exnihilo.validator.service.ValidatorService; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Component; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.servlet.ModelAndView; -import springfox.documentation.builders.PathSelectors; -import springfox.documentation.builders.RequestHandlerSelectors; -import springfox.documentation.service.ApiInfo; -import springfox.documentation.service.Contact; -import springfox.documentation.spi.DocumentationType; -import springfox.documentation.spring.web.plugins.Docket; - -import java.util.Collections; - -@Slf4j -@RestController -public class ValidatorController { - - @Autowired - private ValidatorService validatorService; - - @Bean - public Docket api() { - return new Docket(DocumentationType.SWAGGER_2) - .select() - .apis(RequestHandlerSelectors.any()) - .paths(PathSelectors.any()) - .build().apiInfo(apiInfo()); - } - - - private ApiInfo apiInfo() { - return new ApiInfo( - "This REST API", - "Validates yaml, json and xml files. Hi!!", - "API TOS", - "Terms of service", - new Contact("Anand Varkey Philips", "about.me/anandvarkeyphilips", "anandvarkey.philips@gmail.com"), - "License of API", "API license URL", Collections.emptyList()); - } - - - @RequestMapping("/forEditor") - public ModelAndView forEditor() { - log.info("Inside forEditor"); - return new ModelAndView("yaml-editor"); - } - - /** - * A pre-configured sample REST endpoint to demonstrate the use of Request Parameter. - * - * @param yamlData - * @return validation result - */ - @PostMapping("/yaml") - @ApiOperation( - value = "API for Validating the YAML Data", - notes = "This API validates YAML data input.The API is in beta phase..") - @ApiResponses(value = { - @ApiResponse(code = 200, message = "Successfully connected and validated with API Validator"), - @ApiResponse(code = 401, message = "You are not authenticated properly to view the resource!"), - @ApiResponse(code = 403, message = "Accessing the resource you were trying to reach is forbidden!"), - @ApiResponse(code = 404, message = "Validator Service not available right now!"), - }) - public ResponseEntity validateYAMLController(@RequestBody ObjectNode yamlData) { - log.debug("Calling validateYAMLController..."); - return new ResponseEntity(validatorService.validateYAMLService(yamlData.get("yamlData").asText()), HttpStatus.OK); - } -} diff --git a/src/main/java/io/exnihilo/validator/entity/JSONOrderedObject.java b/src/main/java/io/exnihilo/validator/entity/JSONOrderedObject.java new file mode 100644 index 0000000..838228a --- /dev/null +++ b/src/main/java/io/exnihilo/validator/entity/JSONOrderedObject.java @@ -0,0 +1,2403 @@ +package io.exnihilo.validator.entity; + +import org.json.*; + +import java.io.Closeable; +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.*; +import java.util.Map.Entry; + +/* + Copyright (c) 2002 JSON.org + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + The Software shall be used for Good, not Evil. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +/** + * A JSONObject is an unordered collection of name/value pairs. Its external + * form is a string wrapped in curly braces with colons between the names and + * values, and commas between the values and names. The internal form is an + * object having get and opt methods for accessing + * the values by name, and put methods for adding or replacing + * values by name. The values can be any of these types: Boolean, + * JSONArray, JSONObject, Number, + * String, or the JSONObject.NULL object. A + * JSONObject constructor can be used to convert an external form JSON text + * into an internal form whose values can be retrieved with the + * get and opt methods, or to convert values into a + * JSON text using the put and toString methods. A + * get method returns a value if one can be found, and throws an + * exception if one cannot be found. An opt method returns a + * default value instead of throwing an exception, and so is useful for + * obtaining optional values. + *

+ * The generic get() and opt() methods return an + * object, which you can cast or query for type. There are also typed + * get and opt methods that do type checking and type + * coercion for you. The opt methods differ from the get methods in that they + * do not throw. Instead, they return a specified value, such as null. + *

+ * The put methods add or replace values in an object. For + * example, + * + *

+ * myString = new JSONObject()
+ *         .put("JSON", "Hello, World!").toString();
+ * 
+ *

+ * produces the string {"JSON": "Hello, World"}. + *

+ * The texts produced by the toString methods strictly conform to + * the JSON syntax rules. The constructors are more forgiving in the texts they + * will accept: + *

    + *
  • An extra , (comma) may appear just + * before the closing brace.
  • + *
  • Strings may be quoted with ' (single + * quote).
  • + *
  • Strings do not need to be quoted at all if they do not begin with a + * quote or single quote, and if they do not contain leading or trailing + * spaces, and if they do not contain any of these characters: + * { } [ ] / \ : , # and if they do not look like numbers and + * if they are not the reserved words true, false, + * or null.
  • + *
+ * + * @author JSON.org + * @version 2016-08-15 + */ +public class JSONOrderedObject { + /** + * JSONObject.NULL is equivalent to the value that JavaScript calls null, + * whilst Java's null is equivalent to the value that JavaScript calls + * undefined. + */ + private static final class Null { + + /** + * There is only intended to be a single instance of the NULL object, + * so the clone method returns itself. + * + * @return NULL. + */ + @Override + protected final Object clone() { + return this; + } + + /** + * A Null object is equal to the null value and to itself. + * + * @param object An object to test for nullness. + * @return true if the object parameter is the JSONObject.NULL object or + * null. + */ + @Override + public boolean equals(Object object) { + return object == null || object == this; + } + + /** + * A Null object is equal to the null value and to itself. + * + * @return always returns 0. + */ + @Override + public int hashCode() { + return 0; + } + + /** + * Get the "null" string value. + * + * @return The string "null". + */ + @Override + public String toString() { + return "null"; + } + } + + /** + * The map where the JSONObject's properties are kept. + */ + private final Map map; + + /** + * It is sometimes more convenient and less ambiguous to have a + * NULL object than to use Java's null value. + * JSONObject.NULL.equals(null) returns true. + * JSONObject.NULL.toString() returns "null". + */ + public static final Object NULL = new JSONOrderedObject.Null(); + + /** + * Construct an empty JSONObject. + */ + public JSONOrderedObject() { + // HashMap is used on purpose to ensure that elements are unordered by + // the specification. + // JSON tends to be a portable transfer format to allows the container + // implementations to rearrange their items for a faster element + // retrieval based on associative access. + // Therefore, an implementation mustn't rely on the order of the item. + this.map = new LinkedHashMap(); + } + + /** + * Construct a JSONObject from a subset of another JSONObject. An array of + * strings is used to identify the keys that should be copied. Missing keys + * are ignored. + * + * @param jo A JSONObject. + * @param names An array of strings. + */ + public JSONOrderedObject(JSONOrderedObject jo, String[] names) { + this(names.length); + for (int i = 0; i < names.length; i += 1) { + try { + this.putOnce(names[i], jo.opt(names[i])); + } catch (Exception ignore) { + } + } + } + + /** + * Construct a JSONObject from a JSONTokener. + * + * @param x A JSONTokener object containing the source string. + * @throws JSONException If there is a syntax error in the source string or a + * duplicated key. + */ + public JSONOrderedObject(JSONTokener x) throws JSONException { + this(); + char c; + String key; + + if (x.nextClean() != '{') { + throw x.syntaxError("A JSONObject text must begin with '{'"); + } + for (; ; ) { + c = x.nextClean(); + switch (c) { + case 0: + throw x.syntaxError("A JSONObject text must end with '}'"); + case '}': + return; + default: + x.back(); + key = x.nextValue().toString(); + } + + // The key is followed by ':'. + + c = x.nextClean(); + if (c != ':') { + throw x.syntaxError("Expected a ':' after a key"); + } + + // Use syntaxError(..) to include error location + + if (key != null) { + // Check if key exists + if (this.opt(key) != null) { + // key already exists + throw x.syntaxError("Duplicate key \"" + key + "\""); + } + // Only add value if non-null + Object value = x.nextValue(); + if (value != null) { + this.put(key, value); + } + } + + // Pairs are separated by ','. + + switch (x.nextClean()) { + case ';': + case ',': + if (x.nextClean() == '}') { + return; + } + x.back(); + break; + case '}': + return; + default: + throw x.syntaxError("Expected a ',' or '}'"); + } + } + } + + /** + * Construct a JSONObject from a Map. + * + * @param m A map object that can be used to initialize the contents of + * the JSONObject. + * @throws JSONException If a value in the map is non-finite number. + * @throws NullPointerException If a key in the map is null + */ + public JSONOrderedObject(Map m) { + if (m == null) { + this.map = new HashMap(); + } else { + this.map = new HashMap(m.size()); + for (final Entry e : m.entrySet()) { + if (e.getKey() == null) { + throw new NullPointerException("Null key."); + } + final Object value = e.getValue(); + if (value != null) { + this.map.put(String.valueOf(e.getKey()), wrap(value)); + } + } + } + } + + /** + * Construct a JSONObject from an Object using bean getters. It reflects on + * all of the public methods of the object. For each of the methods with no + * parameters and a name starting with "get" or + * "is" followed by an uppercase letter, the method is invoked, + * and a key and the value returned from the getter method are put into the + * new JSONObject. + *

+ * The key is formed by removing the "get" or "is" + * prefix. If the second remaining character is not upper case, then the + * first character is converted to lower case. + *

+ * Methods that are static, return void, + * have parameters, or are "bridge" methods, are ignored. + *

+ * For example, if an object has a method named "getName", and + * if the result of calling object.getName() is + * "Larry Fine", then the JSONObject will contain + * "name": "Larry Fine". + *

+ * The {@link JSONPropertyName} annotation can be used on a bean getter to + * override key name used in the JSONObject. For example, using the object + * above with the getName method, if we annotated it with: + *

+     * @JSONPropertyName("FullName")
+     * public String getName() { return this.name; }
+     * 
+ * The resulting JSON object would contain "FullName": "Larry Fine" + *

+ * Similarly, the {@link JSONPropertyName} annotation can be used on non- + * get and is methods. We can also override key + * name used in the JSONObject as seen below even though the field would normally + * be ignored: + *

+     * @JSONPropertyName("FullName")
+     * public String fullName() { return this.name; }
+     * 
+ * The resulting JSON object would contain "FullName": "Larry Fine" + *

+ * The {@link JSONPropertyIgnore} annotation can be used to force the bean property + * to not be serialized into JSON. If both {@link JSONPropertyIgnore} and + * {@link JSONPropertyName} are defined on the same method, a depth comparison is + * performed and the one closest to the concrete class being serialized is used. + * If both annotations are at the same level, then the {@link JSONPropertyIgnore} + * annotation takes precedent and the field is not serialized. + * For example, the following declaration would prevent the getName + * method from being serialized: + *

+     * @JSONPropertyName("FullName")
+     * @JSONPropertyIgnore
+     * public String getName() { return this.name; }
+     * 
+ *

+ * + * @param bean An object that has getter methods that should be used to make + * a JSONObject. + */ + public JSONOrderedObject(Object bean) { + this(); + this.populateMap(bean); + } + + /** + * Construct a JSONObject from an Object, using reflection to find the + * public members. The resulting JSONObject's keys will be the strings from + * the names array, and the values will be the field values associated with + * those keys in the object. If a key is not found or not visible, then it + * will not be copied into the new JSONObject. + * + * @param object An object that has fields that should be used to make a + * JSONObject. + * @param names An array of strings, the names of the fields to be obtained + * from the object. + */ + public JSONOrderedObject(Object object, String names[]) { + this(names.length); + Class c = object.getClass(); + for (int i = 0; i < names.length; i += 1) { + String name = names[i]; + try { + this.putOpt(name, c.getField(name).get(object)); + } catch (Exception ignore) { + } + } + } + + /** + * Construct a JSONObject from a source JSON text string. This is the most + * commonly used JSONObject constructor. + * + * @param source A string beginning with { (left + * brace) and ending with } + *  (right brace). + * @throws JSONException If there is a syntax error in the source string or a + * duplicated key. + */ + public JSONOrderedObject(String source) throws JSONException { + this(new JSONTokener(source)); + } + + /** + * Construct a JSONObject from a ResourceBundle. + * + * @param baseName The ResourceBundle base name. + * @param locale The Locale to load the ResourceBundle for. + * @throws JSONException If any JSONExceptions are detected. + */ + public JSONOrderedObject(String baseName, Locale locale) throws JSONException { + this(); + ResourceBundle bundle = ResourceBundle.getBundle(baseName, locale, + Thread.currentThread().getContextClassLoader()); + +// Iterate through the keys in the bundle. + + Enumeration keys = bundle.getKeys(); + while (keys.hasMoreElements()) { + Object key = keys.nextElement(); + if (key != null) { + +// Go through the path, ensuring that there is a nested JSONObject for each +// segment except the last. Add the value using the last segment's name into +// the deepest nested JSONObject. + + String[] path = ((String) key).split("\\."); + int last = path.length - 1; + JSONOrderedObject target = this; + for (int i = 0; i < last; i += 1) { + String segment = path[i]; + JSONOrderedObject nextTarget = target.optJSONObject(segment); + if (nextTarget == null) { + nextTarget = new JSONOrderedObject(); + target.put(segment, nextTarget); + } + target = nextTarget; + } + target.put(path[last], bundle.getString((String) key)); + } + } + } + + /** + * Constructor to specify an initial capacity of the internal map. Useful for library + * internal calls where we know, or at least can best guess, how big this JSONObject + * will be. + * + * @param initialCapacity initial capacity of the internal map. + */ + protected JSONOrderedObject(int initialCapacity) { + this.map = new HashMap(initialCapacity); + } + + /** + * Accumulate values under a key. It is similar to the put method except + * that if there is already an object stored under the key then a JSONArray + * is stored under the key to hold all of the accumulated values. If there + * is already a JSONArray, then the new value is appended to it. In + * contrast, the put method replaces the previous value. + *

+ * If only one value is accumulated that is not a JSONArray, then the result + * will be the same as using put. But if multiple values are accumulated, + * then the result will be like append. + * + * @param key A key string. + * @param value An object to be accumulated under the key. + * @return this. + * @throws JSONException If the value is non-finite number. + * @throws NullPointerException If the key is null. + */ + public JSONOrderedObject accumulate(String key, Object value) throws JSONException { + testValidity(value); + Object object = this.opt(key); + if (object == null) { + this.put(key, + value instanceof JSONArray ? new JSONArray().put(value) + : value); + } else if (object instanceof JSONArray) { + ((JSONArray) object).put(value); + } else { + this.put(key, new JSONArray().put(object).put(value)); + } + return this; + } + + /** + * Append values to the array under a key. If the key does not exist in the + * JSONObject, then the key is put in the JSONObject with its value being a + * JSONArray containing the value parameter. If the key was already + * associated with a JSONArray, then the value parameter is appended to it. + * + * @param key A key string. + * @param value An object to be accumulated under the key. + * @return this. + * @throws JSONException If the value is non-finite number or if the current value associated with + * the key is not a JSONArray. + * @throws NullPointerException If the key is null. + */ + public JSONOrderedObject append(String key, Object value) throws JSONException { + testValidity(value); + Object object = this.opt(key); + if (object == null) { + this.put(key, new JSONArray().put(value)); + } else if (object instanceof JSONArray) { + this.put(key, ((JSONArray) object).put(value)); + } else { + throw new JSONException("JSONObject[" + key + + "] is not a JSONArray."); + } + return this; + } + + /** + * Produce a string from a double. The string "null" will be returned if the + * number is not finite. + * + * @param d A double. + * @return A String. + */ + public static String doubleToString(double d) { + if (Double.isInfinite(d) || Double.isNaN(d)) { + return "null"; + } + +// Shave off trailing zeros and decimal point, if possible. + + String string = Double.toString(d); + if (string.indexOf('.') > 0 && string.indexOf('e') < 0 + && string.indexOf('E') < 0) { + while (string.endsWith("0")) { + string = string.substring(0, string.length() - 1); + } + if (string.endsWith(".")) { + string = string.substring(0, string.length() - 1); + } + } + return string; + } + + /** + * Get the value object associated with a key. + * + * @param key A key string. + * @return The object associated with the key. + * @throws JSONException if the key is not found. + */ + public Object get(String key) throws JSONException { + if (key == null) { + throw new JSONException("Null key."); + } + Object object = this.opt(key); + if (object == null) { + throw new JSONException("JSONObject[" + quote(key) + "] not found."); + } + return object; + } + + /** + * Get the enum value associated with a key. + * + * @param clazz The type of enum to retrieve. + * @param key A key string. + * @return The enum value associated with the key + * @throws JSONException if the key is not found or if the value cannot be converted + * to an enum. + */ + public > E getEnum(Class clazz, String key) throws JSONException { + E val = optEnum(clazz, key); + if (val == null) { + // JSONException should really take a throwable argument. + // If it did, I would re-implement this with the Enum.valueOf + // method and place any thrown exception in the JSONException + throw new JSONException("JSONObject[" + quote(key) + + "] is not an enum of type " + quote(clazz.getSimpleName()) + + "."); + } + return val; + } + + /** + * Get the boolean value associated with a key. + * + * @param key A key string. + * @return The truth. + * @throws JSONException if the value is not a Boolean or the String "true" or + * "false". + */ + public boolean getBoolean(String key) throws JSONException { + Object object = this.get(key); + if (object.equals(Boolean.FALSE) + || (object instanceof String && ((String) object) + .equalsIgnoreCase("false"))) { + return false; + } else if (object.equals(Boolean.TRUE) + || (object instanceof String && ((String) object) + .equalsIgnoreCase("true"))) { + return true; + } + throw new JSONException("JSONObject[" + quote(key) + + "] is not a Boolean."); + } + + /** + * Get the BigInteger value associated with a key. + * + * @param key A key string. + * @return The numeric value. + * @throws JSONException if the key is not found or if the value cannot + * be converted to BigInteger. + */ + public BigInteger getBigInteger(String key) throws JSONException { + Object object = this.get(key); + try { + return new BigInteger(object.toString()); + } catch (Exception e) { + throw new JSONException("JSONObject[" + quote(key) + + "] could not be converted to BigInteger.", e); + } + } + + /** + * Get the BigDecimal value associated with a key. + * + * @param key A key string. + * @return The numeric value. + * @throws JSONException if the key is not found or if the value + * cannot be converted to BigDecimal. + */ + public BigDecimal getBigDecimal(String key) throws JSONException { + Object object = this.get(key); + if (object instanceof BigDecimal) { + return (BigDecimal) object; + } + try { + return new BigDecimal(object.toString()); + } catch (Exception e) { + throw new JSONException("JSONObject[" + quote(key) + + "] could not be converted to BigDecimal.", e); + } + } + + /** + * Get the double value associated with a key. + * + * @param key A key string. + * @return The numeric value. + * @throws JSONException if the key is not found or if the value is not a Number + * object and cannot be converted to a number. + */ + public double getDouble(String key) throws JSONException { + Object object = this.get(key); + try { + return object instanceof Number ? ((Number) object).doubleValue() + : Double.parseDouble(object.toString()); + } catch (Exception e) { + throw new JSONException("JSONObject[" + quote(key) + + "] is not a number.", e); + } + } + + /** + * Get the float value associated with a key. + * + * @param key A key string. + * @return The numeric value. + * @throws JSONException if the key is not found or if the value is not a Number + * object and cannot be converted to a number. + */ + public float getFloat(String key) throws JSONException { + Object object = this.get(key); + try { + return object instanceof Number ? ((Number) object).floatValue() + : Float.parseFloat(object.toString()); + } catch (Exception e) { + throw new JSONException("JSONObject[" + quote(key) + + "] is not a number.", e); + } + } + + /** + * Get the Number value associated with a key. + * + * @param key A key string. + * @return The numeric value. + * @throws JSONException if the key is not found or if the value is not a Number + * object and cannot be converted to a number. + */ + public Number getNumber(String key) throws JSONException { + Object object = this.get(key); + try { + if (object instanceof Number) { + return (Number) object; + } + return stringToNumber(object.toString()); + } catch (Exception e) { + throw new JSONException("JSONObject[" + quote(key) + + "] is not a number.", e); + } + } + + /** + * Get the int value associated with a key. + * + * @param key A key string. + * @return The integer value. + * @throws JSONException if the key is not found or if the value cannot be converted + * to an integer. + */ + public int getInt(String key) throws JSONException { + Object object = this.get(key); + try { + return object instanceof Number ? ((Number) object).intValue() + : Integer.parseInt((String) object); + } catch (Exception e) { + throw new JSONException("JSONObject[" + quote(key) + + "] is not an int.", e); + } + } + + /** + * Get the JSONArray value associated with a key. + * + * @param key A key string. + * @return A JSONArray which is the value. + * @throws JSONException if the key is not found or if the value is not a JSONArray. + */ + public JSONArray getJSONArray(String key) throws JSONException { + Object object = this.get(key); + if (object instanceof JSONArray) { + return (JSONArray) object; + } + throw new JSONException("JSONObject[" + quote(key) + + "] is not a JSONArray."); + } + + /** + * Get the JSONObject value associated with a key. + * + * @param key A key string. + * @return A JSONObject which is the value. + * @throws JSONException if the key is not found or if the value is not a JSONObject. + */ + public JSONOrderedObject getJSONObject(String key) throws JSONException { + Object object = this.get(key); + if (object instanceof JSONOrderedObject) { + return (JSONOrderedObject) object; + } + throw new JSONException("JSONObject[" + quote(key) + + "] is not a JSONObject."); + } + + /** + * Get the long value associated with a key. + * + * @param key A key string. + * @return The long value. + * @throws JSONException if the key is not found or if the value cannot be converted + * to a long. + */ + public long getLong(String key) throws JSONException { + Object object = this.get(key); + try { + return object instanceof Number ? ((Number) object).longValue() + : Long.parseLong((String) object); + } catch (Exception e) { + throw new JSONException("JSONObject[" + quote(key) + + "] is not a long.", e); + } + } + + /** + * Get an array of field names from a JSONObject. + * + * @return An array of field names, or null if there are no names. + */ + public static String[] getNames(JSONOrderedObject jo) { + if (jo.isEmpty()) { + return null; + } + return jo.keySet().toArray(new String[jo.length()]); + } + + /** + * Get an array of field names from an Object. + * + * @return An array of field names, or null if there are no names. + */ + public static String[] getNames(Object object) { + if (object == null) { + return null; + } + Class klass = object.getClass(); + Field[] fields = klass.getFields(); + int length = fields.length; + if (length == 0) { + return null; + } + String[] names = new String[length]; + for (int i = 0; i < length; i += 1) { + names[i] = fields[i].getName(); + } + return names; + } + + /** + * Get the string associated with a key. + * + * @param key A key string. + * @return A string which is the value. + * @throws JSONException if there is no string value for the key. + */ + public String getString(String key) throws JSONException { + Object object = this.get(key); + if (object instanceof String) { + return (String) object; + } + throw new JSONException("JSONObject[" + quote(key) + "] not a string."); + } + + /** + * Determine if the JSONObject contains a specific key. + * + * @param key A key string. + * @return true if the key exists in the JSONObject. + */ + public boolean has(String key) { + return this.map.containsKey(key); + } + + /** + * Increment a property of a JSONObject. If there is no such property, + * create one with a value of 1. If there is such a property, and if it is + * an Integer, Long, Double, or Float, then add one to it. + * + * @param key A key string. + * @return this. + * @throws JSONException If there is already a property with this name that is not an + * Integer, Long, Double, or Float. + */ + public JSONOrderedObject increment(String key) throws JSONException { + Object value = this.opt(key); + if (value == null) { + this.put(key, 1); + } else if (value instanceof BigInteger) { + this.put(key, ((BigInteger) value).add(BigInteger.ONE)); + } else if (value instanceof BigDecimal) { + this.put(key, ((BigDecimal) value).add(BigDecimal.ONE)); + } else if (value instanceof Integer) { + this.put(key, ((Integer) value).intValue() + 1); + } else if (value instanceof Long) { + this.put(key, ((Long) value).longValue() + 1L); + } else if (value instanceof Double) { + this.put(key, ((Double) value).doubleValue() + 1.0d); + } else if (value instanceof Float) { + this.put(key, ((Float) value).floatValue() + 1.0f); + } else { + throw new JSONException("Unable to increment [" + quote(key) + "]."); + } + return this; + } + + /** + * Determine if the value associated with the key is null or if there is no + * value. + * + * @param key A key string. + * @return true if there is no value associated with the key or if the value + * is the JSONObject.NULL object. + */ + public boolean isNull(String key) { + return JSONOrderedObject.NULL.equals(this.opt(key)); + } + + /** + * Get an enumeration of the keys of the JSONObject. Modifying this key Set will also + * modify the JSONObject. Use with caution. + * + * @return An iterator of the keys. + * @see Set#iterator() + */ + public Iterator keys() { + return this.keySet().iterator(); + } + + /** + * Get a set of keys of the JSONObject. Modifying this key Set will also modify the + * JSONObject. Use with caution. + * + * @return A keySet. + * @see Map#keySet() + */ + public Set keySet() { + return this.map.keySet(); + } + + /** + * Get a set of entries of the JSONObject. These are raw values and may not + * match what is returned by the JSONObject get* and opt* functions. Modifying + * the returned EntrySet or the Entry objects contained therein will modify the + * backing JSONObject. This does not return a clone or a read-only view. + *

+ * Use with caution. + * + * @return An Entry Set + * @see Map#entrySet() + */ + protected Set> entrySet() { + return this.map.entrySet(); + } + + /** + * Get the number of keys stored in the JSONObject. + * + * @return The number of keys in the JSONObject. + */ + public int length() { + return this.map.size(); + } + + /** + * Check if JSONObject is empty. + * + * @return true if JSONObject is empty, otherwise false. + */ + public boolean isEmpty() { + return map.isEmpty(); + } + + /** + * Produce a JSONArray containing the names of the elements of this + * JSONObject. + * + * @return A JSONArray containing the key strings, or null if the JSONObject + * is empty. + */ + public JSONArray names() { + if (this.map.isEmpty()) { + return null; + } + return new JSONArray(this.map.keySet()); + } + + /** + * Produce a string from a Number. + * + * @param number A Number + * @return A String. + * @throws JSONException If n is a non-finite number. + */ + public static String numberToString(Number number) throws JSONException { + if (number == null) { + throw new JSONException("Null pointer"); + } + testValidity(number); + + // Shave off trailing zeros and decimal point, if possible. + + String string = number.toString(); + if (string.indexOf('.') > 0 && string.indexOf('e') < 0 + && string.indexOf('E') < 0) { + while (string.endsWith("0")) { + string = string.substring(0, string.length() - 1); + } + if (string.endsWith(".")) { + string = string.substring(0, string.length() - 1); + } + } + return string; + } + + /** + * Get an optional value associated with a key. + * + * @param key A key string. + * @return An object which is the value, or null if there is no value. + */ + public Object opt(String key) { + return key == null ? null : this.map.get(key); + } + + /** + * Get the enum value associated with a key. + * + * @param clazz The type of enum to retrieve. + * @param key A key string. + * @return The enum value associated with the key or null if not found + */ + public > E optEnum(Class clazz, String key) { + return this.optEnum(clazz, key, null); + } + + /** + * Get the enum value associated with a key. + * + * @param clazz The type of enum to retrieve. + * @param key A key string. + * @param defaultValue The default in case the value is not found + * @return The enum value associated with the key or defaultValue + * if the value is not found or cannot be assigned to clazz + */ + public > E optEnum(Class clazz, String key, E defaultValue) { + try { + Object val = this.opt(key); + if (NULL.equals(val)) { + return defaultValue; + } + if (clazz.isAssignableFrom(val.getClass())) { + // we just checked it! + @SuppressWarnings("unchecked") + E myE = (E) val; + return myE; + } + return Enum.valueOf(clazz, val.toString()); + } catch (IllegalArgumentException e) { + return defaultValue; + } catch (NullPointerException e) { + return defaultValue; + } + } + + /** + * Get an optional boolean associated with a key. It returns false if there + * is no such key, or if the value is not Boolean.TRUE or the String "true". + * + * @param key A key string. + * @return The truth. + */ + public boolean optBoolean(String key) { + return this.optBoolean(key, false); + } + + /** + * Get an optional boolean associated with a key. It returns the + * defaultValue if there is no such key, or if it is not a Boolean or the + * String "true" or "false" (case insensitive). + * + * @param key A key string. + * @param defaultValue The default. + * @return The truth. + */ + public boolean optBoolean(String key, boolean defaultValue) { + Object val = this.opt(key); + if (NULL.equals(val)) { + return defaultValue; + } + if (val instanceof Boolean) { + return ((Boolean) val).booleanValue(); + } + try { + // we'll use the get anyway because it does string conversion. + return this.getBoolean(key); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get an optional BigDecimal associated with a key, or the defaultValue if + * there is no such key or if its value is not a number. If the value is a + * string, an attempt will be made to evaluate it as a number. + * + * @param key A key string. + * @param defaultValue The default. + * @return An object which is the value. + */ + public BigDecimal optBigDecimal(String key, BigDecimal defaultValue) { + Object val = this.opt(key); + if (NULL.equals(val)) { + return defaultValue; + } + if (val instanceof BigDecimal) { + return (BigDecimal) val; + } + if (val instanceof BigInteger) { + return new BigDecimal((BigInteger) val); + } + if (val instanceof Double || val instanceof Float) { + return new BigDecimal(((Number) val).doubleValue()); + } + if (val instanceof Long || val instanceof Integer + || val instanceof Short || val instanceof Byte) { + return new BigDecimal(((Number) val).longValue()); + } + // don't check if it's a string in case of unchecked Number subclasses + try { + return new BigDecimal(val.toString()); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get an optional BigInteger associated with a key, or the defaultValue if + * there is no such key or if its value is not a number. If the value is a + * string, an attempt will be made to evaluate it as a number. + * + * @param key A key string. + * @param defaultValue The default. + * @return An object which is the value. + */ + public BigInteger optBigInteger(String key, BigInteger defaultValue) { + Object val = this.opt(key); + if (NULL.equals(val)) { + return defaultValue; + } + if (val instanceof BigInteger) { + return (BigInteger) val; + } + if (val instanceof BigDecimal) { + return ((BigDecimal) val).toBigInteger(); + } + if (val instanceof Double || val instanceof Float) { + return new BigDecimal(((Number) val).doubleValue()).toBigInteger(); + } + if (val instanceof Long || val instanceof Integer + || val instanceof Short || val instanceof Byte) { + return BigInteger.valueOf(((Number) val).longValue()); + } + // don't check if it's a string in case of unchecked Number subclasses + try { + // the other opt functions handle implicit conversions, i.e. + // jo.put("double",1.1d); + // jo.optInt("double"); -- will return 1, not an error + // this conversion to BigDecimal then to BigInteger is to maintain + // that type cast support that may truncate the decimal. + final String valStr = val.toString(); + if (isDecimalNotation(valStr)) { + return new BigDecimal(valStr).toBigInteger(); + } + return new BigInteger(valStr); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get an optional double associated with a key, or NaN if there is no such + * key or if its value is not a number. If the value is a string, an attempt + * will be made to evaluate it as a number. + * + * @param key A string which is the key. + * @return An object which is the value. + */ + public double optDouble(String key) { + return this.optDouble(key, Double.NaN); + } + + /** + * Get an optional double associated with a key, or the defaultValue if + * there is no such key or if its value is not a number. If the value is a + * string, an attempt will be made to evaluate it as a number. + * + * @param key A key string. + * @param defaultValue The default. + * @return An object which is the value. + */ + public double optDouble(String key, double defaultValue) { + Object val = this.opt(key); + if (NULL.equals(val)) { + return defaultValue; + } + if (val instanceof Number) { + return ((Number) val).doubleValue(); + } + if (val instanceof String) { + try { + return Double.parseDouble((String) val); + } catch (Exception e) { + return defaultValue; + } + } + return defaultValue; + } + + /** + * Get the optional double value associated with an index. NaN is returned + * if there is no value for the index, or if the value is not a number and + * cannot be converted to a number. + * + * @param key A key string. + * @return The value. + */ + public float optFloat(String key) { + return this.optFloat(key, Float.NaN); + } + + /** + * Get the optional double value associated with an index. The defaultValue + * is returned if there is no value for the index, or if the value is not a + * number and cannot be converted to a number. + * + * @param key A key string. + * @param defaultValue The default value. + * @return The value. + */ + public float optFloat(String key, float defaultValue) { + Object val = this.opt(key); + if (JSONOrderedObject.NULL.equals(val)) { + return defaultValue; + } + if (val instanceof Number) { + return ((Number) val).floatValue(); + } + if (val instanceof String) { + try { + return Float.parseFloat((String) val); + } catch (Exception e) { + return defaultValue; + } + } + return defaultValue; + } + + /** + * Get an optional int value associated with a key, or zero if there is no + * such key or if the value is not a number. If the value is a string, an + * attempt will be made to evaluate it as a number. + * + * @param key A key string. + * @return An object which is the value. + */ + public int optInt(String key) { + return this.optInt(key, 0); + } + + /** + * Get an optional int value associated with a key, or the default if there + * is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number. + * + * @param key A key string. + * @param defaultValue The default. + * @return An object which is the value. + */ + public int optInt(String key, int defaultValue) { + Object val = this.opt(key); + if (NULL.equals(val)) { + return defaultValue; + } + if (val instanceof Number) { + return ((Number) val).intValue(); + } + + if (val instanceof String) { + try { + return new BigDecimal((String) val).intValue(); + } catch (Exception e) { + return defaultValue; + } + } + return defaultValue; + } + + /** + * Get an optional JSONArray associated with a key. It returns null if there + * is no such key, or if its value is not a JSONArray. + * + * @param key A key string. + * @return A JSONArray which is the value. + */ + public JSONArray optJSONArray(String key) { + Object o = this.opt(key); + return o instanceof JSONArray ? (JSONArray) o : null; + } + + /** + * Get an optional JSONObject associated with a key. It returns null if + * there is no such key, or if its value is not a JSONObject. + * + * @param key A key string. + * @return A JSONObject which is the value. + */ + public JSONOrderedObject optJSONObject(String key) { + Object object = this.opt(key); + return object instanceof JSONOrderedObject ? (JSONOrderedObject) object : null; + } + + /** + * Get an optional long value associated with a key, or zero if there is no + * such key or if the value is not a number. If the value is a string, an + * attempt will be made to evaluate it as a number. + * + * @param key A key string. + * @return An object which is the value. + */ + public long optLong(String key) { + return this.optLong(key, 0); + } + + /** + * Get an optional long value associated with a key, or the default if there + * is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number. + * + * @param key A key string. + * @param defaultValue The default. + * @return An object which is the value. + */ + public long optLong(String key, long defaultValue) { + Object val = this.opt(key); + if (NULL.equals(val)) { + return defaultValue; + } + if (val instanceof Number) { + return ((Number) val).longValue(); + } + + if (val instanceof String) { + try { + return new BigDecimal((String) val).longValue(); + } catch (Exception e) { + return defaultValue; + } + } + return defaultValue; + } + + /** + * Get an optional {@link Number} value associated with a key, or null + * if there is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number ({@link BigDecimal}). This method + * would be used in cases where type coercion of the number value is unwanted. + * + * @param key A key string. + * @return An object which is the value. + */ + public Number optNumber(String key) { + return this.optNumber(key, null); + } + + /** + * Get an optional {@link Number} value associated with a key, or the default if there + * is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number. This method + * would be used in cases where type coercion of the number value is unwanted. + * + * @param key A key string. + * @param defaultValue The default. + * @return An object which is the value. + */ + public Number optNumber(String key, Number defaultValue) { + Object val = this.opt(key); + if (NULL.equals(val)) { + return defaultValue; + } + if (val instanceof Number) { + return (Number) val; + } + + if (val instanceof String) { + try { + return stringToNumber((String) val); + } catch (Exception e) { + return defaultValue; + } + } + return defaultValue; + } + + /** + * Get an optional string associated with a key. It returns an empty string + * if there is no such key. If the value is not a string and is not null, + * then it is converted to a string. + * + * @param key A key string. + * @return A string which is the value. + */ + public String optString(String key) { + return this.optString(key, ""); + } + + /** + * Get an optional string associated with a key. It returns the defaultValue + * if there is no such key. + * + * @param key A key string. + * @param defaultValue The default. + * @return A string which is the value. + */ + public String optString(String key, String defaultValue) { + Object object = this.opt(key); + return NULL.equals(object) ? defaultValue : object.toString(); + } + + /** + * Populates the internal map of the JSONObject with the bean properties. The + * bean can not be recursive. + * + * @param bean the bean + * @see JSONOrderedObject#JSONOrderedObject(Object) + */ + private void populateMap(Object bean) { + Class klass = bean.getClass(); + + // If klass is a System class then set includeSuperClass to false. + + boolean includeSuperClass = klass.getClassLoader() != null; + + Method[] methods = includeSuperClass ? klass.getMethods() : klass.getDeclaredMethods(); + for (final Method method : methods) { + final int modifiers = method.getModifiers(); + if (Modifier.isPublic(modifiers) + && !Modifier.isStatic(modifiers) + && method.getParameterTypes().length == 0 + && !method.isBridge() + && method.getReturnType() != Void.TYPE + && isValidMethodName(method.getName())) { + final String key = getKeyNameFromMethod(method); + if (key != null && !key.isEmpty()) { + try { + final Object result = method.invoke(bean); + if (result != null) { + this.map.put(key, wrap(result)); + // we don't use the result anywhere outside of wrap + // if it's a resource we should be sure to close it + // after calling toString + if (result instanceof Closeable) { + try { + ((Closeable) result).close(); + } catch (IOException ignore) { + } + } + } + } catch (IllegalAccessException ignore) { + } catch (IllegalArgumentException ignore) { + } catch (InvocationTargetException ignore) { + } + } + } + } + } + + private boolean isValidMethodName(String name) { + return !"getClass".equals(name) && !"getDeclaringClass".equals(name); + } + + private String getKeyNameFromMethod(Method method) { + final int ignoreDepth = getAnnotationDepth(method, JSONPropertyIgnore.class); + if (ignoreDepth > 0) { + final int forcedNameDepth = getAnnotationDepth(method, JSONPropertyName.class); + if (forcedNameDepth < 0 || ignoreDepth <= forcedNameDepth) { + // the hierarchy asked to ignore, and the nearest name override + // was higher or non-existent + return null; + } + } + JSONPropertyName annotation = getAnnotation(method, JSONPropertyName.class); + if (annotation != null && annotation.value() != null && !annotation.value().isEmpty()) { + return annotation.value(); + } + String key; + final String name = method.getName(); + if (name.startsWith("get") && name.length() > 3) { + key = name.substring(3); + } else if (name.startsWith("is") && name.length() > 2) { + key = name.substring(2); + } else { + return null; + } + // if the first letter in the key is not uppercase, then skip. + // This is to maintain backwards compatibility before PR406 + // (https://github.com/stleary/JSON-java/pull/406/) + if (Character.isLowerCase(key.charAt(0))) { + return null; + } + if (key.length() == 1) { + key = key.toLowerCase(Locale.ROOT); + } else if (!Character.isUpperCase(key.charAt(1))) { + key = key.substring(0, 1).toLowerCase(Locale.ROOT) + key.substring(1); + } + return key; + } + + /** + * Searches the class hierarchy to see if the method or it's super + * implementations and interfaces has the annotation. + * + * @param type of the annotation + * @param m method to check + * @param annotationClass annotation to look for + * @return the {@link Annotation} if the annotation exists on the current method + * or one of it's super class definitions + */ + private static A getAnnotation(final Method m, final Class annotationClass) { + // if we have invalid data the result is null + if (m == null || annotationClass == null) { + return null; + } + + if (m.isAnnotationPresent(annotationClass)) { + return m.getAnnotation(annotationClass); + } + + // if we've already reached the Object class, return null; + Class c = m.getDeclaringClass(); + if (c.getSuperclass() == null) { + return null; + } + + // check directly implemented interfaces for the method being checked + for (Class i : c.getInterfaces()) { + try { + Method im = i.getMethod(m.getName(), m.getParameterTypes()); + return getAnnotation(im, annotationClass); + } catch (final SecurityException ex) { + continue; + } catch (final NoSuchMethodException ex) { + continue; + } + } + + try { + return getAnnotation( + c.getSuperclass().getMethod(m.getName(), m.getParameterTypes()), + annotationClass); + } catch (final SecurityException ex) { + return null; + } catch (final NoSuchMethodException ex) { + return null; + } + } + + /** + * Searches the class hierarchy to see if the method or it's super + * implementations and interfaces has the annotation. Returns the depth of the + * annotation in the hierarchy. + * + * @param m method to check + * @param annotationClass annotation to look for + * @return Depth of the annotation or -1 if the annotation is not on the method. + */ + private static int getAnnotationDepth(final Method m, final Class annotationClass) { + // if we have invalid data the result is -1 + if (m == null || annotationClass == null) { + return -1; + } + + if (m.isAnnotationPresent(annotationClass)) { + return 1; + } + + // if we've already reached the Object class, return -1; + Class c = m.getDeclaringClass(); + if (c.getSuperclass() == null) { + return -1; + } + + // check directly implemented interfaces for the method being checked + for (Class i : c.getInterfaces()) { + try { + Method im = i.getMethod(m.getName(), m.getParameterTypes()); + int d = getAnnotationDepth(im, annotationClass); + if (d > 0) { + // since the annotation was on the interface, add 1 + return d + 1; + } + } catch (final SecurityException ex) { + continue; + } catch (final NoSuchMethodException ex) { + continue; + } + } + + try { + int d = getAnnotationDepth( + c.getSuperclass().getMethod(m.getName(), m.getParameterTypes()), + annotationClass); + if (d > 0) { + // since the annotation was on the superclass, add 1 + return d + 1; + } + return -1; + } catch (final SecurityException ex) { + return -1; + } catch (final NoSuchMethodException ex) { + return -1; + } + } + + /** + * Put a key/boolean pair in the JSONObject. + * + * @param key A key string. + * @param value A boolean which is the value. + * @return this. + * @throws JSONException If the value is non-finite number. + * @throws NullPointerException If the key is null. + */ + public JSONOrderedObject put(String key, boolean value) throws JSONException { + return this.put(key, value ? Boolean.TRUE : Boolean.FALSE); + } + + /** + * Put a key/value pair in the JSONObject, where the value will be a + * JSONArray which is produced from a Collection. + * + * @param key A key string. + * @param value A Collection value. + * @return this. + * @throws JSONException If the value is non-finite number. + * @throws NullPointerException If the key is null. + */ + public JSONOrderedObject put(String key, Collection value) throws JSONException { + return this.put(key, new JSONArray(value)); + } + + /** + * Put a key/double pair in the JSONObject. + * + * @param key A key string. + * @param value A double which is the value. + * @return this. + * @throws JSONException If the value is non-finite number. + * @throws NullPointerException If the key is null. + */ + public JSONOrderedObject put(String key, double value) throws JSONException { + return this.put(key, Double.valueOf(value)); + } + + /** + * Put a key/float pair in the JSONObject. + * + * @param key A key string. + * @param value A float which is the value. + * @return this. + * @throws JSONException If the value is non-finite number. + * @throws NullPointerException If the key is null. + */ + public JSONOrderedObject put(String key, float value) throws JSONException { + return this.put(key, Float.valueOf(value)); + } + + /** + * Put a key/int pair in the JSONObject. + * + * @param key A key string. + * @param value An int which is the value. + * @return this. + * @throws JSONException If the value is non-finite number. + * @throws NullPointerException If the key is null. + */ + public JSONOrderedObject put(String key, int value) throws JSONException { + return this.put(key, Integer.valueOf(value)); + } + + /** + * Put a key/long pair in the JSONObject. + * + * @param key A key string. + * @param value A long which is the value. + * @return this. + * @throws JSONException If the value is non-finite number. + * @throws NullPointerException If the key is null. + */ + public JSONOrderedObject put(String key, long value) throws JSONException { + return this.put(key, Long.valueOf(value)); + } + + /** + * Put a key/value pair in the JSONObject, where the value will be a + * JSONObject which is produced from a Map. + * + * @param key A key string. + * @param value A Map value. + * @return this. + * @throws JSONException If the value is non-finite number. + * @throws NullPointerException If the key is null. + */ + public JSONOrderedObject put(String key, Map value) throws JSONException { + return this.put(key, new JSONOrderedObject(value)); + } + + /** + * Put a key/value pair in the JSONObject. If the value is null, then the + * key will be removed from the JSONObject if it is present. + * + * @param key A key string. + * @param value An object which is the value. It should be of one of these + * types: Boolean, Double, Integer, JSONArray, JSONObject, Long, + * String, or the JSONObject.NULL object. + * @return this. + * @throws JSONException If the value is non-finite number. + * @throws NullPointerException If the key is null. + */ + public JSONOrderedObject put(String key, Object value) throws JSONException { + if (key == null) { + throw new NullPointerException("Null key."); + } + if (value != null) { + testValidity(value); + this.map.put(key, value); + } else { + this.remove(key); + } + return this; + } + + /** + * Put a key/value pair in the JSONObject, but only if the key and the value + * are both non-null, and only if there is not already a member with that + * name. + * + * @param key string + * @param value object + * @return this. + * @throws JSONException if the key is a duplicate + */ + public JSONOrderedObject putOnce(String key, Object value) throws JSONException { + if (key != null && value != null) { + if (this.opt(key) != null) { + throw new JSONException("Duplicate key \"" + key + "\""); + } + return this.put(key, value); + } + return this; + } + + /** + * Put a key/value pair in the JSONObject, but only if the key and the value + * are both non-null. + * + * @param key A key string. + * @param value An object which is the value. It should be of one of these + * types: Boolean, Double, Integer, JSONArray, JSONObject, Long, + * String, or the JSONObject.NULL object. + * @return this. + * @throws JSONException If the value is a non-finite number. + */ + public JSONOrderedObject putOpt(String key, Object value) throws JSONException { + if (key != null && value != null) { + return this.put(key, value); + } + return this; + } + + /** + * Creates a JSONPointer using an initialization string and tries to + * match it to an item within this JSONObject. For example, given a + * JSONObject initialized with this document: + *

+     * {
+     *     "a":{"b":"c"}
+     * }
+     * 
+ * and this JSONPointer string: + *
+     * "/a/b"
+     * 
+ * Then this method will return the String "c". + * A JSONPointerException may be thrown from code called by this method. + * + * @param jsonPointer string that can be used to create a JSONPointer + * @return the item matched by the JSONPointer, otherwise null + */ + public Object query(String jsonPointer) { + return query(new JSONPointer(jsonPointer)); + } + + /** + * Uses a user initialized JSONPointer and tries to + * match it to an item within this JSONObject. For example, given a + * JSONObject initialized with this document: + *
+     * {
+     *     "a":{"b":"c"}
+     * }
+     * 
+ * and this JSONPointer: + *
+     * "/a/b"
+     * 
+ * Then this method will return the String "c". + * A JSONPointerException may be thrown from code called by this method. + * + * @param jsonPointer string that can be used to create a JSONPointer + * @return the item matched by the JSONPointer, otherwise null + */ + public Object query(JSONPointer jsonPointer) { + return jsonPointer.queryFrom(this); + } + + /** + * Queries and returns a value from this object using {@code jsonPointer}, or + * returns null if the query fails due to a missing key. + * + * @param jsonPointer the string representation of the JSON pointer + * @return the queried value or {@code null} + * @throws IllegalArgumentException if {@code jsonPointer} has invalid syntax + */ + public Object optQuery(String jsonPointer) { + return optQuery(new JSONPointer(jsonPointer)); + } + + /** + * Queries and returns a value from this object using {@code jsonPointer}, or + * returns null if the query fails due to a missing key. + * + * @param jsonPointer The JSON pointer + * @return the queried value or {@code null} + * @throws IllegalArgumentException if {@code jsonPointer} has invalid syntax + */ + public Object optQuery(JSONPointer jsonPointer) { + try { + return jsonPointer.queryFrom(this); + } catch (JSONPointerException e) { + return null; + } + } + + /** + * Produce a string in double quotes with backslash sequences in all the + * right places. A backslash will be inserted within = '\u0080' && c < '\u00a0') + || (c >= '\u2000' && c < '\u2100')) { + w.write("\\u"); + hhhh = Integer.toHexString(c); + w.write("0000", 0, 4 - hhhh.length()); + w.write(hhhh); + } else { + w.write(c); + } + } + } + w.write('"'); + return w; + } + + /** + * Remove a name and its value, if present. + * + * @param key The name to be removed. + * @return The value that was associated with the name, or null if there was + * no value. + */ + public Object remove(String key) { + return this.map.remove(key); + } + + /** + * Determine if two JSONObjects are similar. + * They must contain the same set of names which must be associated with + * similar values. + * + * @param other The other JSONObject + * @return true if they are equal + */ + public boolean similar(Object other) { + try { + if (!(other instanceof JSONOrderedObject)) { + return false; + } + if (!this.keySet().equals(((JSONOrderedObject) other).keySet())) { + return false; + } + for (final Entry entry : this.entrySet()) { + String name = entry.getKey(); + Object valueThis = entry.getValue(); + Object valueOther = ((JSONOrderedObject) other).get(name); + if (valueThis == valueOther) { + continue; + } + if (valueThis == null) { + return false; + } + if (valueThis instanceof JSONOrderedObject) { + if (!((JSONOrderedObject) valueThis).similar(valueOther)) { + return false; + } + } else if (valueThis instanceof JSONArray) { + if (!((JSONArray) valueThis).similar(valueOther)) { + return false; + } + } else if (!valueThis.equals(valueOther)) { + return false; + } + } + return true; + } catch (Throwable exception) { + return false; + } + } + + /** + * Tests if the value should be tried as a decimal. It makes no test if there are actual digits. + * + * @param val value to test + * @return true if the string is "-0" or if it contains '.', 'e', or 'E', false otherwise. + */ + protected static boolean isDecimalNotation(final String val) { + return val.indexOf('.') > -1 || val.indexOf('e') > -1 + || val.indexOf('E') > -1 || "-0".equals(val); + } + + /** + * Converts a string to a number using the narrowest possible type. Possible + * returns for this function are BigDecimal, Double, BigInteger, Long, and Integer. + * When a Double is returned, it should always be a valid Double and not NaN or +-infinity. + * + * @param val value to convert + * @return Number representation of the value. + * @throws NumberFormatException thrown if the value is not a valid number. A public + * caller should catch this and wrap it in a {@link JSONException} if applicable. + */ + protected static Number stringToNumber(final String val) throws NumberFormatException { + char initial = val.charAt(0); + if ((initial >= '0' && initial <= '9') || initial == '-') { + // decimal representation + if (isDecimalNotation(val)) { + // quick dirty way to see if we need a BigDecimal instead of a Double + // this only handles some cases of overflow or underflow + if (val.length() > 14) { + return new BigDecimal(val); + } + final Double d = Double.valueOf(val); + if (d.isInfinite() || d.isNaN()) { + // if we can't parse it as a double, go up to BigDecimal + // this is probably due to underflow like 4.32e-678 + // or overflow like 4.65e5324. The size of the string is small + // but can't be held in a Double. + return new BigDecimal(val); + } + return d; + } + // integer representation. + // This will narrow any values to the smallest reasonable Object representation + // (Integer, Long, or BigInteger) + + // string version + // The compare string length method reduces GC, + // but leads to smaller integers being placed in larger wrappers even though not + // needed. i.e. 1,000,000,000 -> Long even though it's an Integer + // 1,000,000,000,000,000,000 -> BigInteger even though it's a Long + //if(val.length()<=9){ + // return Integer.valueOf(val); + //} + //if(val.length()<=18){ + // return Long.valueOf(val); + //} + //return new BigInteger(val); + + // BigInteger version: We use a similar bitLenth compare as + // BigInteger#intValueExact uses. Increases GC, but objects hold + // only what they need. i.e. Less runtime overhead if the value is + // long lived. Which is the better tradeoff? This is closer to what's + // in stringToValue. + BigInteger bi = new BigInteger(val); + if (bi.bitLength() <= 31) { + return Integer.valueOf(bi.intValue()); + } + if (bi.bitLength() <= 63) { + return Long.valueOf(bi.longValue()); + } + return bi; + } + throw new NumberFormatException("val [" + val + "] is not a valid number."); + } + + /** + * Try to convert a string into a number, boolean, or null. If the string + * can't be converted, return the string. + * + * @param string A String. + * @return A simple JSON value. + */ + // Changes to this method must be copied to the corresponding method in + // the XML class to keep full support for Android + public static Object stringToValue(String string) { + if (string.equals("")) { + return string; + } + if (string.equalsIgnoreCase("true")) { + return Boolean.TRUE; + } + if (string.equalsIgnoreCase("false")) { + return Boolean.FALSE; + } + if (string.equalsIgnoreCase("null")) { + return JSONOrderedObject.NULL; + } + + /* + * If it might be a number, try converting it. If a number cannot be + * produced, then the value will just be a string. + */ + + char initial = string.charAt(0); + if ((initial >= '0' && initial <= '9') || initial == '-') { + try { + // if we want full Big Number support this block can be replaced with: + // return stringToNumber(string); + if (isDecimalNotation(string)) { + Double d = Double.valueOf(string); + if (!d.isInfinite() && !d.isNaN()) { + return d; + } + } else { + Long myLong = Long.valueOf(string); + if (string.equals(myLong.toString())) { + if (myLong.longValue() == myLong.intValue()) { + return Integer.valueOf(myLong.intValue()); + } + return myLong; + } + } + } catch (Exception ignore) { + } + } + return string; + } + + /** + * Throw an exception if the object is a NaN or infinite number. + * + * @param o The object to test. + * @throws JSONException If o is a non-finite number. + */ + public static void testValidity(Object o) throws JSONException { + if (o != null) { + if (o instanceof Double) { + if (((Double) o).isInfinite() || ((Double) o).isNaN()) { + throw new JSONException( + "JSON does not allow non-finite numbers."); + } + } else if (o instanceof Float) { + if (((Float) o).isInfinite() || ((Float) o).isNaN()) { + throw new JSONException( + "JSON does not allow non-finite numbers."); + } + } + } + } + + /** + * Produce a JSONArray containing the values of the members of this + * JSONObject. + * + * @param names A JSONArray containing a list of key strings. This determines + * the sequence of the values in the result. + * @return A JSONArray of values. + * @throws JSONException If any of the values are non-finite numbers. + */ + public JSONArray toJSONArray(JSONArray names) throws JSONException { + if (names == null || names.isEmpty()) { + return null; + } + JSONArray ja = new JSONArray(); + for (int i = 0; i < names.length(); i += 1) { + ja.put(this.opt(names.getString(i))); + } + return ja; + } + + /** + * Make a JSON text of this JSONObject. For compactness, no whitespace is + * added. If this would not result in a syntactically correct JSON text, + * then null will be returned instead. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * + * @return a printable, displayable, portable, transmittable representation + * of the object, beginning with { (left + * brace) and ending with } (right + * brace). + */ + @Override + public String toString() { + try { + return this.toString(0); + } catch (Exception e) { + return null; + } + } + + /** + * Make a pretty-printed JSON text of this JSONObject. + * + *

If indentFactor > 0 and the {@link JSONOrderedObject} + * has only one key, then the object will be output on a single line: + *

{@code {"key": 1}}
+ * + *

If an object has 2 or more keys, then it will be output across + * multiple lines:

{
+     *  "key1": 1,
+     *  "key2": "value 2",
+     *  "key3": 3
+     * }
+ *

+ * Warning: This method assumes that the data structure is acyclical. + * + * + * @param indentFactor The number of spaces to add to each level of indentation. + * @return a printable, displayable, portable, transmittable representation + * of the object, beginning with { (left + * brace) and ending with } (right + * brace). + * @throws JSONException If the object contains an invalid number. + */ + public String toString(int indentFactor) throws JSONException { + StringWriter w = new StringWriter(); + synchronized (w.getBuffer()) { + return this.write(w, indentFactor, 0).toString(); + } + } + + /** + * Make a JSON text of an Object value. If the object has an + * value.toJSONString() method, then that method will be used to produce the + * JSON text. The method is required to produce a strictly conforming text. + * If the object does not contain a toJSONString method (which is the most + * common case), then a text will be produced by other means. If the value + * is an array or Collection, then a JSONArray will be made from it and its + * toJSONString method will be called. If the value is a MAP, then a + * JSONObject will be made from it and its toJSONString method will be + * called. Otherwise, the value's toString method will be called, and the + * result will be quoted. + * + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @param value The value to be serialized. + * @return a printable, displayable, transmittable representation of the + * object, beginning with { (left + * brace) and ending with } (right + * brace). + * @throws JSONException If the value is or contains an invalid number. + */ + public static String valueToString(Object value) throws JSONException { + // moves the implementation to JSONWriter as: + // 1. It makes more sense to be part of the writer class + // 2. For Android support this method is not available. By implementing it in the Writer + // Android users can use the writer with the built in Android JSONObject implementation. + return JSONWriter.valueToString(value); + } + + /** + * Wrap an object, if necessary. If the object is null, return the NULL + * object. If it is an array or collection, wrap it in a JSONArray. If it is + * a map, wrap it in a JSONObject. If it is a standard property (Double, + * String, et al) then it is already wrapped. Otherwise, if it comes from + * one of the java packages, turn it into a string. And if it doesn't, try + * to wrap it in a JSONObject. If the wrapping fails, then null is returned. + * + * @param object The object to wrap + * @return The wrapped value + */ + public static Object wrap(Object object) { + try { + if (object == null) { + return NULL; + } + if (object instanceof JSONOrderedObject || object instanceof JSONArray + || NULL.equals(object) || object instanceof JSONString + || object instanceof Byte || object instanceof Character + || object instanceof Short || object instanceof Integer + || object instanceof Long || object instanceof Boolean + || object instanceof Float || object instanceof Double + || object instanceof String || object instanceof BigInteger + || object instanceof BigDecimal || object instanceof Enum) { + return object; + } + + if (object instanceof Collection) { + Collection coll = (Collection) object; + return new JSONArray(coll); + } + if (object.getClass().isArray()) { + return new JSONArray(object); + } + if (object instanceof Map) { + Map map = (Map) object; + return new JSONOrderedObject(map); + } + Package objectPackage = object.getClass().getPackage(); + String objectPackageName = objectPackage != null ? objectPackage + .getName() : ""; + if (objectPackageName.startsWith("java.") + || objectPackageName.startsWith("javax.") + || object.getClass().getClassLoader() == null) { + return object.toString(); + } + return new JSONOrderedObject(object); + } catch (Exception exception) { + return null; + } + } + + /** + * Write the contents of the JSONObject as JSON text to a writer. For + * compactness, no whitespace is added. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * + * @return The writer. + * @throws JSONException + */ + public Writer write(Writer writer) throws JSONException { + return this.write(writer, 0, 0); + } + + static final Writer writeValue(Writer writer, Object value, + int indentFactor, int indent) throws JSONException, IOException { + if (value == null || value.equals(null)) { + writer.write("null"); + } else if (value instanceof JSONString) { + Object o; + try { + o = ((JSONString) value).toJSONString(); + } catch (Exception e) { + throw new JSONException(e); + } + writer.write(o != null ? o.toString() : quote(value.toString())); + } else if (value instanceof Number) { + // not all Numbers may match actual JSON Numbers. i.e. fractions or Imaginary + final String numberAsString = numberToString((Number) value); + try { + // Use the BigDecimal constructor for its parser to validate the format. + @SuppressWarnings("unused") + BigDecimal testNum = new BigDecimal(numberAsString); + // Close enough to a JSON number that we will use it unquoted + writer.write(numberAsString); + } catch (NumberFormatException ex) { + // The Number value is not a valid JSON number. + // Instead we will quote it as a string + quote(numberAsString, writer); + } + } else if (value instanceof Boolean) { + writer.write(value.toString()); + } else if (value instanceof Enum) { + writer.write(quote(((Enum) value).name())); + } else if (value instanceof JSONOrderedObject) { + ((JSONOrderedObject) value).write(writer, indentFactor, indent); + } else if (value instanceof JSONArray) { + ((JSONArray) value).write(writer, indentFactor, indent); + } else if (value instanceof Map) { + Map map = (Map) value; + new JSONOrderedObject(map).write(writer, indentFactor, indent); + } else if (value instanceof Collection) { + Collection coll = (Collection) value; + new JSONArray(coll).write(writer, indentFactor, indent); + } else if (value.getClass().isArray()) { + new JSONArray(value).write(writer, indentFactor, indent); + } else { + quote(value.toString(), writer); + } + return writer; + } + + static final void indent(Writer writer, int indent) throws IOException { + for (int i = 0; i < indent; i += 1) { + writer.write(' '); + } + } + + /** + * Write the contents of the JSONObject as JSON text to a writer. + * + *

If indentFactor > 0 and the {@link JSONOrderedObject} + * has only one key, then the object will be output on a single line: + *

{@code {"key": 1}}
+ * + *

If an object has 2 or more keys, then it will be output across + * multiple lines:

{
+     *  "key1": 1,
+     *  "key2": "value 2",
+     *  "key3": 3
+     * }
+ *

+ * Warning: This method assumes that the data structure is acyclical. + * + * + * @param writer Writes the serialized JSON + * @param indentFactor The number of spaces to add to each level of indentation. + * @param indent The indentation of the top level. + * @return The writer. + * @throws JSONException + */ + public Writer write(Writer writer, int indentFactor, int indent) + throws JSONException { + try { + boolean commanate = false; + final int length = this.length(); + writer.write('{'); + + if (length == 1) { + final Entry entry = this.entrySet().iterator().next(); + final String key = entry.getKey(); + writer.write(quote(key)); + writer.write(':'); + if (indentFactor > 0) { + writer.write(' '); + } + try { + writeValue(writer, entry.getValue(), indentFactor, indent); + } catch (Exception e) { + throw new JSONException("Unable to write JSONObject value for key: " + key, e); + } + } else if (length != 0) { + final int newindent = indent + indentFactor; + for (final Entry entry : this.entrySet()) { + if (commanate) { + writer.write(','); + } + if (indentFactor > 0) { + writer.write('\n'); + } + indent(writer, newindent); + final String key = entry.getKey(); + writer.write(quote(key)); + writer.write(':'); + if (indentFactor > 0) { + writer.write(' '); + } + try { + writeValue(writer, entry.getValue(), indentFactor, newindent); + } catch (Exception e) { + throw new JSONException("Unable to write JSONObject value for key: " + key, e); + } + commanate = true; + } + if (indentFactor > 0) { + writer.write('\n'); + } + indent(writer, indent); + } + writer.write('}'); + return writer; + } catch (IOException exception) { + throw new JSONException(exception); + } + } + + /** + * Returns a java.util.Map containing all of the entries in this object. + * If an entry in the object is a JSONArray or JSONObject it will also + * be converted. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @return a java.util.Map containing the entries of this object + */ + public Map toMap() { + Map results = new HashMap(); + for (Entry entry : this.entrySet()) { + Object value; + if (entry.getValue() == null || NULL.equals(entry.getValue())) { + value = null; + } else if (entry.getValue() instanceof JSONOrderedObject) { + value = ((JSONOrderedObject) entry.getValue()).toMap(); + } else if (entry.getValue() instanceof JSONArray) { + value = ((JSONArray) entry.getValue()).toList(); + } else { + value = entry.getValue(); + } + results.put(entry.getKey(), value); + } + return results; + } +} diff --git a/src/main/java/io/exnihilo/validator/entity/ValidationResponseEntity.java b/src/main/java/io/exnihilo/validator/entity/ValidationEntity.java similarity index 82% rename from src/main/java/io/exnihilo/validator/entity/ValidationResponseEntity.java rename to src/main/java/io/exnihilo/validator/entity/ValidationEntity.java index aa4fee9..b89fb94 100644 --- a/src/main/java/io/exnihilo/validator/entity/ValidationResponseEntity.java +++ b/src/main/java/io/exnihilo/validator/entity/ValidationEntity.java @@ -5,7 +5,7 @@ import org.springframework.context.annotation.ScopedProxyMode; import org.springframework.stereotype.Component; /** - * Validation Response Entity has the response details for all configured validator methods. + * Validation Response Entity has the response details for all configured editor methods. * * @author Anand Varkey Philips * @date 27/10/2018 @@ -14,8 +14,10 @@ @Component @Data @Scope(value = "prototype", proxyMode = ScopedProxyMode.INTERFACES) -public class ValidationResponseEntity { +public class ValidationEntity { + private boolean isValid; private int lineNumber; private int columnNumber; private String validationMessage; + private String inputMessage; } diff --git a/src/main/java/io/exnihilo/validator/service/ValidatorService.java b/src/main/java/io/exnihilo/validator/service/ValidatorService.java index 2618bfd..2400dc3 100644 --- a/src/main/java/io/exnihilo/validator/service/ValidatorService.java +++ b/src/main/java/io/exnihilo/validator/service/ValidatorService.java @@ -1,7 +1,9 @@ package io.exnihilo.validator.service; -import io.exnihilo.validator.entity.ValidationResponseEntity; +import io.exnihilo.validator.entity.JSONOrderedObject; +import io.exnihilo.validator.entity.ValidationEntity; import lombok.extern.slf4j.Slf4j; +import org.json.JSONException; import org.springframework.beans.factory.annotation.Lookup; import org.springframework.stereotype.Service; import org.yaml.snakeyaml.Yaml; @@ -22,11 +24,10 @@ public class ValidatorService { @Lookup - public ValidationResponseEntity getPrototypeBean() { + public ValidationEntity getPrototypeBean() { return null; } - /** * Splitting of yaml data in case of "---", then validating each part separately, * and then returning success data or line and column numbers in case of failure. @@ -34,38 +35,98 @@ public ValidationResponseEntity getPrototypeBean() { * @param yamlData * @return validation result */ - public ValidationResponseEntity validateYAMLService(String yamlData) { - ValidationResponseEntity validationResponseEntity = getPrototypeBean(); - String validationMessage = "Valid YAML!!!"; + public ValidationEntity validateYamlService(String yamlData) { + ValidationEntity validationEntity = getPrototypeBean(); + validationEntity.setInputMessage(yamlData); try { Yaml yaml = new Yaml(); Map obj = yaml.load(yamlData.replace("---", "")); - log.debug("Value obtained successfully: {}", obj.toString()); - validationResponseEntity.setValidationMessage(validationMessage); + log.debug("YAML Value obtained successfully: {}", obj.toString()); + validationEntity.setValidationMessage("Valid YAML!!!"); } catch (Exception e) { - validationMessage = e.getMessage(); - validationResponseEntity.setValidationMessage(validationMessage); + validationEntity.setValidationMessage(e.getMessage()); log.error("Exception occurred in validation: ", e); - if (validationMessage.contains("line ")) { + if (e.getMessage().contains("line ")) { String pattern1 = "line "; String pattern2 = ","; Pattern p = Pattern.compile(Pattern.quote(pattern1) + "(.*?)" + Pattern.quote(pattern2)); - Matcher m = p.matcher(validationMessage); + Matcher m = p.matcher(e.getMessage()); + while (m.find()) { + validationEntity.setLineNumber(Integer.parseInt(m.group(1))); + } + pattern1 = "column "; + pattern2 = ":"; + p = Pattern.compile(Pattern.quote(pattern1) + "(.*?)" + Pattern.quote(pattern2)); + m = p.matcher(e.getMessage()); while (m.find()) { - validationResponseEntity.setLineNumber(Integer.parseInt(m.group(1))); + validationEntity.setColumnNumber(Integer.parseInt(m.group(1))); } } - if (validationMessage.contains("line ")) { - String pattern1 = "column "; - String pattern2 = ":"; + } finally { + return validationEntity; + } + } + + public ValidationEntity validateJsonService(String json) { + ValidationEntity validationEntity = getPrototypeBean(); + validationEntity.setInputMessage(json); + try { + String indentedJson = (new JSONOrderedObject(json)).toString(4); + log.debug("JSON Value obtained successfully: {}", indentedJson); + validationEntity.setValidationMessage("Valid JSON!!!"); + } catch (JSONException e) { + validationEntity.setValidationMessage(e.getMessage()); + log.error("Exception occurred in validation: ", e); + if (e.getMessage().contains("line ")) { + String pattern1 = "line "; + String pattern2 = "]"; Pattern p = Pattern.compile(Pattern.quote(pattern1) + "(.*?)" + Pattern.quote(pattern2)); - Matcher m = p.matcher(validationMessage); + Matcher m = p.matcher(e.getMessage()); + while (m.find()) { + validationEntity.setLineNumber(Integer.parseInt(m.group(1))); + } + pattern1 = "[character "; + pattern2 = " line"; + p = Pattern.compile(Pattern.quote(pattern1) + "(.*?)" + Pattern.quote(pattern2)); + m = p.matcher(e.getMessage()); + while (m.find()) { + validationEntity.setColumnNumber(Integer.parseInt(m.group(1))); + } + } + } finally { + return validationEntity; + } + } + + public ValidationEntity formatJsonService(String json) { + ValidationEntity validationEntity = getPrototypeBean(); + validationEntity.setInputMessage(json); + try { + String indentedJson = (new JSONOrderedObject(json)).toString(4); + log.debug("JSON Value obtained successfully: {}", indentedJson); + validationEntity.setInputMessage(indentedJson); + validationEntity.setValidationMessage("Valid JSON!!!"); + } catch (JSONException e) { + validationEntity.setValidationMessage(e.getMessage()); + log.error("Exception occurred in validation: ", e); + if (e.getMessage().contains("line ")) { + String pattern1 = "line "; + String pattern2 = "]"; + Pattern p = Pattern.compile(Pattern.quote(pattern1) + "(.*?)" + Pattern.quote(pattern2)); + Matcher m = p.matcher(e.getMessage()); + while (m.find()) { + validationEntity.setLineNumber(Integer.parseInt(m.group(1))); + } + pattern1 = "[character "; + pattern2 = " line"; + p = Pattern.compile(Pattern.quote(pattern1) + "(.*?)" + Pattern.quote(pattern2)); + m = p.matcher(e.getMessage()); while (m.find()) { - validationResponseEntity.setColumnNumber(Integer.parseInt(m.group(1))); + validationEntity.setColumnNumber(Integer.parseInt(m.group(1))); } } } finally { - return validationResponseEntity; + return validationEntity; } } } \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 6cb862d..4783e60 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,8 +1,9 @@ server: - port: 8090 + port: 9090 servlet.context-path: /validator logging: level.root: info - file: /packages/logs/yaml-validator/yaml-validator.log + level.io.exnihilo: debug + file: /packages/logs/yaml-editor/yaml-editor.log spring.pid.fail-on-write-error: true -spring.pid.file: /packages/config/yaml-validator/yaml-validator.pid \ No newline at end of file +spring.pid.file: /packages/config/yaml-editor/yaml-editor.pid \ No newline at end of file diff --git a/src/main/resources/static/css/forEditor.css b/src/main/resources/static/css/editor.css similarity index 79% rename from src/main/resources/static/css/forEditor.css rename to src/main/resources/static/css/editor.css index c22bfee..9638f77 100644 --- a/src/main/resources/static/css/forEditor.css +++ b/src/main/resources/static/css/editor.css @@ -10,5 +10,5 @@ body { right: 0px; } .failedGutter{ - background-color: #d60e0e; + background-color: #d60e0e !important; } \ No newline at end of file diff --git a/src/main/resources/static/js/editor.js b/src/main/resources/static/js/editor.js new file mode 100644 index 0000000..5a5cd4d --- /dev/null +++ b/src/main/resources/static/js/editor.js @@ -0,0 +1,98 @@ +var initialData = `server: + port: 9090 + servlet.context-path: /validator +logging: + level.root: info + file: /packages/logs/yaml-validator/yaml-validator.log +spring.pid.fail-on-write-error: true +spring.pid.file: /packages/config/yaml-validator/yaml-validator.pid`; +var errorLineNumber = 0; +var editor, yamlMode; + +$(document).ready(function() { + $('#validationResult').hide(); + editor = ace.edit("editor"); + editor.setTheme("ace/theme/idle_fingers"); + yamlMode = ace.require("ace/mode/yaml").Mode; + editor.session.setMode(new yamlMode()); + editor.setValue(initialData, 0); + editor.clearSelection(); + editor.focus(); + + $("#encodeData").click(function() { + editor.setValue(btoa(editor.getValue())); + editor.clearSelection(); + }); + $("#decodeData").click(function() { + editor.setValue(atob(editor.getValue())); + editor.clearSelection(); + }); + $("#validateYamlData").click(function() { + ajaxCall("yaml","Valid YAML!!!"); + }); + $("#validateJsonData").click(function() { + ajaxCall("json","Valid JSON!!!"); + }); + $("#formatJsonData").click(function() { + ajaxCall("formatJson","Valid JSON!!!"); + }); + $("#shareByEmail").click(function() { + if (editor.getValue().length > 2000) { + alert("The maximum Data Size for sharing by email is 2000 chars for the time being!") + return false; + } + var link = "mailto:" + + "?cc=anandvarkeyphilips@gmail.com" + + "&subject=" + escape("Data used for validation") + + "&body=" + escape(editor.getValue()); + window.location.href = link; + }); +}); + +function ajaxCall(url,validationMessage) { + $.ajax({ + url: url, + type: "POST", + data: JSON.stringify({ + validationMessage: editor.getValue() + }), + contentType: "application/json", + success: function(data) { + editor.setValue(data.inputMessage); + console.log("Success Operation validateData"); + console.log(data); + if (data.validationMessage == validationMessage) { + var newEditSession = editor.getSession(); + newEditSession.removeGutterDecoration((errorLineNumber - 1), "failedGutter"); + editor.setSession(newEditSession); + editor.clearSelection(); + editor.focus(); + $('#validationResult').show(); + $('#validationResult').text(data.validationMessage); + $('#validationResult').css({ + backgroundColor: 'green' + }); + errorLineNumber = 0; + } else { + $('#validationResult').show(); + $('#validationResult').text(data.validationMessage); + $('#validationResult').css({ + backgroundColor: 'red' + }); + if (data.lineNumber > 0) { + errorLineNumber = data.lineNumber; + var newEditSession = editor.getSession(); + newEditSession.addGutterDecoration((errorLineNumber - 1), "failedGutter"); + editor.setSession(newEditSession); + editor.clearSelection(); + editor.focus(); + editor.gotoLine(data.lineNumber, data.columnNumber, true); + } + } + }, + error: function(data) { + console.log("Failed Operation validateData"); + console.log(data); + } + }); +} \ No newline at end of file diff --git a/src/main/resources/static/js/forEditor.js b/src/main/resources/static/js/forEditor.js deleted file mode 100644 index 6b40a39..0000000 --- a/src/main/resources/static/js/forEditor.js +++ /dev/null @@ -1,85 +0,0 @@ -var initialData = `server: - port: 8090 - servlet.context-path: /validator -logging: - level.root: info - file: /packages/logs/yaml-validator/yaml-validator.log -spring.pid.fail-on-write-error: true -spring.pid.file: /packages/config/yaml-validator/yaml-validator.pid`; - -$(document).ready(function() { - var errorLineNumber = 0; - $('#validationResult').hide(); - var editor = ace.edit("editor"); - editor.setTheme("ace/theme/idle_fingers"); - var yamlMode = ace.require("ace/mode/yaml").Mode; - editor.session.setMode(new yamlMode()); - editor.setValue(initialData, 0); - editor.clearSelection; - editor.focus(); - - $("#encodeData").click(function() { - editor.setValue(btoa(editor.getValue())); - }); - $("#decodeData").click(function() { - editor.setValue(atob(editor.getValue())); - }); - $("#validateData").click(function() { - $.ajax({ - url: 'yaml', - type: "POST", - data: JSON.stringify({ - yamlData: editor.getValue() - }), - contentType: "application/json", - xhrFields: { - withCredentials: false - }, - success: function(data) { - console.log("Success Operation validateData"); - console.log(data); - if (data.validationMessage == "Valid YAML!!!") { - var newEditSession = editor.getSession(); - newEditSession.removeGutterDecoration((errorLineNumber - 1), "failedGutter"); - editor.setSession(newEditSession); - editor.focus(); - $('#validationResult').show(); - $('#validationResult').text('Valid YAML!!!'); - $('#validationResult').css({ - backgroundColor: 'green' - }); - errorLineNumber = 0; - } else { - $('#validationResult').show(); - $('#validationResult').text(data.validationMessage); - $('#validationResult').css({ - backgroundColor: 'red' - }); - if (data.lineNumber > 0) { - errorLineNumber = data.lineNumber; - var newEditSession = editor.getSession(); - newEditSession.addGutterDecoration((errorLineNumber - 1), "failedGutter"); - editor.setSession(newEditSession); - editor.focus(); - editor.gotoLine(data.lineNumber, data.columnNumber, true); - } - } - }, - error: function(data) { - console.log("Failed Operation validateData"); - console.log(data); - } - }); - }); - $("#shareByEmail").click(function() { - if (editor.getValue().length > 2000) { - alert("The maximum Data Size for sharing by email is 2000 chars for the time being!") - return false; - } - var link = "mailto:" + - "?cc=anandvarkeyphilips@gmail.com" + - "&subject=" + escape("YAML Data used for validation") + - "&body=" + escape(editor.getValue()); - window.location.href = link; - }); -}); \ No newline at end of file diff --git a/src/main/resources/templates/yaml-editor.ftl b/src/main/resources/templates/editor.ftl similarity index 86% rename from src/main/resources/templates/yaml-editor.ftl rename to src/main/resources/templates/editor.ftl index 842c772..7439a02 100644 --- a/src/main/resources/templates/yaml-editor.ftl +++ b/src/main/resources/templates/editor.ftl @@ -8,10 +8,10 @@ - + - + @@ -33,7 +33,9 @@

- + + +