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 <T extends Descriptor> Map<String, T> 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}