001/*
002 * (C) Copyright 2006-2016 Nuxeo SA (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 com.google.common.base.Objects;
030
031import org.apache.commons.lang.StringUtils;
032import org.codehaus.jackson.JsonNode;
033import org.codehaus.jackson.map.ObjectMapper;
034import org.nuxeo.ecm.automation.core.Constants;
035import org.nuxeo.runtime.api.Framework;
036import org.nuxeo.runtime.services.config.ConfigurationService;
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 = Objects.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        loadProperties(reader, this);
085    }
086
087    /**
088     * Constructs a Properties map based on a Json node.
089     *
090     * @param node
091     * @throws IOException
092     * @since 5.7.3
093     */
094    public Properties(JsonNode node) throws IOException {
095        Iterator<Entry<String, JsonNode>> fields = node.getFields();
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     * @param om
107     * @param subNode
108     * @return
109     * @throws IOException
110     * @since 5.8-HF01
111     */
112    private String extractValueFromNode(JsonNode node, ObjectMapper om) throws IOException {
113        if (!node.isNull()) {
114            return node.isContainerNode() ? om.writeValueAsString(node) : node.getValueAsText();
115        } else {
116            return null;
117        }
118    }
119
120    public static Map<String, String> loadProperties(Reader reader) throws IOException {
121        Map<String, String> map = new HashMap<String, String>();
122        loadProperties(reader, map);
123        return map;
124    }
125
126    public static void loadProperties(Reader reader, Map<String, String> map) throws IOException {
127
128        boolean isPropertyValueToBeTrimmed = isPropertyValueTrimmed();
129        BufferedReader in = new BufferedReader(reader);
130        String line = in.readLine();
131        String prevLine = null;
132        String lineSeparator = "\n";
133        while (line != null) {
134            if (prevLine == null) {
135                // we start a new property
136                if (line.startsWith("#") || StringUtils.isBlank(line)) {
137                    // skip comments, empty or blank line
138                    line = in.readLine();
139                    continue;
140                }
141            }
142            if (line.endsWith("\\") && Boolean.valueOf(multiLineEscape)) {
143                line = line.substring(0, line.length() - 1);
144                prevLine = (prevLine != null ? prevLine + line : line) + lineSeparator;
145                line = in.readLine();
146                continue;
147            }
148            if (prevLine != null) {
149                line = prevLine + line;
150            }
151            prevLine = null;
152            setPropertyLine(map, line, isPropertyValueToBeTrimmed);
153            line = in.readLine();
154        }
155        if (prevLine != null) {
156            setPropertyLine(map, prevLine, isPropertyValueToBeTrimmed);
157        }
158    }
159
160    /**
161     * @param isPropertyValueToBeTrimmed The caller may store the value, to prevent from fetching it every time.
162     */
163    private static void setPropertyLine(Map<String, String> map, String line, boolean isPropertyValueToBeTrimmed)
164            throws IOException {
165        int i = line.indexOf('=');
166        if (i == -1) {
167            throw new IOException("Invalid property line (cannot find a '=') in: '" + line + "'");
168        }
169        // we trim() the key, but not the value (by default, but you may override this for backward compatibility with
170        // former code. See: NXP-19170): spaces and new lines are legitimate part of the value
171        String value = line.substring(i + 1);
172        if (isPropertyValueToBeTrimmed) {
173            value = value.trim();
174        }
175        map.put(line.substring(0, i).trim(), value);
176    }
177
178}