All Downloads are FREE. Search and download functionalities are using the official Maven repository.

core.apiCore.helpers.JsonHelper Maven / Gradle / Ivy

package core.apiCore.helpers;

import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.XML;
import org.skyscreamer.jsonassert.JSONCompare;
import org.skyscreamer.jsonassert.JSONCompareMode;
import org.skyscreamer.jsonassert.JSONCompareResult;

import com.fasterxml.jackson.core.JsonParser.Feature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.Gson;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.Option;
import com.jayway.jsonpath.ReadContext;

import core.helpers.Helper;
import core.support.configReader.Config;
import core.support.logger.TestLog;
import core.support.objects.KeyValue;
import core.support.objects.ServiceObject;
import io.restassured.response.Response;

public class JsonHelper {

	public static String failOnEscapeChars = "service.validation.fail.on.escapechars";

	/**
	 * replaces output parameter with response values eg. $token with id form values
	 * are in form of list separated by ";"
	 * 
	 * if Command is used 
	 * @param response
	 * @param outputParam
	 */
	public static void saveOutboundJsonParameters(Response response, String outputParam) {
		if (response == null || outputParam.isEmpty())
			return;
		
		// replace parameters for outputParam
		outputParam = DataHelper.replaceParameters(outputParam);

		List keywords = DataHelper.getValidationMap(outputParam);
		for (KeyValue keyword : keywords) {
			if(keyword.key.toString().trim().startsWith(DataHelper.JSON_COMMAND.command.name())) {
				String value = validateResponseBody(keyword.key.toString(),response);
				keyword.key = value;
				configMapKeyValues(response, keyword);
			}else
				configMapJsonKeyValues(response, keyword);
		}
		
	}

	/**
	 * map key value to config eg.features.features.id:1:<$id>
	 * 
	 * @param response
	 * @param keyValue
	 */
	public static void configMapJsonKeyValues(Response response, KeyValue keyword) {
		
		String value = getJsonValue(response, keyword.key);
		keyword.key = value;
		configMapKeyValues(response, keyword);
	}
	
	/**
	 * map key value to config eg.features.features.id:1:<$id>
	 * 
	 * @param response
	 * @param keyValue
	 */
	public static void configMapKeyValues(Response response, KeyValue keyword) {

		// fail if value is wrong format
		if (!keyword.value.toString().startsWith("<") || !keyword.value.toString().contains("$")
				|| !keyword.value.toString().endsWith(">"))
			Helper.assertFalse("variable placement must of format path: <$variable>. invalid value: "
					+ keyword.value.toString());

		String key = (String) keyword.value;
		key = key.replace("$", "").replace("<", "").replace(">", "").trim();
		// gets json value. if list, returns string separated by comma
		String value = keyword.key;

		if (!keyword.position.isEmpty()) {
			value = value.split(",")[Integer.valueOf(keyword.position) - 1];
		}
		Config.putValue(key, value, false);
		TestLog.logPass("output parameter: " + key + " value: " + value);
		
	}

	/**
	 * gets json value as list if applicable, or string if single item converts to
	 * string separated by ","
	 * 
	 * @param response
	 * @param path
	 * @return
	 */
	public static String getJsonValue(Response response, String path) {
		String jsonResponse = response.getBody().asString();
		String value = getJsonValue(jsonResponse, path);
		return value;
	}

	/**
	 * gets json value as list if applicable, or string if single item converts to
	 * string separated by "," https://github.com/json-path/JsonPath
	 * 
	 * @param path https://github.com/json-path/JsonPath
	 * 
	 *             for testing json path values: http://jsonpath.herokuapp.com/
	 * @return value string list separated by ","
	 */
	public static String getJsonValue(String json, String path) {
		return getJsonValue(json, path, true);
	}

	/**
	 * gets json value as list if applicable, or string if single item converts to
	 * string separated by "," https://github.com/json-path/JsonPath
	 * 
	 * @param path https://github.com/json-path/JsonPath
	 * 
	 *             for testing json path values: http://jsonpath.herokuapp.com/
	 * @return value string list separated by ","
	 */
	public static String getJsonValue(String json, String path, boolean isAlwaysReturnList) {
		Object values = getJsonPathValue(json, path, isAlwaysReturnList);

		if(values == null)
			return null;
		
		// return json response without normalizing
		if (isValidJsonObject(values)) {
			return values.toString();
		}

		return DataHelper.objectToString(values);
	}
	
	/**
	 * gets json value as list if applicable, or string if single item converts to
	 * string separated by "," https://github.com/json-path/JsonPath
	 *
	 * @param path https://github.com/json-path/JsonPath
	 *
	 *             for testing json path values: http://jsonpath.herokuapp.com/
	 * @return value string list separated by ","
	 */
	public static Object getJsonPathValue(String json, String path, boolean isAlwaysReturnList) {
		return getJsonPathValue(json, path, isAlwaysReturnList, true);
	}

	/**
	 * gets json value as list if applicable, or string if single item converts to
	 * string separated by "," https://github.com/json-path/JsonPath
	 * 
	 * @param path https://github.com/json-path/JsonPath
	 * 
	 *             for testing json path values: http://jsonpath.herokuapp.com/
	 * @return value string list separated by ","
	 */
	public static Object getJsonPathValue(String json, String path, boolean isAlwaysReturnList, boolean checkError) {
		String prefix = "$.";
		Object values = null;

		// validate escape characters in json
		isJSONValid(json, true);
		if (Config.getBooleanValue(failOnEscapeChars) && containsEscapeChar(json))
			Helper.assertFalse("invalid escape character in json. invalid chars are: \\\", \\b, "
					+ "\\n, \\r, \\f, \\', \\\\: " + json);

		// in case user forgets to remove prefix
		if (path.startsWith(prefix))
			path = path.replace(prefix, "");

		// set always return list. on json path method errors, need to be turned off.
		// eg. length()
		Configuration config = null;
		if (isAlwaysReturnList)
			config = Configuration.defaultConfiguration().addOptions(Option.ALWAYS_RETURN_LIST);
		else
			config = Configuration.defaultConfiguration();

		ReadContext ctx = JsonPath.using(config).parse(json);
		
		if(path.equals("."))
			return json;

		try {
			values = ctx.read(prefix + path);
		} catch (Exception e) {
			// in case always return list is not applicable to json and we need to turn it
			// off and rerun
			if (e.getMessage().contains(Option.ALWAYS_RETURN_LIST.name()) && isAlwaysReturnList)
				values = getJsonValue(json, path, false);
			else
				if(checkError)
					TestLog.logWarning("invalid path: '" + path + "' for json string: " + json
						+ "\n. see http://jsonpath.herokuapp.com to validate your path against json string. see https://github.com/json-path/JsonPath for more info. \n"
						+ e.getMessage());
		}

		if (values == null)
			if(checkError)
				TestLog.logWarning("no results returned: '" + path
					+ "'. see http://jsonpath.herokuapp.com to validate your path against json string. see https://github.com/json-path/JsonPath for more info.");

		return values;
	}

	public static boolean isJsonPathValueString(String json, String path) {
		String prefix = "$.";
		Object jsonResponse = null;
		Object value = "";
		// in case user forgets to remove prefix
		if (path.startsWith(prefix))
			path = path.replace(prefix, "");

		Configuration config = Configuration.defaultConfiguration().addOptions(Option.ALWAYS_RETURN_LIST);
		ReadContext ctx = JsonPath.using(config).parse(json);

		try {
			jsonResponse = ctx.read(prefix + path);
		} catch (Exception e) {
			e.getCause();
		}
		
		if(jsonResponse == null)
			return false;

		if (jsonResponse instanceof List) {
			net.minidev.json.JSONArray array = (net.minidev.json.JSONArray) jsonResponse;
			if (!array.isEmpty())
				value = array.get(0);
		}

		if (value instanceof String)
			return true;
		return false;

	}

	/**
	 * \t Insert a tab in the text at this point. \b Insert a backspace in the text
	 * at this point. \n Insert a newline in the text at this point. \r Insert a
	 * carriage return in the text at this point. \f Insert a formfeed in the text
	 * at this point. \' Insert a single quote character in the text at this point.
	 * \" Insert a double quote character in the text at this point. \\ Insert a
	 * backslash character in the text at this point.
	 * 
	 * @param value
	 * @return
	 */
	public static boolean containsEscapeChar(String value) {
		String[] escapeChars = { "\\\"", "\\b", "\\n", "\\r", "\\f", "\\'", "\\\\" };
		for (String escape : escapeChars) {
			if (value.contains(escape))
				return true;
		}
		return false;
	}

	/**
	 * get json path value from xml string
	 * 
	 * @param xml
	 * @param path
	 * @return
	 */
	public static String getJsonValueFromXml(String xml, String path) {

		// convert xml stirng to json string
		String json = XMLToJson(xml);

		return getJsonValue(json, path);
	}

	/**
	 * convert xml string to json string
	 * 
	 * @param xml string
	 * @return json string
	 */
	public static String XMLToJson(String xml) {
		int printIndentFactor = 4;
		String jsonString = StringUtils.EMPTY;
		try {
			JSONObject xmlJSONObj = XML.toJSONObject(xml);
			jsonString = xmlJSONObj.toString(printIndentFactor);
		} catch (JSONException je) {
			je.toString();
		}
		return jsonString;
	}

	public static String getResponseValue(Response response) {
		return response.getBody().asString();
	}

	/**
	 * validates the json maps against the keyword requirements examples:
	 * "person.roles.name": hasItems("admin"), "person.lastName":
	 * equalTo("Administrator"), "person.lastName": isNotEmpty, "person.roles.name":
	 * contains("admin"), "person.roles.name": containsInAnyOrder(admin),
	 * "person.roles": nodeSizeGreaterThan(0), "person.sites.": nodeSizeExact(0)
	 * "person.roles.name": sequence("admin"),
	 * 
	 * 
	 * @param jsonMap
	 * @param response
	 */
	public static List validateJsonKeywords(List keywords, String responseString) {
		List errorMessages = new ArrayList();
		for (KeyValue keyword : keywords) {
			String jsonPath = Helper.removeSurroundingQuotes(keyword.key);
			String expectedValue = Helper.stringRemoveLines((String) keyword.value);
			String command = "";

			String[] expected = expectedValue.split("[\\(\\)]");
			// get value in between parenthesis
			if (expected.length > 1) {
				command = expected[0];
				expectedValue = expected[1];
			} else if (expected.length == 1) {
				command = expectedValue;
				expectedValue = "";
			}

			TestLog.ConsoleLog("command: <" + command + "> json path: <" + jsonPath + ">");
			// get response string from json path (eg. data.user.id) would return "2"
			String jsonPathResponse = getJsonValue(responseString, jsonPath);
			
			// if response is empty and isEmpty command is not used
			// empty response for custom commands are handle by developer
			if(StringUtils.isBlank(jsonPathResponse) && !keyword.value.toString().startsWith(DataHelper.JSON_COMMAND.command.name())) {
				if(!keyword.value.equals(DataHelper.JSON_COMMAND.isEmpty.name()))
					errorMessages.add("response returned, however, no jsonpath response returned for path: " + jsonPath);
				continue;
			}

			// validate response
			String errorMessage = DataHelper.validateCommand(command, jsonPathResponse, expectedValue, keyword.position, false);
			errorMessages.add(errorMessage);
		}

		// remove all empty response strings
		errorMessages.removeAll(Collections.singleton(""));
		return errorMessages;
	}
	
	/**
	 * validates json string
	 * 
	 * @param value
	 * @return
	 */
	public static boolean isJSONValid(String value, boolean isFailOnError) {
		String error = StringUtils.EMPTY;

		// if contains keyword indicators, then return false
		String expectedJson = Helper.stringNormalize(value);
		if (expectedJson.startsWith(DataHelper.VERIFY_JSON_PART_INDICATOR)
				|| expectedJson.startsWith(DataHelper.VERIFY_JSON_PART_INDICATOR_UNDERSCORE)
				|| expectedJson.startsWith(DataHelper.VERIFY_RESPONSE_NO_EMPTY)
				|| expectedJson.startsWith(DataHelper.VERIFY_RESPONSE_BODY_INDICATOR)) {
			return false;
		}

		try {
			new JSONObject(value);
		} catch (JSONException ex) {
			try {
				error = ex.getMessage();
				new JSONArray(value);
			} catch (JSONException ex1) {
				if (error.isEmpty())
					error = ex1.getMessage();
				if (isFailOnError)
					Helper.assertFalse("Invalid Json. Error: " + error + "\n json: " + value);
				return false;
			}
		}
		return true;
	}
	
	/**
	 * validates expected json string against json body from response
	 * 
	 * @param expectedJson
	 * @param actualJson
	 * @return
	 */
	public static String validateByJsonBody(String expectedJson, String response) {
		return validateByJsonBody(expectedJson, response, false);
	}

	/**
	 * validates expected json string against json body from response
	 * 
	 * @param expectedJson
	 * @param actualJson
	 * @return
	 */
	public static String validateByJsonBody(String expectedJson, String response, boolean hasToBeJsonBody) {
		expectedJson = Helper.stringRemoveLines(expectedJson);
		if (JsonHelper.isJSONValid(expectedJson, false)) {
			if (StringUtils.isBlank(response))
				return "response from json path is empty";

			TestLog.logPass("expecting partial json: " + Helper.stringRemoveLines(expectedJson));
			try {
				JSONCompareResult result = JSONCompare.compareJSON(expectedJson, response, JSONCompareMode.LENIENT);
				if (result.failed()) {
					return result.getMessage() + "\n"
							+ "see http://jsonpath.herokuapp.com to validate your path against json string. see https://github.com/json-path/JsonPath for more info. \n\n expectedJson: "
							+ expectedJson + "\n\n response: " + response + "\n\n";
				}
				// JSONAssert.assertEquals(expectedJson, response, JSONCompareMode.LENIENT);
			} catch (JSONException e) {
				e.printStackTrace();
				return e.getMessage() + "\n"
						+ "see http://jsonpath.herokuapp.com to validate your path against json string. see https://github.com/json-path/JsonPath for more info. \n\n expectedJson: "
						+ expectedJson + "\n\n response: " + response + "\n\n";

			}
		}else if(hasToBeJsonBody) {
			return "expected String is not valid json: " + expectedJson;
		}
			
		return StringUtils.EMPTY;
	}

	/**
	 * validates json response against keywords
	 * 
	 * @param expectedJson
	 * @param response
	 */
	public static List validateByKeywords(String expectedJson, Response response) {
		String responseString = JsonHelper.getResponseValue(response);
		return validateByKeywords(expectedJson, responseString);
	}

	/**
	 * validates json response against keywords
	 * 
	 * @param expectedJson
	 * @param response
	 */
	public static List validateByKeywords(String expectedJson, String responseString) {
		List errorMessages = new ArrayList();

		expectedJson = Helper.stringRemoveLines(expectedJson);
		expectedJson = Helper.removeSurroundingQuotes(expectedJson);

		if (!JsonHelper.isJSONValid(expectedJson, false)) {
			if (expectedJson.startsWith(DataHelper.VERIFY_JSON_PART_INDICATOR)
					|| expectedJson.startsWith(DataHelper.VERIFY_JSON_PART_INDICATOR_UNDERSCORE)) {
				// get hashmap of json path And verification
				List keywords = DataHelper.getValidationMap(expectedJson);
				// validate based on keywords
				errorMessages = JsonHelper.validateJsonKeywords(keywords, responseString);

				// response is not empty
			} else if (expectedJson.startsWith("_NOT_EMPTY_")) {
				if (responseString.isEmpty())
					errorMessages.add("response is empty");
			}
		}
		return errorMessages;
	}

	/**
	 * validates response body this is validating the response body as text
	 * 
	 * @param expected
	 * @param response
	 * @return
	 */
	public static String validateResponseBody(String expected, Response response) {
		String responseString = JsonHelper.getResponseValue(response);
		return validateResponseBody(expected, responseString);

	}

	/**
	 * validates response body this is validating the response body as text
	 * 
	 * @param expected
	 * @param response
	 * @return
	 */
	public static String validateResponseBody(String expected, String responseString) {
		expected = Helper.stringRemoveLines(expected);
		expected = Helper.removeSurroundingQuotes(expected);

		// command added as part of output parameter command support
		if (!expected.startsWith(DataHelper.VERIFY_RESPONSE_BODY_INDICATOR)
				&& !expected.startsWith(DataHelper.VERIFY_HEADER_PART_INDICATOR)
				&& !expected.startsWith(DataHelper.JSON_COMMAND.command.name())
				&& !expected.startsWith(DataHelper.VERIFY_TOPIC_PART_INDICATOR)) {
			return StringUtils.EMPTY;
		}

		// remove the indicator _VERIFY_RESPONSE_BODY_
		expected = removeResponseIndicator(expected);

		String[] expectedArr = expected.split("[\\(\\)]");
		String expectedValue = StringUtils.EMPTY;
		String command = StringUtils.EMPTY;

		// if the expected does not contain parameters. eg. isEmpty, isNotEmpty
		if (expectedArr.length == 1)
			command = expected.trim();
		else {
			// get value in between parenthesis
			command = expectedArr[0].trim();
			expectedValue = expectedArr[1].trim();
		}

		return DataHelper.validateCommand(command, responseString, expectedValue, "0", false);

	}

	/**
	 * remove response indicators
	 * 
	 * @param expected
	 * @return
	 */
	public static String removeResponseIndicator(String expected) {
		List indicator = new ArrayList();
		indicator.add(DataHelper.VERIFY_RESPONSE_BODY_INDICATOR);
		indicator.add(DataHelper.VERIFY_JSON_PART_INDICATOR);
		indicator.add(DataHelper.VERIFY_JSON_PART_INDICATOR_UNDERSCORE);
		indicator.add(DataHelper.VERIFY_HEADER_PART_INDICATOR);
		indicator.add(DataHelper.VERIFY_TOPIC_PART_INDICATOR);
		indicator.add(DataHelper.REQUEST_BODY_UPDATE_INDICATOR);

		for (String value : indicator) {
			expected = expected.replace(value, "");
		}

		return expected.trim();
	}

	/**
	 * if request body is empty, return json template string if request body
	 * contains @ variable tag, replace tag with value format for request body: json
	 * path:position:value or json path:vlaue eg.
	 * "features.feature.name:1:value_<@_TIME_19>"
	 * 
	 * @param serviceObject
	 * @return
	 */
	public static String getRequestBodyFromJsonTemplate(ServiceObject serviceObject) {

		// return empty string if not json template
		if (!isJsonFile(serviceObject.getTemplateFile()))
			return StringUtils.EMPTY;

		Path templatePath = DataHelper.getTemplateFilePath(serviceObject.getTemplateFile());
		String jsonFileValue = DataHelper.convertFileToString(templatePath);
		jsonFileValue = DataHelper.replaceParameters(jsonFileValue);

		if (serviceObject.getRequestBody().isEmpty()) {
			return jsonFileValue;
		} else {
			return updateJsonFromRequestBody(serviceObject.getRequestBody(), jsonFileValue);
		}
	}

	/**
	 * return true if file is json file
	 * 
	 * @param filename
	 * @return
	 */
	public static boolean isJsonFile(String filename) {
		if (filename.toLowerCase().endsWith("json"))
			return true;
		return false;
	}

	public static String convertJsonFileToString(Path templatePath) {
		return Helper.readFileContent(templatePath.toString());
	}
	
	public static String updateJsonFromRequestBody(ServiceObject service) {
		String jsonString = DataHelper.getServiceObjectTemplateString(service);
		return updateJsonFromRequestBody(service.getRequestBody(), jsonString);
	}

	public static String updateJsonFromRequestBody(String requestbody, String jsonString) {
		Helper.assertTrue("json string is empty", !jsonString.isEmpty());

		// replace parameters
		jsonString = DataHelper.replaceParameters(jsonString);

		// if request body is json, will not replace template
		if (isJSONValid(requestbody, false))
			return requestbody;

		// get key value mapping of header parameters
		List keywords = DataHelper.getValidationMap(requestbody);
		for (KeyValue keyword : keywords) {
			if(keyword.value.toString().isEmpty()) continue;
			
			jsonString = replaceJsonPathValue(jsonString, keyword.key, keyword.value.toString());
		}
		return jsonString;
	}

	/**
	 * replace json string value based on json path eg. path: cars.name:toyota or
	 * cars.name:2:toyota
	 * 
	 * @param jsonString
	 * @param path
	 * @param value
	 * @return
	 */
	public static String replaceJsonPathValue(String jsonString, String path, String value) {
		Configuration conf = Configuration.defaultConfiguration().addOptions(Option.DEFAULT_PATH_LEAF_TO_NULL).addOptions(Option.SUPPRESS_EXCEPTIONS);  
		DocumentContext doc = null;
				
		// if json path value does not exist, we will add a new node
		if(isJsonPathValueExist(jsonString, path)) {
			 doc = JsonPath.parse(jsonString);
		}else	
			doc = JsonPath.using(conf).parse(jsonString);
		
		String prefix = "$.";
		boolean isJsonPathValueString = JsonHelper.isJsonPathValueString(jsonString, path);

		// if json path data type is other than string, remove quotes
		Object replacementValue = convertToObject(value, isJsonPathValueString);

		// in case user forgets to remove prefix
		if (path.startsWith(prefix))
			path = path.replace(prefix, "");

		try {
			doc.set(JsonPath.compile("$." + path), replacementValue);
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		return doc.jsonString();
	}
	
	public static boolean isJsonPathValueExist(String jsonString, String jsonPath) {
		Object value = JsonHelper.getJsonPathValue(jsonString, jsonPath, false, false);
		if(value == null)
			return false;
		return true;
	}

	/**
	 * convert string to object converts string to appropriate object type json
	 * supports string, number, boolean, null, object, array
	 * 
	 * @param value
	 * @param isPathValueString the path value to replace. if string, then
	 *                          replacement type will be string
	 * @return
	 */
	public static Object convertToObject(String value, boolean isPathValueString) {
		Object objectvalue = null;

		if (isPathValueString)
			objectvalue = value;
		else {
			ObjectMapper ob = new ObjectMapper();
			ob.configure(Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
			try {
				objectvalue = ob.readValue(value, Object.class);
			} catch (IOException e) {
				objectvalue = value;
			}
		}

		// if path value string is string and being replaced with null
		if (objectvalue != null && objectvalue.equals("null"))
			objectvalue = null;

		return objectvalue;
	}

	/**
	 * return true if json string is a json array
	 * 
	 * @param jsonString
	 * @return
	 */
	public static JSONArray getJsonArray(String jsonString) {
		try {
			return new JSONArray(jsonString);
		} catch (JSONException ex) {
			ex.getMessage();
		}
		return null;
	}

	/**
	 * return true if json string is a json o
	 * 
	 * @param jsonString
	 * @return
	 */
	public static JSONObject getJsonObject(String jsonString) {
		try {
			return new JSONObject(jsonString);
		} catch (JSONException ex) {
			ex.getMessage();
		}
		return null;
	}

	/**
	 * checks if json string is a structured json body with key value pairs, or just
	 * array list. eg: [{"key":"value"}] vs ["value1","value2"]
	 * 
	 * 	when syntax is {} then this is JsonObject

		when syntax is [] then this is JsonArray
	 * @param jsonString
	 * @return
	 */
	public static boolean isValidJsonObject(Object jsonObject) {
		if (jsonObject instanceof Map)
			jsonObject = new Gson().toJson(jsonObject, Map.class);

		String jsonString = jsonObject.toString();

		if (getJsonArray(jsonString) == null && getJsonObject(jsonString) == null)
			return false;
		
		
		if(jsonString.startsWith("{") && jsonString.endsWith("}")) 
			return true;
		
		if((jsonString.startsWith("[") && jsonString.endsWith("]")) &&
				jsonString.contains("{") && jsonString.contains("}")) 
			return true;
		
		if(jsonString.startsWith("[") && jsonString.endsWith("]"))
				return false;

		String jsonNormalized = DataHelper.objectToString(jsonString);
		String[] keyValues = jsonNormalized.split(",");
		for(String keyvalue : keyValues) {
			if (keyvalue.contains(":") && StringUtils.countMatches(keyvalue, ":") == 1)
			return true;
		}
		return false;
	}

	public static List arrayRemoveAllEmptyString(List list) {
		list.removeAll(Collections.singleton(""));
		return list;
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy