001/*
002 * (C) Copyright 2006-2018 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     bstefanescu, Ronan DANIELLOU <rdaniellou@nuxeo.com>
018 */
019package org.nuxeo.ecm.automation.core.util;
020
021import java.io.BufferedReader;
022import java.io.IOException;
023import java.io.Reader;
024import java.io.StringReader;
025import java.util.HashMap;
026import java.util.Iterator;
027import java.util.Map;
028
029import org.apache.commons.lang3.StringUtils;
030import org.nuxeo.ecm.automation.core.Constants;
031import org.nuxeo.runtime.api.Framework;
032import org.nuxeo.runtime.services.config.ConfigurationService;
033
034import com.fasterxml.jackson.databind.JsonNode;
035import com.fasterxml.jackson.databind.ObjectMapper;
036import com.google.common.base.MoreObjects;
037
038/**
039 * Inline properties file content. This class exists to have a real type for parameters accepting properties content.
040 *
041 * @see Constants
042 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
043 */
044public class Properties extends HashMap<String, String> {
045
046    private static final long serialVersionUID = 1L;
047
048    /**
049     * Spaces may be legitimate part of the value, there is no reason to trim them. But before NXP-19050, the behavior
050     * was to trim the values. We have put in place a contribution, which is overridden before Nuxeo 8 series, for
051     * backward compatibility. See NXP-19170.
052     *
053     * @since 8.2
054     */
055    public static final String IS_PROPERTY_VALUE_TRIMMED_KEY = "nuxeo.automation.properties.value.trim";
056
057    /**
058     * Default value is <code>false</code>.
059     *
060     * @since 8.2
061     */
062    protected static boolean isPropertyValueTrimmed() {
063        return Framework.getService(ConfigurationService.class).isBooleanPropertyTrue(IS_PROPERTY_VALUE_TRIMMED_KEY);
064    }
065
066    public static final String PROPERTIES_MULTILINE_ESCAPE = "nuxeo" + ".automation.properties.multiline.escape";
067
068    protected static final String multiLineEscape = MoreObjects.firstNonNull(
069            Framework.getProperty(PROPERTIES_MULTILINE_ESCAPE), "true");
070
071    public Properties() {
072    }
073
074    public Properties(int size) {
075        super(size);
076    }
077
078    public Properties(Map<String, String> props) {
079        super(props);
080    }
081
082    public Properties(String content) throws IOException {
083        StringReader reader = new StringReader(content);
084        Map<String, String> props = new HashMap<>();
085        loadProperties(reader, props);
086        putAll(props);
087    }
088
089    /**
090     * Constructs a Properties map based on a Json node.
091     *
092     * @since 5.7.3
093     */
094    public Properties(JsonNode node) throws IOException {
095        Iterator<Entry<String, JsonNode>> fields = node.fields();
096        ObjectMapper om = new ObjectMapper();
097        while (fields.hasNext()) {
098            Entry<String, JsonNode> entry = fields.next();
099            String key = entry.getKey();
100            JsonNode subNode = entry.getValue();
101            put(key, extractValueFromNode(subNode, om));
102        }
103    }
104
105    /**
106     * @since 5.8-HF01
107     */
108    private String extractValueFromNode(JsonNode node, ObjectMapper om) throws IOException {
109        if (!node.isNull()) {
110            return node.isContainerNode() ? om.writeValueAsString(node) : node.asText();
111        } else {
112            return null;
113        }
114    }
115
116    public static Map<String, String> loadProperties(Reader reader) throws IOException {
117        Map<String, String> map = new HashMap<>();
118        loadProperties(reader, map);
119        return map;
120    }
121
122    public static void loadProperties(Reader reader, Map<String, String> map) throws IOException {
123
124        boolean isPropertyValueToBeTrimmed = isPropertyValueTrimmed();
125        BufferedReader in = new BufferedReader(reader);
126        String line = in.readLine();
127        String prevLine = null;
128        String lineSeparator = "\n";
129        while (line != null) {
130            if (prevLine == null) {
131                // we start a new property
132                if (line.startsWith("#") || StringUtils.isBlank(line)) {
133                    // skip comments, empty or blank line
134                    line = in.readLine();
135                    continue;
136                }
137            }
138            if (line.endsWith("\\") && Boolean.parseBoolean(multiLineEscape)) {
139                line = line.substring(0, line.length() - 1);
140                prevLine = (prevLine != null ? prevLine + line : line) + lineSeparator;
141                line = in.readLine();
142                continue;
143            }
144            if (prevLine != null) {
145                line = prevLine + line;
146            }
147            prevLine = null;
148            setPropertyLine(map, line, isPropertyValueToBeTrimmed);
149            line = in.readLine();
150        }
151        if (prevLine != null) {
152            setPropertyLine(map, prevLine, isPropertyValueToBeTrimmed);
153        }
154    }
155
156    /**
157     * @param isPropertyValueToBeTrimmed The caller may store the value, to prevent from fetching it every time.
158     */
159    private static void setPropertyLine(Map<String, String> map, String line, boolean isPropertyValueToBeTrimmed)
160            throws IOException {
161        int i = line.indexOf('=');
162        if (i == -1) {
163            throw new IOException("Invalid property line (cannot find a '=') in: '" + line + "'");
164        }
165        // we trim() the key, but not the value (by default, but you may override this for backward compatibility with
166        // former code. See: NXP-19170): spaces and new lines are legitimate part of the value
167        String value = line.substring(i + 1);
168        if (isPropertyValueToBeTrimmed) {
169            value = value.trim();
170        }
171        map.put(line.substring(0, i).trim(), value);
172    }
173
174}