001/*
002 * (C) Copyright 2015-2019 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 *      Andre Justo
018 *      Anahide Tchertchian
019 *      Kevin Leturc <kleturc@nuxeo.com>
020 */
021package org.nuxeo.runtime.services.config;
022
023import static java.lang.Boolean.FALSE;
024import static java.lang.Boolean.TRUE;
025
026import java.io.IOException;
027import java.io.Serializable;
028import java.time.Duration;
029import java.time.format.DateTimeParseException;
030import java.util.List;
031import java.util.Map;
032import java.util.Optional;
033import java.util.Properties;
034import java.util.stream.Collectors;
035
036import org.apache.commons.lang3.StringUtils;
037import org.apache.logging.log4j.LogManager;
038import org.apache.logging.log4j.Logger;
039import org.nuxeo.common.utils.DurationUtils;
040import org.nuxeo.runtime.api.Framework;
041import org.nuxeo.runtime.logging.DeprecationLogger;
042import org.nuxeo.runtime.model.ComponentInstance;
043import org.nuxeo.runtime.model.DefaultComponent;
044
045import com.fasterxml.jackson.databind.ObjectMapper;
046import com.fasterxml.jackson.databind.node.ObjectNode;
047import com.fasterxml.jackson.dataformat.javaprop.JavaPropsMapper;
048
049/**
050 * @since 7.4
051 */
052public class ConfigurationServiceImpl extends DefaultComponent implements ConfigurationService {
053
054    protected static final Logger log = LogManager.getLogger(ConfigurationServiceImpl.class);
055
056    public static final String CONFIGURATION_EP = "configuration";
057
058    protected static final JavaPropsMapper PROPERTIES_MAPPER = new JavaPropsMapper();
059
060    protected static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
061
062    /**
063     * XXX remove once we are able to get such a cached map from DefaultComponent
064     *
065     * @since 10.3
066     */
067    protected volatile Map<String, ConfigurationPropertyDescriptor> descriptors;
068
069    /**
070     * XXX remove once we are able to get such a cached map from DefaultComponent.
071     * <p>
072     * We'd ideally need a &lt;T extends Descriptor&gt; Map&lt;String, T&gt; getDescriptors(String xp) with cache method.
073     *
074     * @since 10.3
075     */
076    protected Map<String, ConfigurationPropertyDescriptor> getDescriptors() {
077        Map<String, ConfigurationPropertyDescriptor> d = descriptors;
078        if (d == null) {
079            synchronized (this) {
080                d = descriptors;
081                if (d == null) {
082                    List<ConfigurationPropertyDescriptor> descs = getDescriptors(CONFIGURATION_EP);
083                    descriptors = d = descs.stream().collect(Collectors.toMap(desc -> desc.getId(), desc -> desc));
084                }
085            }
086        }
087        return d;
088    }
089
090    @Override
091    @Deprecated
092    public String getProperty(String key) {
093        return getString(key, null);
094    }
095
096    @Override
097    @Deprecated
098    public String getProperty(String key, String defaultValue) {
099        return getString(key, defaultValue);
100    }
101
102    @Override
103    @Deprecated
104    public boolean isBooleanPropertyTrue(String key) {
105        String value = getProperty(key);
106        return Boolean.parseBoolean(value);
107    }
108
109    @Override
110    @Deprecated
111    public boolean isBooleanPropertyFalse(String key) {
112        String value = getProperty(key);
113        return StringUtils.isNotBlank(value) && !Boolean.parseBoolean(value);
114    }
115
116    @Override
117    public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
118        if (CONFIGURATION_EP.equals(extensionPoint)) {
119            synchronized (this) {
120                descriptors = null;
121            }
122            ConfigurationPropertyDescriptor configurationPropertyDescriptor = (ConfigurationPropertyDescriptor) contribution;
123            String key = configurationPropertyDescriptor.getName();
124            if (Framework.getProperties().containsKey(key)) {
125                String message = "Property '" + key + "' should now be contributed to extension "
126                        + "point 'org.nuxeo.runtime.ConfigurationService', using target 'configuration'";
127                DeprecationLogger.log(message, "7.4");
128            }
129            super.registerContribution(contribution, extensionPoint, contributor);
130        }
131    }
132
133    @Override
134    public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
135        if (CONFIGURATION_EP.equals(extensionPoint)) {
136            synchronized (this) {
137                descriptors = null;
138            }
139            super.unregisterContribution(contribution, extensionPoint, contributor);
140        }
141    }
142
143    @Override
144    public Map<String, Serializable> getProperties(String namespace) {
145        if (StringUtils.isEmpty(namespace)) {
146            return null;
147        }
148        if (namespace.charAt(namespace.length() - 1) == '.') {
149            throw new IllegalArgumentException("namespace cannot end with a dot");
150        }
151        return getDescriptors().values()
152                               .stream()
153                               .filter(desc -> startsWithNamespace(desc.getName(), namespace))
154                               .collect(Collectors.toMap(desc -> desc.getId().substring(namespace.length() + 1),
155                                       desc -> desc.getValue() != null && desc.list
156                                               ? desc.getValue().split(LIST_SEPARATOR)
157                                               : desc.getValue()));
158    }
159
160    @Override
161    public String getPropertiesAsJson(String namespace) throws IOException {
162        // Build properties with indexes for lists
163        Properties properties = new Properties();
164        getProperties(namespace).forEach((key, value) -> {
165            if (value instanceof String[]) {
166                int idx = 1;
167                for (String v : (String[]) value) {
168                    properties.put(String.format("%s.%d", key, idx++), v);
169                }
170            } else {
171                properties.put(key, value);
172            }
173        });
174        return OBJECT_MAPPER.writer()
175                            .writeValueAsString(PROPERTIES_MAPPER.readPropertiesAs(properties, ObjectNode.class));
176    }
177
178    /**
179     * Returns true if a string starts with a namespace.
180     *
181     * @param string a string
182     * @since 10.3
183     */
184    protected static boolean startsWithNamespace(String string, String namespace) {
185        int nl = namespace.length();
186        return string.length() > nl && string.charAt(nl) == '.' && string.startsWith(namespace);
187    }
188
189    /**
190     * @since 11.1
191     */
192    @Override
193    public Optional<String> getString(String key) {
194        return Optional.ofNullable(getDescriptors().get(key))
195                       .map(ConfigurationPropertyDescriptor::getValue)
196                       .filter(StringUtils::isNotBlank);
197    }
198
199    /**
200     * @since 11.1
201     */
202    @Override
203    public String getString(String key, String defaultValue) {
204        return getString(key).orElse(defaultValue);
205    }
206
207    /**
208     * @since 11.1
209     */
210    @Override
211    public Optional<Integer> getInteger(String key) {
212        return getString(key).map(value -> {
213            try {
214                return Integer.valueOf(value);
215            } catch (NumberFormatException e) {
216                log.error("Invalid configuration property '{}', '{}' should be a number", key, value, e);
217                return null;
218            }
219        });
220    }
221
222    /**
223     * @since 11.1
224     */
225    @Override
226    public int getInteger(String key, int defaultValue) {
227        return getInteger(key).orElse(defaultValue);
228    }
229
230    /**
231     * @since 11.1
232     */
233    @Override
234    public Optional<Long> getLong(String key) {
235        return getString(key).map(value -> {
236            try {
237                return Long.valueOf(value);
238            } catch (NumberFormatException e) {
239                log.error("Invalid configuration property '{}', '{}' should be a number", key, value, e);
240                return null;
241            }
242        });
243    }
244
245    /**
246     * @since 11.1
247     */
248    @Override
249    public long getLong(String key, long defaultValue) {
250        return getLong(key).orElse(defaultValue);
251    }
252
253    /**
254     * @since 11.1
255     */
256    @Override
257    public Optional<Boolean> getBoolean(String key) {
258        return getString(key).map(value -> {
259            // don't use Boolean.parseBoolean because we want to enforce typing
260            if ("true".equalsIgnoreCase(value)) {
261                return TRUE;
262            } else if ("false".equalsIgnoreCase(value)) {
263                return FALSE;
264            } else {
265                log.error("Invalid configuration property '{}', '{}' should be a boolean", key, value);
266                return null;
267            }
268        });
269    }
270
271    /**
272     * @since 11.1
273     */
274    @Override
275    public boolean isBooleanTrue(String key) {
276        return getBoolean(key).filter(TRUE::equals).orElse(FALSE);
277    }
278
279    /**
280     * @since 11.1
281     */
282    @Override
283    public boolean isBooleanFalse(String key) {
284        // inverse value as we're getting FALSE
285        return getBoolean(key).filter(FALSE::equals).map(value -> !value).orElse(FALSE);
286    }
287
288    /**
289     * @since 11.1
290     */
291    @Override
292    public Optional<Duration> getDuration(String key) {
293        return getString(key).map(value -> {
294            try {
295                return DurationUtils.parse(value);
296            } catch (DateTimeParseException e) {
297                log.error("Invalid configuration property '{}', '{}' should be a duration", key, value, e);
298                return null;
299            }
300        });
301    }
302
303    /**
304     * @since 11.1
305     */
306    @Override
307    public Duration getDuration(String key, Duration defaultValue) {
308        return getDuration(key).orElse(defaultValue);
309    }
310
311}