001/*
002 * (C) Copyright 2015-2017 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 java.io.IOException;
024import java.io.Serializable;
025import java.util.List;
026import java.util.Map;
027import java.util.Properties;
028import java.util.stream.Collectors;
029
030import com.fasterxml.jackson.dataformat.javaprop.JavaPropsMapper;
031import com.fasterxml.jackson.databind.node.ObjectNode;
032import org.apache.commons.lang3.StringUtils;
033import org.apache.commons.logging.Log;
034import org.apache.commons.logging.LogFactory;
035import org.nuxeo.runtime.api.Framework;
036import org.nuxeo.runtime.logging.DeprecationLogger;
037import org.nuxeo.runtime.model.ComponentInstance;
038import org.nuxeo.runtime.model.DefaultComponent;
039
040import com.fasterxml.jackson.databind.ObjectMapper;
041
042/**
043 * @since 7.4
044 */
045public class ConfigurationServiceImpl extends DefaultComponent implements ConfigurationService {
046
047    protected static final Log log = LogFactory.getLog(ConfigurationServiceImpl.class);
048
049    public static final String CONFIGURATION_EP = "configuration";
050
051    protected static final JavaPropsMapper PROPERTIES_MAPPER = new JavaPropsMapper();
052    protected static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
053
054    /**
055     * XXX remove once we are able to get such a cached map from DefaultComponent
056     *
057     * @since 10.3
058     */
059    protected volatile Map<String, ConfigurationPropertyDescriptor> descriptors;
060
061    /**
062     * XXX remove once we are able to get such a cached map from DefaultComponent.
063     * <p>
064     * We'd ideally need a <T extends Descriptor> Map<String, T> getDescriptors(String xp) with cache method.
065     *
066     * @since 10.3
067     */
068    protected Map<String, ConfigurationPropertyDescriptor> getDescriptors() {
069        Map<String, ConfigurationPropertyDescriptor> d = descriptors;
070        if (d == null) {
071            synchronized (this) {
072                d = descriptors;
073                if (d == null) {
074                    List<ConfigurationPropertyDescriptor> descs = getDescriptors(CONFIGURATION_EP);
075                    descriptors = d = descs.stream().collect(Collectors.toMap(desc -> desc.getId(), desc -> desc));
076                }
077            }
078        }
079        return d;
080    }
081
082    @Override
083    public String getProperty(String key) {
084        return getProperty(key, null);
085    }
086
087    @Override
088    public String getProperty(String key, String defaultValue) {
089        ConfigurationPropertyDescriptor conf = getDescriptors().get(key);
090        if (conf == null) {
091            return defaultValue;
092        }
093        String value = conf.getValue();
094        return value != null ? value : defaultValue;
095    }
096
097    @Override
098    public boolean isBooleanPropertyTrue(String key) {
099        String value = getProperty(key);
100        return Boolean.parseBoolean(value);
101    }
102
103    @Override
104    public boolean isBooleanPropertyFalse(String key) {
105        String value = getProperty(key);
106        return StringUtils.isNotBlank(value) && !Boolean.parseBoolean(value);
107    }
108
109    @Override
110    public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
111        if (CONFIGURATION_EP.equals(extensionPoint)) {
112            synchronized (this) {
113                descriptors = null;
114            }
115            ConfigurationPropertyDescriptor configurationPropertyDescriptor = (ConfigurationPropertyDescriptor) contribution;
116            String key = configurationPropertyDescriptor.getName();
117            if (Framework.getProperties().containsKey(key)) {
118                String message = "Property '" + key + "' should now be contributed to extension "
119                        + "point 'org.nuxeo.runtime.ConfigurationService', using target 'configuration'";
120                DeprecationLogger.log(message, "7.4");
121                Framework.getRuntime().getMessageHandler().addWarning(message);
122            }
123            super.registerContribution(contribution, extensionPoint, contributor);
124        }
125    }
126
127    @Override
128    public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
129        if (CONFIGURATION_EP.equals(extensionPoint)) {
130            synchronized (this) {
131                descriptors = null;
132            }
133            super.unregisterContribution(contribution, extensionPoint, contributor);
134        }
135    }
136
137    @Override
138    public Map<String, Serializable> getProperties(String namespace) {
139        if (StringUtils.isEmpty(namespace)) {
140            return null;
141        }
142        if (namespace.charAt(namespace.length() - 1) == '.') {
143            throw new IllegalArgumentException("namespace cannot end with a dot");
144        }
145        return getDescriptors().values()
146                               .stream()
147                               .filter(desc -> startsWithNamespace(desc.getName(), namespace))
148                               .collect(Collectors.toMap(desc -> desc.getId().substring(namespace.length() + 1),
149                                       desc -> desc.getValue() != null && desc.list
150                                               ? desc.getValue().split(LIST_SEPARATOR) : desc.getValue()));
151    }
152
153    @Override
154    public String getPropertiesAsJson(String namespace) throws IOException {
155        // Build properties with indexes for lists
156        Properties properties = new Properties();
157        getProperties(namespace).forEach((key, value) -> {
158            if (value instanceof String[]) {
159                int idx = 1;
160                for (String v : (String[]) value) {
161                    properties.put(String.format("%s.%d", key, idx++), v);
162                }
163            } else {
164                properties.put(key, value);
165            }
166        });
167        return OBJECT_MAPPER.writer().writeValueAsString(
168                PROPERTIES_MAPPER.readPropertiesAs(properties, ObjectNode.class));
169    }
170
171    /**
172     * Returns true if a string starts with a namespace.
173     *
174     * @param string a string
175     * @param namespace
176     * @since 10.3
177     */
178    protected static boolean startsWithNamespace(String string, String namespace) {
179        int nl = namespace.length();
180        return string.length() > nl && string.charAt(nl) == '.' && string.startsWith(namespace);
181    }
182
183}