aboutsummaryrefslogtreecommitdiffstats
path: root/config-application-package/src/main/java/com/yahoo/config/application/PropertiesProcessor.java
blob: d0a6dbeee759a809b5f1a308375f2e436a755980 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.config.application;

import java.util.logging.Level;
import com.yahoo.text.XML;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.xml.transform.TransformerException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.logging.Logger;

/**
 * Handles getting properties from services.xml and replacing references to properties with their real values
 *
 * @author hmusum
 */
class PropertiesProcessor implements PreProcessor {

    private final static Logger log = Logger.getLogger(PropertiesProcessor.class.getName());
    private final LinkedHashMap<String, String> properties;

    PropertiesProcessor() {
        properties = new LinkedHashMap<>();
    }

    public Document process(Document input) throws TransformerException {
        Document doc = Xml.copyDocument(input);
        Document document = buildProperties(doc);
        applyProperties(document.getDocumentElement());
        return document;
    }

    private Document buildProperties(Document input) {
        NodeList list = input.getElementsByTagNameNS(XmlPreProcessor.preprocessNamespaceUri, "properties");
        while (list.getLength() > 0) {
            Element propertiesElement = (Element) list.item(0);
            Element parent = (Element) propertiesElement.getParentNode();
            for (Node node : XML.getChildren(propertiesElement)) {
                String propertyName = node.getNodeName();
                if (properties.containsKey(propertyName)) {
                    log.log(Level.WARNING, "Duplicate definition for property '" + propertyName + "' detected");
                }
                properties.put(propertyName, node.getTextContent());
            }
            parent.removeChild(propertiesElement);
            list = input.getElementsByTagNameNS(XmlPreProcessor.preprocessNamespaceUri, "properties");
        }
        return input;
    }

    private void applyProperties(Element parent) {
        NamedNodeMap attributes = parent.getAttributes();
        for (int i = 0; i < attributes.getLength(); i++) {
            Node a = attributes.item(i);
            if (hasProperty(a)) {
                replaceAttributePropertyWithValue(a);
            }
        }

        if (XML.getChildren(parent).isEmpty() && parent.getTextContent() != null) {
            if (hasPropertyInElement(parent)) {
                replaceElementPropertyWithValue(parent);
            }
        }

        // Repeat for remaining children;
        for (Element child : XML.getChildren(parent)) {
            applyProperties(child);
        }
    }

    private void replaceAttributePropertyWithValue(Node a) {
        String propertyValue = a.getNodeValue();
        String replacedPropertyValue = replaceValue(propertyValue);
        a.setNodeValue(replacedPropertyValue);
    }

    private String replaceValue(String propertyValue) {
        // Use a list with keys sorted by length (longest key first)
        // Needed for replacing values where you have overlapping keys
        ArrayList<String> keys = new ArrayList<>(properties.keySet());
        keys.sort(Collections.reverseOrder(Comparator.comparing(String::length)));

        for (String key : keys) {
            String value = properties.get(key);
            // Try to find exact match first and since this is done with longest key
            // first, the else branch will only happen when there cannot be an exact
            // match, i.e. where you want to replace only parts of the attribute or node value
            if (propertyValue.equals("${" + key + "}")) {
                String regex = "\\$\\{" + key + "}";
                return propertyValue.replaceAll(regex, value);
            } else if (propertyValue.contains(key)) {
                return propertyValue.replaceAll("\\$\\{" + key + "}", value);
            }
        }
        throw new IllegalArgumentException("Unable to find property replace in " + propertyValue);
    }

    private void replaceElementPropertyWithValue(Node a) {
        String propertyValue = a.getTextContent();
        String replacedPropertyValue = replaceValue(propertyValue);
        a.setTextContent(replacedPropertyValue);
    }

    private static boolean hasProperty(Node node) {
        return hasProperty(node.getNodeValue());
    }

    private static boolean hasPropertyInElement(Node node) {
        return hasProperty(node.getTextContent());
    }

    private static boolean hasProperty(String s) {
        return s.matches("^.*\\$\\{.+}.*$");
    }

    public LinkedHashMap<String, String> getProperties() {
        return properties;
    }

}