/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.ranger.utils.install; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.util.Properties; import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.apache.commons.cli.BasicParser; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Option; import org.apache.commons.cli.OptionBuilder; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; public class XmlConfigChanger { private static final String EMPTY_TOKEN = "%EMPTY%"; private static final String EMPTY_TOKEN_VALUE = ""; public static final String ROOT_NODE_NAME = "configuration"; public static final String NAME_NODE_NAME = "name"; public static final String PROPERTY_NODE_NAME = "property"; public static final String VALUE_NODE_NAME = "value"; private File inpFile; private File outFile; private File confFile; private File propFile; private Document doc; Properties installProperties = new Properties(); public static void main(String[] args) { XmlConfigChanger xmlConfigChanger = new XmlConfigChanger(); xmlConfigChanger.parseConfig(args); try { xmlConfigChanger.run(); } catch(Throwable t) { System.err.println("*************************************************************************"); System.err.println("******* ERROR: unable to process xml configuration changes due to error:" + t.getMessage()); t.printStackTrace(); System.err.println("*************************************************************************"); System.exit(1); } } @SuppressWarnings("static-access") public void parseConfig(String[] args) { Options options = new Options(); Option inputOption = OptionBuilder.hasArgs(1).isRequired().withLongOpt("input").withDescription("Input xml file name").create('i'); options.addOption(inputOption); Option outputOption = OptionBuilder.hasArgs(1).isRequired().withLongOpt("output").withDescription("Output xml file name").create('o'); options.addOption(outputOption); Option configOption = OptionBuilder.hasArgs(1).isRequired().withLongOpt("config").withDescription("Config file name").create('c'); options.addOption(configOption); Option installPropOption = OptionBuilder.hasArgs(1).isRequired(false).withLongOpt("installprop").withDescription("install.properties").create('p'); options.addOption(installPropOption); CommandLineParser parser = new BasicParser(); CommandLine cmd = null; try { cmd = parser.parse(options, args); } catch (ParseException e) { String header = "ERROR: " + e; HelpFormatter helpFormatter = new HelpFormatter(); helpFormatter.printHelp("java " + XmlConfigChanger.class.getName(), header, options, null, true); throw new RuntimeException(e); } String inputFileName = cmd.getOptionValue('i'); this.inpFile = new File(inputFileName); if (! this.inpFile.canRead()) { String header = "ERROR: Input file [" + this.inpFile.getAbsolutePath() + "] can not be read."; HelpFormatter helpFormatter = new HelpFormatter(); helpFormatter.printHelp("java " + XmlConfigChanger.class.getName(), header, options, null, true); throw new RuntimeException(header); } String outputFileName = cmd.getOptionValue('o'); this.outFile = new File(outputFileName); if (this.outFile.exists()) { String header = "ERROR: Output file [" + this.outFile.getAbsolutePath() + "] already exists. Specify a filepath for creating new output file for the input [" + this.inpFile.getAbsolutePath() + "]"; HelpFormatter helpFormatter = new HelpFormatter(); helpFormatter.printHelp("java " + XmlConfigChanger.class.getName(), header, options, null, true); throw new RuntimeException(header); } String configFileName = cmd.getOptionValue('c'); this.confFile = new File(configFileName); if (! this.confFile.canRead()) { String header = "ERROR: Config file [" + this.confFile.getAbsolutePath() + "] can not be read."; HelpFormatter helpFormatter = new HelpFormatter(); helpFormatter.printHelp("java " + XmlConfigChanger.class.getName(), header, options, null, true); throw new RuntimeException(header); } String installPropFileName = (cmd.hasOption('p') ? cmd.getOptionValue('p') : null ); if (installPropFileName != null) { this.propFile = new File(installPropFileName); if (! this.propFile.canRead()) { String header = "ERROR: Install Property file [" + this.propFile.getAbsolutePath() + "] can not be read."; HelpFormatter helpFormatter = new HelpFormatter(); helpFormatter.printHelp("java " + XmlConfigChanger.class.getName(), header, options, null, true); throw new RuntimeException(header); } } } public void run() throws ParserConfigurationException, SAXException, IOException, TransformerException { loadInstallProperties(); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); factory.setFeature("http://xml.org/sax/features/external-general-entities", false); factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); DocumentBuilder builder = factory.newDocumentBuilder(); doc = builder.parse(inpFile); BufferedReader reader = null; try { reader = new BufferedReader(new FileReader(confFile)); String line = null; @SuppressWarnings("unused") int lineNo = 0; Properties variables = new Properties(); while ((line = reader.readLine()) != null) { lineNo++; line = line.trim(); if (line.isEmpty() ) continue; if (line.startsWith("#")) { continue; } if (line.contains("#")) { int len = line.indexOf("#"); line = line.substring(0,len); } String[] tokens = line.split("\\s+"); String propName = tokens[0]; String propValue = null; try { if (propnameContainsVariables(propName)) { propName = replaceProp(propName, variables); } propValue = replaceProp(tokens[1],installProperties); } catch (ValidationException e) { // throw new RuntimeException("Unable to replace tokens in the line: \n[" + line + "]\n in file [" + confFile.getAbsolutePath() + "] line number:[" + lineNo + "]" ); throw new RuntimeException(e); } String actionType = tokens[2]; String options = (tokens.length > 3 ? tokens[3] : null); boolean createIfNotExists = (options != null && options.contains("create-if-not-exists")); if ("add".equals(actionType)) { addProperty(propName, propValue); } else if ("mod".equals(actionType)) { modProperty(propName, propValue,createIfNotExists); } else if ("del".equals(actionType)) { delProperty(propName); } else if ("append".equals(actionType)) { String curVal = getProperty(propName); if (curVal == null) { if (createIfNotExists) { addProperty(propName, propValue); } } else { String appendDelimitor = (tokens.length > 4 ? tokens[4] : " "); if (! curVal.contains(propValue)) { String newVal = null; if (curVal.length() == 0) { newVal = propValue; } else { newVal = curVal + appendDelimitor + propValue; } modProperty(propName, newVal,createIfNotExists); } } } else if ("delval".equals(actionType)) { String curVal = getProperty(propName); if (curVal != null) { String appendDelimitor = (tokens.length > 4 ? tokens[4] : " "); if (curVal.contains(propValue)) { String[] valTokens = curVal.split(appendDelimitor); StringBuilder sb = new StringBuilder(); for(String v : valTokens) { if (! v.equals(propValue)) { if (sb.length() > 0) { sb.append(appendDelimitor); } sb.append(v); } } String newVal = sb.toString(); modProperty(propName, newVal,createIfNotExists); } } } else if ("var".equals(actionType)) { variables.put(propName, propValue); } else { throw new RuntimeException("Unknown Command Found: [" + actionType + "], Supported Types: add modify del append"); } } TransformerFactory tfactory = TransformerFactory.newInstance(); tfactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, Boolean.TRUE); Transformer transformer = tfactory.newTransformer(); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); DOMSource source = new DOMSource(doc); FileOutputStream out = new FileOutputStream(outFile); StreamResult result = new StreamResult(out); transformer.transform(source, result); out.close(); } finally { if (reader != null) { reader.close(); } } } /** * Check if prop name contains a substitution variable embedded in it, e.g. %VAR_NAME%. * @param propName * @return true if propname contains at least 2 '%' characters in it, else false */ private boolean propnameContainsVariables(String propName) { if (propName != null) { int first = propName.indexOf('%'); if (first != -1) { // indexof is safe even if 2nd argument is beyond size of string, i.e. if 1st percent was the last character of the string. int second = propName.indexOf('%', first + 1); if (second != -1) { return true; } } } return false; } private void addProperty(String propName, String val) { NodeList nl = doc.getElementsByTagName(ROOT_NODE_NAME); Node rootConfig = nl.item(0); rootConfig.appendChild(createNewElement(propName,val)); } private void modProperty(String propName, String val, boolean createIfNotExists) { Node node = findProperty(propName); if (node != null) { NodeList cnl = node.getChildNodes(); for (int j = 0; j < cnl.getLength() ; j++) { String nodeName = cnl.item(j).getNodeName(); if (nodeName.equals(VALUE_NODE_NAME)) { if (cnl.item(j).hasChildNodes()) { cnl.item(j).getChildNodes().item(0).setNodeValue(val); } else { Node propValueNode = cnl.item(j); Node txtNode = doc.createTextNode(val); propValueNode.appendChild(txtNode); txtNode.setNodeValue(val); } return; } } } if (createIfNotExists) { addProperty(propName, val); } } private String getProperty(String propName) { String ret = null; try { Node node = findProperty(propName); if (node != null) { NodeList cnl = node.getChildNodes(); for (int j = 0; j < cnl.getLength() ; j++) { String nodeName = cnl.item(j).getNodeName(); if (nodeName.equals(VALUE_NODE_NAME)) { Node valueNode = null; if (cnl.item(j).hasChildNodes()) { valueNode = cnl.item(j).getChildNodes().item(0); } if (valueNode == null) { // Value Node is defined with ret = ""; } else { ret = valueNode.getNodeValue(); } break; } } } } catch(Throwable t) { throw new RuntimeException("getProperty(" + propName + ") failed.", t); } return ret; } private void delProperty(String propName) { Node node = findProperty(propName); if (node != null) { node.getParentNode().removeChild(node); } } private Node findProperty(String propName) { Node ret = null; try { NodeList nl = doc.getElementsByTagName(PROPERTY_NODE_NAME); for(int i = 0; i < nl.getLength() ; i++) { NodeList cnl = nl.item(i).getChildNodes(); boolean found = false; for (int j = 0; j < cnl.getLength() ; j++) { String nodeName = cnl.item(j).getNodeName(); if (nodeName.equals(NAME_NODE_NAME)) { String pName = cnl.item(j).getChildNodes().item(0).getNodeValue(); found = pName.equals(propName); if (found) break; } } if (found) { ret = nl.item(i); break; } } } catch(Throwable t) { throw new RuntimeException("findProperty(" + propName + ") failed.", t); } return ret; } private Element createNewElement(String propName, String val) { Element ret = null; try { if (doc != null) { ret = doc.createElement(PROPERTY_NODE_NAME); Node propNameNode = doc.createElement(NAME_NODE_NAME); Node txtNode = doc.createTextNode(propName); propNameNode.appendChild(txtNode); propNameNode.setNodeValue(propName); ret.appendChild(propNameNode); Node propValueNode = doc.createElement(VALUE_NODE_NAME); txtNode = doc.createTextNode(val); propValueNode.appendChild(txtNode); propValueNode.setNodeValue(propName); ret.appendChild(propValueNode); } } catch(Throwable t) { throw new RuntimeException("createNewElement(" + propName + ") with value [" + val + "] failed.", t); } return ret; } private void loadInstallProperties() { if (propFile != null) { try (FileInputStream in = new FileInputStream(propFile)) { installProperties.load(in); } catch (IOException ioe) { System.err.println("******* ERROR: load file failure. The reason: " + ioe.getMessage()); ioe.printStackTrace(); } } // To support environment variable, we will add all environment variables to the Properties installProperties.putAll(System.getenv()); } private String replaceProp(String propValue, Properties prop) throws ValidationException { StringBuilder tokensb = new StringBuilder(); StringBuilder retsb = new StringBuilder(); boolean isToken = false; for(char c : propValue.toCharArray()) { if (c == '%') { if (isToken) { String token = tokensb.toString(); String tokenValue = (token.length() == 0 ? "%" : prop.getProperty(token) ); if (tokenValue == null || tokenValue.trim().isEmpty()) { throw new ValidationException("ERROR: configuration token [" + token + "] is not defined in the file: [" + (propFile != null ? propFile.getAbsolutePath() : "{no install.properties file specified using -p option}") + "]"); } else { if (EMPTY_TOKEN.equals(tokenValue)) { retsb.append(EMPTY_TOKEN_VALUE); } else { retsb.append(tokenValue); } } isToken = false; } else { isToken = true; tokensb.setLength(0); } } else if (isToken) { tokensb.append(String.valueOf(c)); } else { retsb.append(String.valueOf(c)); } } if (isToken) { throw new ValidationException("ERROR: configuration has a token defined without end-token [" + propValue + "] in the file: [" + (propFile != null ? propFile.getAbsolutePath() : "{no install.properties file specified using -p option}") + "]"); } return retsb.toString(); } @SuppressWarnings("serial") class ValidationException extends Exception { public ValidationException(String msg) { super(msg); } public ValidationException(Throwable cause) { super(cause); } } }