/* * ToroDB * Copyright © 2014 8Kdata Technology (www.8kdata.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.torodb.packaging.config.util; import com.beust.jcommander.internal.Console; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.core.JsonGenerationException; import com.fasterxml.jackson.core.JsonParser.Feature; import com.fasterxml.jackson.core.JsonPointer; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonMappingException.Reference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.exc.PropertyBindingException; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; import com.google.common.base.Charsets; import com.torodb.packaging.config.model.backend.BackendPasswordConfig; import com.torodb.packaging.config.model.protocol.mongo.MongoPasswordConfig; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.List; import java.util.ResourceBundle; import java.util.Set; import javax.validation.ConstraintViolation; import javax.validation.Path; import javax.validation.Validation; import javax.validation.Validator; import javax.validation.ValidatorFactory; public class ConfigUtils { private static final Logger LOGGER = LogManager.getLogger(ConfigUtils.class); public static final String getUserHomePath() { return System.getProperty("user.home", "."); } public static final String getUserHomeFilePath(String file) { return getUserHomePath() + File.separatorChar + file; } public static ObjectMapper mapper() { ObjectMapper objectMapper = new ObjectMapper(); configMapper(objectMapper); return objectMapper; } public static YAMLMapper yamlMapper() { YAMLMapper yamlMapper = new YAMLMapper(); configMapper(yamlMapper); return yamlMapper; } public static XmlMapper xmlMapper() { XmlMapper xmlMapper = new XmlMapper(); configMapper(xmlMapper); return xmlMapper; } private static void configMapper(ObjectMapper objectMapper) { objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true); objectMapper.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, true); objectMapper.configure(Feature.ALLOW_COMMENTS, true); objectMapper.configure(Feature.ALLOW_YAML_COMMENTS, true); objectMapper.setSerializationInclusion(Include.NON_NULL); objectMapper.enable(SerializationFeature.INDENT_OUTPUT); } public static IllegalArgumentException transformJsonMappingException( JsonMappingException jsonMappingException) { JsonPointer jsonPointer = JsonPointer.compile("/config"); for (Reference reference : jsonMappingException.getPath()) { if (reference.getIndex() != -1) { jsonPointer = jsonPointer.append(JsonPointer.compile("/" + reference.getIndex())); } if (reference.getFieldName() != null) { jsonPointer = jsonPointer.append(JsonPointer.compile("/" + reference.getFieldName())); } } if (jsonMappingException instanceof PropertyBindingException) { return transformJsonMappingException(jsonPointer, (PropertyBindingException) jsonMappingException); } return transformJsonMappingException(jsonPointer, jsonMappingException); } private static IllegalArgumentException transformJsonMappingException(JsonPointer jsonPointer, JsonMappingException jsonMappingException) { return transformJsonMappingException(jsonPointer, "Wrong value", jsonMappingException); } private static IllegalArgumentException transformJsonMappingException(JsonPointer jsonPointer, PropertyBindingException jsonMappingException) { return transformJsonMappingException(jsonPointer, "Unrecognized field " + jsonMappingException .getPropertyName() + " (known fields: " + jsonMappingException.getKnownPropertyIds() + ")", jsonMappingException); } private static IllegalArgumentException transformJsonMappingException(JsonPointer jsonPointer, String message, JsonMappingException jsonMappingException) { if (LOGGER.isDebugEnabled()) { return new IllegalArgumentException("Validation error at " + jsonPointer + ": " + message, jsonMappingException); } return new IllegalArgumentException("Validation error at " + jsonPointer + ": " + message); } public static <T> T readConfigFromYaml(Class<T> configClass, String yamlString) throws JsonProcessingException, IOException { ObjectMapper objectMapper = mapper(); YAMLMapper yamlMapper = yamlMapper(); JsonNode configNode = yamlMapper.readTree(yamlString); T config = objectMapper.treeToValue(configNode, configClass); validateBean(config); return config; } public static <T> T readConfigFromXml(Class<T> configClass, String xmlString) throws JsonProcessingException, IOException { ObjectMapper objectMapper = mapper(); XmlMapper xmlMapper = xmlMapper(); JsonNode configNode = xmlMapper.readTree(xmlString); T config = objectMapper.treeToValue(configNode, configClass); validateBean(config); return config; } public static void parseToropassFile(final BackendPasswordConfig backendPasswordConfig) throws FileNotFoundException, IOException { backendPasswordConfig.setPassword(getPasswordFromPassFile( backendPasswordConfig.getToropassFile(), backendPasswordConfig.getHost(), backendPasswordConfig.getPort(), backendPasswordConfig.getDatabase(), backendPasswordConfig.getUser())); } public static void parseMongopassFile(final MongoPasswordConfig mongoPasswordConfig) throws FileNotFoundException, IOException { mongoPasswordConfig.setPassword(getPasswordFromPassFile( mongoPasswordConfig.getMongopassFile(), mongoPasswordConfig.getHost(), mongoPasswordConfig.getPort(), mongoPasswordConfig.getDatabase(), mongoPasswordConfig.getUser())); } private static String getPasswordFromPassFile(String passFile, String host, int port, String database, String user) throws FileNotFoundException, IOException { File pass = new File(passFile); if (pass.exists() && pass.canRead() && pass.isFile()) { BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(pass), Charsets.UTF_8)); try { String line; int index = 0; while ((line = br.readLine()) != null) { index++; String[] passChunks = line.split(":"); if (passChunks.length != 5) { LOGGER.warn("Wrong format at line " + index + " of file " + passFile); continue; } if ((passChunks[0].equals("*") || passChunks[0].equals(host)) && (passChunks[1].equals("*") || passChunks[1].equals(String.valueOf(port))) && (passChunks[2].equals("*") || passChunks[2].equals(database)) && (passChunks[3].equals("*") || passChunks[3].equals(user))) { return passChunks[4]; } } br.close(); } finally { br.close(); } } return null; } public static ObjectNode mergeParam(ObjectMapper objectMapper, ObjectNode configRootNode, String pathAndProp, String value) throws Exception { if (JsonPointer.compile(pathAndProp).equals(JsonPointer.compile("/"))) { return (ObjectNode) objectMapper.readTree(value); } String path = pathAndProp.substring(0, pathAndProp.lastIndexOf("/")); String prop = pathAndProp.substring(pathAndProp.lastIndexOf("/") + 1); JsonPointer pathPointer = JsonPointer.compile(path); JsonNode pathNode = configRootNode.at(pathPointer); if (pathNode.isMissingNode() || pathNode.isNull()) { JsonPointer currentPointer = pathPointer; JsonPointer childOfCurrentPointer = null; List<JsonPointer> missingPointers = new ArrayList<>(); List<JsonPointer> childOfMissingPointers = new ArrayList<>(); do { if (pathNode.isMissingNode() || pathNode.isNull()) { missingPointers.add(0, currentPointer); childOfMissingPointers.add(0, childOfCurrentPointer); } childOfCurrentPointer = currentPointer; currentPointer = currentPointer.head(); pathNode = configRootNode.at(currentPointer); } while (pathNode.isMissingNode() || pathNode.isNull()); for (int missingPointerIndex = 0; missingPointerIndex < missingPointers.size(); missingPointerIndex++) { final JsonPointer missingPointer = missingPointers.get(missingPointerIndex); final JsonPointer childOfMissingPointer = childOfMissingPointers.get(missingPointerIndex); final List<JsonNode> newNodes = new ArrayList<>(); if (pathNode.isObject()) { ((ObjectNode) pathNode).set(missingPointer.last().getMatchingProperty(), createNode(childOfMissingPointer, newNodes)); } else if (pathNode.isArray() && missingPointer.last().mayMatchElement()) { for (int index = ((ArrayNode) pathNode).size(); index < missingPointer.last() .getMatchingIndex() + 1; index++) { ((ArrayNode) pathNode).add(createNode(childOfMissingPointer, newNodes)); } } else { throw new RuntimeException("Cannot set param " + pathAndProp + "=" + value); } pathNode = newNodes.get(newNodes.size() - 1); } } Object valueAsObject; try { valueAsObject = objectMapper.readValue(value, Object.class); } catch (JsonMappingException jsonMappingException) { throw JsonMappingException.wrapWithPath(jsonMappingException, configRootNode, path .substring(1) + "/" + prop); } if (pathNode instanceof ObjectNode) { ObjectNode objectNode = (ObjectNode) pathNode; if (valueAsObject != null) { JsonNode valueNode = objectMapper.valueToTree(valueAsObject); objectNode.set(prop, valueNode); } else { objectNode.remove(prop); } } else if (pathNode instanceof ArrayNode) { ArrayNode arrayNode = (ArrayNode) pathNode; Integer index = Integer.valueOf(prop); if (valueAsObject != null) { JsonNode valueNode = objectMapper.valueToTree(valueAsObject); arrayNode.set(index, valueNode); } else { arrayNode.remove(index); } } return configRootNode; } public static <T> JsonNode getParam(T config, String pathAndProp) throws Exception { XmlMapper xmlMapper = xmlMapper(); JsonNode configNode = xmlMapper.valueToTree(config); if (JsonPointer.compile(pathAndProp).equals(JsonPointer.compile("/"))) { return configNode; } JsonPointer pathPointer = JsonPointer.compile(pathAndProp); JsonNode pathNode = configNode.at(pathPointer); if (pathNode.isMissingNode() || pathNode.isNull()) { return null; } return pathNode; } private static JsonNode createNode(JsonPointer childOfPointer, List<JsonNode> newNodes) { JsonNode newNode; if (childOfPointer == null || !childOfPointer.last().mayMatchElement()) { newNode = JsonNodeFactory.instance.objectNode(); } else { newNode = JsonNodeFactory.instance.arrayNode(); } newNodes.add(newNode); return newNode; } public static <T> void printYamlConfig(T config, Console console) throws IOException, JsonGenerationException, JsonMappingException { ObjectMapper objectMapper = yamlMapper(); ObjectWriter objectWriter = objectMapper.writer(); printConfig(config, console, objectWriter); } public static <T> void printXmlConfig(T config, Console console) throws IOException, JsonGenerationException, JsonMappingException { ObjectMapper objectMapper = xmlMapper(); ObjectWriter objectWriter = objectMapper.writer(); objectWriter = objectWriter.withRootName("config"); printConfig(config, console, objectWriter); } private static <T> void printConfig(T config, Console console, ObjectWriter objectWriter) throws IOException, JsonGenerationException, JsonMappingException, UnsupportedEncodingException { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); PrintStream printStream = new PrintStream(byteArrayOutputStream, false, Charsets.UTF_8.name()); objectWriter.writeValue(printStream, config); console.println(byteArrayOutputStream.toString(Charsets.UTF_8.name())); } public static <T> void printParamDescriptionFromConfigSchema(Class<T> configClass, ResourceBundle resourceBundle, Console console, int tabs) throws UnsupportedEncodingException, JsonMappingException { ObjectMapper objectMapper = mapper(); DescriptionFactoryWrapper visitor = new DescriptionFactoryWrapper( resourceBundle, console, tabs); objectMapper.acceptJsonFormatVisitor(objectMapper.constructType(configClass), visitor); console.println(""); } public static <T> void validateBean(T config) { ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); Validator validator = factory.getValidator(); Set<ConstraintViolation<T>> constraintViolations = validator.validate(config); if (!constraintViolations.isEmpty()) { IllegalArgumentException illegalArgumentException = transformConstraintsValidation( constraintViolations); throw illegalArgumentException; } } private static <T> IllegalArgumentException transformConstraintsValidation( Set<ConstraintViolation<T>> constraintViolations) { ConstraintViolation<T> constraintViolation = constraintViolations.iterator().next(); Path path = constraintViolation.getPropertyPath(); JsonPointer jsonPointer = toJsonPointer(path); return new IllegalArgumentException("Constraint validation errors at " + jsonPointer.toString() + ": " + constraintViolation.getMessage()); } public static JsonPointer toJsonPointer(Path path) { JsonPointer pointer = JsonPointer.valueOf(null); for (Path.Node pathNode : path) { if (pathNode.getIndex() != null) { pointer = pointer.append(JsonPointer.valueOf("/" + pathNode.getIndex())); } if (pathNode.getName() != null) { pointer = pointer.append(JsonPointer.valueOf("/" + pathNode.getName())); } } return pointer; } }