001 /*
002 * SonarQube, open source software quality management tool.
003 * Copyright (C) 2008-2014 SonarSource
004 * mailto:contact AT sonarsource DOT com
005 *
006 * SonarQube is free software; you can redistribute it and/or
007 * modify it under the terms of the GNU Lesser General Public
008 * License as published by the Free Software Foundation; either
009 * version 3 of the License, or (at your option) any later version.
010 *
011 * SonarQube is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014 * Lesser General Public License for more details.
015 *
016 * You should have received a copy of the GNU Lesser General Public License
017 * along with this program; if not, write to the Free Software Foundation,
018 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
019 */
020 package org.sonar.api.config;
021
022 import com.google.common.base.Joiner;
023 import com.google.common.base.Splitter;
024 import com.google.common.base.Strings;
025 import com.google.common.collect.ImmutableMap;
026 import com.google.common.collect.Lists;
027 import com.google.common.collect.Maps;
028 import org.apache.commons.lang.ArrayUtils;
029 import org.apache.commons.lang.StringUtils;
030 import org.sonar.api.BatchComponent;
031 import org.sonar.api.ServerComponent;
032 import org.sonar.api.utils.DateUtils;
033
034 import javax.annotation.Nullable;
035
036 import java.util.Date;
037 import java.util.List;
038 import java.util.Map;
039 import java.util.Properties;
040
041 /**
042 * Project settings on batch side, or global settings on server side. This component does not access to database, so
043 * property changed via setter methods are not persisted.
044 * <p/>
045 * <p>
046 * For testing, you can create a new empty {@link Settings} component using {@link #Settings()} and then
047 * populate it using all variant of {@code setProperty}. <br/>
048 * If you want to test with default values of your properties taken into account there are two ways dependening on how you declare your properties.
049 * <ul>
050 * <li>If you are using annotations like:
051 * <pre>
052 * <code>{@literal @}Properties({
053 * {@literal @}Property(
054 * key = "sonar.myProp",
055 * defaultValue = "A default value",
056 * name = "My property"),
057 * })
058 * public class MyPlugin extends SonarPlugin {
059 * </code>
060 * </pre>
061 * then you can use:
062 * <pre>
063 * <code>new Settings(new PropertyDefinitions(MyPlugin.class))
064 * </code>
065 * </pre>
066 * </li>
067 * <li>If you are using the {@link PropertyDefinition#builder(String)} way like:
068 * <pre>
069 * <code>
070 * public class MyPlugin extends SonarPlugin {
071 * public List getExtensions() {
072 * return Arrays.asList(
073 * PropertyDefinition.builder("sonar.myProp").name("My property").defaultValue("A default value").build()
074 * );
075 * }
076 * }
077 * </code>
078 * </pre>
079 * then you can use:
080 * <pre>
081 * <code>new Settings(new PropertyDefinitions(new MyPlugin().getExtensions()))
082 * </code>
083 * </pre>
084 * </li>
085 * </p>
086 * @since 2.12
087 */
088 public class Settings implements BatchComponent, ServerComponent {
089
090 protected Map<String, String> properties;
091 protected PropertyDefinitions definitions;
092 private Encryption encryption;
093
094 public Settings() {
095 this(new PropertyDefinitions());
096 }
097
098 public Settings(PropertyDefinitions definitions) {
099 this.properties = Maps.newHashMap();
100 this.definitions = definitions;
101 this.encryption = new Encryption(null);
102 }
103
104 /**
105 * Clone settings. Actions are not propagated to cloned settings.
106 *
107 * @since 3.1
108 */
109 public Settings(Settings other) {
110 this.properties = Maps.newHashMap(other.properties);
111 this.definitions = other.definitions;
112 this.encryption = other.encryption;
113 }
114
115 public Encryption getEncryption() {
116 return encryption;
117 }
118
119 public String getDefaultValue(String key) {
120 return definitions.getDefaultValue(key);
121 }
122
123 public boolean hasKey(String key) {
124 return properties.containsKey(key);
125 }
126
127 public boolean hasDefaultValue(String key) {
128 return StringUtils.isNotEmpty(getDefaultValue(key));
129 }
130
131 public String getString(String key) {
132 String value = getClearString(key);
133 if (value != null && encryption.isEncrypted(value)) {
134 try {
135 value = encryption.decrypt(value);
136 } catch (Exception e) {
137 throw new IllegalStateException("Fail to decrypt the property " + key + ". Please check your secret key.", e);
138 }
139 }
140 return value;
141 }
142
143 /**
144 * Does not decrypt value.
145 */
146 protected String getClearString(String key) {
147 doOnGetProperties(key);
148 String validKey = definitions.validKey(key);
149 String value = properties.get(validKey);
150 if (value == null) {
151 value = getDefaultValue(validKey);
152 }
153 return value;
154 }
155
156 public boolean getBoolean(String key) {
157 String value = getString(key);
158 return StringUtils.isNotEmpty(value) && Boolean.parseBoolean(value);
159 }
160
161 /**
162 * @return the value as int. If the property does not exist and has no default value, then 0 is returned.
163 */
164 public int getInt(String key) {
165 String value = getString(key);
166 if (StringUtils.isNotEmpty(value)) {
167 return Integer.parseInt(value);
168 }
169 return 0;
170 }
171
172 public long getLong(String key) {
173 String value = getString(key);
174 if (StringUtils.isNotEmpty(value)) {
175 return Long.parseLong(value);
176 }
177 return 0L;
178 }
179
180 public Date getDate(String key) {
181 String value = getString(key);
182 if (StringUtils.isNotEmpty(value)) {
183 return DateUtils.parseDate(value);
184 }
185 return null;
186 }
187
188 public Date getDateTime(String key) {
189 String value = getString(key);
190 if (StringUtils.isNotEmpty(value)) {
191 return DateUtils.parseDateTime(value);
192 }
193 return null;
194 }
195
196 public Float getFloat(String key) {
197 String value = getString(key);
198 if (StringUtils.isNotEmpty(value)) {
199 try {
200 return Float.valueOf(value);
201 } catch (NumberFormatException e) {
202 throw new IllegalStateException(String.format("The property '%s' is not a float value", key));
203 }
204 }
205 return null;
206 }
207
208 public Double getDouble(String key) {
209 String value = getString(key);
210 if (StringUtils.isNotEmpty(value)) {
211 try {
212 return Double.valueOf(value);
213 } catch (NumberFormatException e) {
214 throw new IllegalStateException(String.format("The property '%s' is not a double value", key));
215 }
216 }
217 return null;
218 }
219
220 /**
221 * Value is split by comma and trimmed. Never returns null.
222 * <p/>
223 * Examples :
224 * <ul>
225 * <li>"one,two,three " -> ["one", "two", "three"]</li>
226 * <li>" one, two, three " -> ["one", "two", "three"]</li>
227 * <li>"one, , three" -> ["one", "", "three"]</li>
228 * </ul>
229 */
230 public String[] getStringArray(String key) {
231 PropertyDefinition property = getDefinitions().get(key);
232 if ((null != property) && (property.multiValues())) {
233 String value = getString(key);
234 if (value == null) {
235 return ArrayUtils.EMPTY_STRING_ARRAY;
236 }
237
238 List<String> values = Lists.newArrayList();
239 for (String v : Splitter.on(",").trimResults().split(value)) {
240 values.add(v.replace("%2C", ","));
241 }
242 return values.toArray(new String[values.size()]);
243 }
244
245 return getStringArrayBySeparator(key, ",");
246 }
247
248 /**
249 * Value is split by carriage returns.
250 *
251 * @return non-null array of lines. The line termination characters are excluded.
252 * @since 3.2
253 */
254 public String[] getStringLines(String key) {
255 String value = getString(key);
256 if (Strings.isNullOrEmpty(value)) {
257 return ArrayUtils.EMPTY_STRING_ARRAY;
258 }
259 return value.split("\r?\n|\r", -1);
260 }
261
262 /**
263 * Value is splitted and trimmed.
264 */
265 public String[] getStringArrayBySeparator(String key, String separator) {
266 String value = getString(key);
267 if (value != null) {
268 String[] strings = StringUtils.splitByWholeSeparator(value, separator);
269 String[] result = new String[strings.length];
270 for (int index = 0; index < strings.length; index++) {
271 result[index] = StringUtils.trim(strings[index]);
272 }
273 return result;
274 }
275 return ArrayUtils.EMPTY_STRING_ARRAY;
276 }
277
278 public List<String> getKeysStartingWith(String prefix) {
279 List<String> result = Lists.newArrayList();
280 for (String key : properties.keySet()) {
281 if (StringUtils.startsWith(key, prefix)) {
282 result.add(key);
283 }
284 }
285 return result;
286 }
287
288 public Settings appendProperty(String key, String value) {
289 String newValue = properties.get(definitions.validKey(key));
290 if (StringUtils.isEmpty(newValue)) {
291 newValue = StringUtils.trim(value);
292 } else {
293 newValue += "," + StringUtils.trim(value);
294 }
295 return setProperty(key, newValue);
296 }
297
298 public Settings setProperty(String key, @Nullable String[] values) {
299 PropertyDefinition property = getDefinitions().get(key);
300 if ((null == property) || (!property.multiValues())) {
301 throw new IllegalStateException("Fail to set multiple values on a single value property " + key);
302 }
303
304 String text = null;
305 if (values != null) {
306 List<String> escaped = Lists.newArrayList();
307 for (String value : values) {
308 if (null != value) {
309 escaped.add(value.replace(",", "%2C"));
310 } else {
311 escaped.add("");
312 }
313 }
314
315 String escapedValue = Joiner.on(',').join(escaped);
316 text = StringUtils.trim(escapedValue);
317 }
318 return setProperty(key, text);
319 }
320
321 public Settings setProperty(String key, @Nullable String value) {
322 String validKey = definitions.validKey(key);
323 if (value == null) {
324 properties.remove(validKey);
325 doOnRemoveProperty(validKey);
326 } else {
327 properties.put(validKey, StringUtils.trim(value));
328 doOnSetProperty(validKey, value);
329 }
330 return this;
331 }
332
333 public Settings setProperty(String key, @Nullable Boolean value) {
334 return setProperty(key, value == null ? null : String.valueOf(value));
335 }
336
337 public Settings setProperty(String key, @Nullable Integer value) {
338 return setProperty(key, value == null ? null : String.valueOf(value));
339 }
340
341 public Settings setProperty(String key, @Nullable Long value) {
342 return setProperty(key, value == null ? null : String.valueOf(value));
343 }
344
345 public Settings setProperty(String key, @Nullable Double value) {
346 return setProperty(key, value == null ? null : String.valueOf(value));
347 }
348
349 public Settings setProperty(String key, @Nullable Float value) {
350 return setProperty(key, value == null ? null : String.valueOf(value));
351 }
352
353 public Settings setProperty(String key, @Nullable Date date) {
354 return setProperty(key, date, false);
355 }
356
357 public Settings addProperties(Map<String, String> props) {
358 for (Map.Entry<String, String> entry : props.entrySet()) {
359 setProperty(entry.getKey(), entry.getValue());
360 }
361 return this;
362 }
363
364 public Settings addProperties(Properties props) {
365 for (Map.Entry<Object, Object> entry : props.entrySet()) {
366 setProperty(entry.getKey().toString(), entry.getValue().toString());
367 }
368 return this;
369 }
370
371 /**
372 * @deprecated since 4.4 For embedding purpose all properties should be provided by the bootstrapper
373 */
374 @Deprecated
375 public Settings addSystemProperties() {
376 return addProperties(System.getProperties());
377 }
378
379 /**
380 * @deprecated since 4.4 For embedding purpose all properties should be provided by the bootstrapper
381 */
382 @Deprecated
383 public Settings addEnvironmentVariables() {
384 return addProperties(System.getenv());
385 }
386
387 public Settings setProperties(Map<String, String> props) {
388 clear();
389 return addProperties(props);
390 }
391
392 public Settings setProperty(String key, @Nullable Date date, boolean includeTime) {
393 if (date == null) {
394 return removeProperty(key);
395 }
396 return setProperty(key, includeTime ? DateUtils.formatDateTime(date) : DateUtils.formatDate(date));
397 }
398
399 public Settings removeProperty(String key) {
400 return setProperty(key, (String) null);
401 }
402
403 public Settings clear() {
404 properties.clear();
405 doOnClearProperties();
406 return this;
407 }
408
409 /**
410 * @return immutable properties
411 */
412 public Map<String, String> getProperties() {
413 return ImmutableMap.copyOf(properties);
414 }
415
416 public PropertyDefinitions getDefinitions() {
417 return definitions;
418 }
419
420 /**
421 * Create empty settings. Definition of available properties is loaded from the given annotated class.
422 * This method is usually used by unit tests.
423 */
424 public static Settings createForComponent(Object component) {
425 return new Settings(new PropertyDefinitions(component));
426 }
427
428 protected void doOnSetProperty(String key, @Nullable String value) {
429 // can be overridden
430 }
431
432 protected void doOnRemoveProperty(String key) {
433 // can be overridden
434 }
435
436 protected void doOnClearProperties() {
437 // can be overridden
438 }
439
440 protected void doOnGetProperties(String key) {
441 // can be overridden
442 }
443 }