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 */
020package org.sonar.api.config;
021
022import com.google.common.base.Joiner;
023import com.google.common.base.Splitter;
024import com.google.common.base.Strings;
025import com.google.common.collect.ImmutableMap;
026import com.google.common.collect.Maps;
027import org.apache.commons.lang.ArrayUtils;
028import org.apache.commons.lang.StringUtils;
029import org.sonar.api.batch.BatchSide;
030import org.sonar.api.server.ServerSide;
031import org.sonar.api.utils.DateUtils;
032
033import javax.annotation.Nullable;
034
035import java.util.ArrayList;
036import java.util.Date;
037import java.util.List;
038import java.util.Map;
039import 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@BatchSide
089@ServerSide
090public class Settings {
091
092  protected Map<String, String> properties;
093  protected PropertyDefinitions definitions;
094  private Encryption encryption;
095
096  public Settings() {
097    this(new PropertyDefinitions());
098  }
099
100  public Settings(PropertyDefinitions definitions) {
101    this.properties = Maps.newHashMap();
102    this.definitions = definitions;
103    this.encryption = new Encryption(null);
104  }
105
106  /**
107   * Clone settings. Actions are not propagated to cloned settings.
108   *
109   * @since 3.1
110   */
111  public Settings(Settings other) {
112    this.properties = Maps.newHashMap(other.properties);
113    this.definitions = other.definitions;
114    this.encryption = other.encryption;
115  }
116
117  public Encryption getEncryption() {
118    return encryption;
119  }
120
121  public String getDefaultValue(String key) {
122    return definitions.getDefaultValue(key);
123  }
124
125  public boolean hasKey(String key) {
126    return properties.containsKey(key);
127  }
128
129  public boolean hasDefaultValue(String key) {
130    return StringUtils.isNotEmpty(getDefaultValue(key));
131  }
132
133  public String getString(String key) {
134    String value = getClearString(key);
135    if (value != null && encryption.isEncrypted(value)) {
136      try {
137        value = encryption.decrypt(value);
138      } catch (Exception e) {
139        throw new IllegalStateException("Fail to decrypt the property " + key + ". Please check your secret key.", e);
140      }
141    }
142    return value;
143  }
144
145  /**
146   * Does not decrypt value.
147   */
148  protected String getClearString(String key) {
149    doOnGetProperties(key);
150    String validKey = definitions.validKey(key);
151    String value = properties.get(validKey);
152    if (value == null) {
153      value = getDefaultValue(validKey);
154    }
155    return value;
156  }
157
158  public boolean getBoolean(String key) {
159    String value = getString(key);
160    return StringUtils.isNotEmpty(value) && Boolean.parseBoolean(value);
161  }
162
163  /**
164   * @return the value as int. If the property does not exist and has no default value, then 0 is returned.
165   */
166  public int getInt(String key) {
167    String value = getString(key);
168    if (StringUtils.isNotEmpty(value)) {
169      return Integer.parseInt(value);
170    }
171    return 0;
172  }
173
174  public long getLong(String key) {
175    String value = getString(key);
176    if (StringUtils.isNotEmpty(value)) {
177      return Long.parseLong(value);
178    }
179    return 0L;
180  }
181
182  public Date getDate(String key) {
183    String value = getString(key);
184    if (StringUtils.isNotEmpty(value)) {
185      return DateUtils.parseDate(value);
186    }
187    return null;
188  }
189
190  public Date getDateTime(String key) {
191    String value = getString(key);
192    if (StringUtils.isNotEmpty(value)) {
193      return DateUtils.parseDateTime(value);
194    }
195    return null;
196  }
197
198  public Float getFloat(String key) {
199    String value = getString(key);
200    if (StringUtils.isNotEmpty(value)) {
201      try {
202        return Float.valueOf(value);
203      } catch (NumberFormatException e) {
204        throw new IllegalStateException(String.format("The property '%s' is not a float value", key));
205      }
206    }
207    return null;
208  }
209
210  public Double getDouble(String key) {
211    String value = getString(key);
212    if (StringUtils.isNotEmpty(value)) {
213      try {
214        return Double.valueOf(value);
215      } catch (NumberFormatException e) {
216        throw new IllegalStateException(String.format("The property '%s' is not a double value", key));
217      }
218    }
219    return null;
220  }
221
222  /**
223   * Value is split by comma and trimmed. Never returns null.
224   * <p/>
225   * Examples :
226   * <ul>
227   * <li>"one,two,three " -> ["one", "two", "three"]</li>
228   * <li>"  one, two, three " -> ["one", "two", "three"]</li>
229   * <li>"one, , three" -> ["one", "", "three"]</li>
230   * </ul>
231   */
232  public String[] getStringArray(String key) {
233    PropertyDefinition property = getDefinitions().get(key);
234    if ((null != property) && (property.multiValues())) {
235      String value = getString(key);
236      if (value == null) {
237        return ArrayUtils.EMPTY_STRING_ARRAY;
238      }
239
240      List<String> values = new ArrayList<>();
241      for (String v : Splitter.on(",").trimResults().split(value)) {
242        values.add(v.replace("%2C", ","));
243      }
244      return values.toArray(new String[values.size()]);
245    }
246
247    return getStringArrayBySeparator(key, ",");
248  }
249
250  /**
251   * Value is split by carriage returns.
252   *
253   * @return non-null array of lines. The line termination characters are excluded.
254   * @since 3.2
255   */
256  public String[] getStringLines(String key) {
257    String value = getString(key);
258    if (Strings.isNullOrEmpty(value)) {
259      return ArrayUtils.EMPTY_STRING_ARRAY;
260    }
261    return value.split("\r?\n|\r", -1);
262  }
263
264  /**
265   * Value is splitted and trimmed.
266   */
267  public String[] getStringArrayBySeparator(String key, String separator) {
268    String value = getString(key);
269    if (value != null) {
270      String[] strings = StringUtils.splitByWholeSeparator(value, separator);
271      String[] result = new String[strings.length];
272      for (int index = 0; index < strings.length; index++) {
273        result[index] = StringUtils.trim(strings[index]);
274      }
275      return result;
276    }
277    return ArrayUtils.EMPTY_STRING_ARRAY;
278  }
279
280  public List<String> getKeysStartingWith(String prefix) {
281    List<String> result = new ArrayList<>();
282    for (String key : properties.keySet()) {
283      if (StringUtils.startsWith(key, prefix)) {
284        result.add(key);
285      }
286    }
287    return result;
288  }
289
290  public Settings appendProperty(String key, String value) {
291    String newValue = properties.get(definitions.validKey(key));
292    if (StringUtils.isEmpty(newValue)) {
293      newValue = StringUtils.trim(value);
294    } else {
295      newValue += "," + StringUtils.trim(value);
296    }
297    return setProperty(key, newValue);
298  }
299
300  public Settings setProperty(String key, @Nullable String[] values) {
301    PropertyDefinition property = getDefinitions().get(key);
302    if ((null == property) || (!property.multiValues())) {
303      throw new IllegalStateException("Fail to set multiple values on a single value property " + key);
304    }
305
306    String text = null;
307    if (values != null) {
308      List<String> escaped = new ArrayList<>();
309      for (String value : values) {
310        if (null != value) {
311          escaped.add(value.replace(",", "%2C"));
312        } else {
313          escaped.add("");
314        }
315      }
316
317      String escapedValue = Joiner.on(',').join(escaped);
318      text = StringUtils.trim(escapedValue);
319    }
320    return setProperty(key, text);
321  }
322
323  public Settings setProperty(String key, @Nullable String value) {
324    String validKey = definitions.validKey(key);
325    if (value == null) {
326      properties.remove(validKey);
327      doOnRemoveProperty(validKey);
328    } else {
329      properties.put(validKey, StringUtils.trim(value));
330      doOnSetProperty(validKey, value);
331    }
332    return this;
333  }
334
335  public Settings setProperty(String key, @Nullable Boolean value) {
336    return setProperty(key, value == null ? null : String.valueOf(value));
337  }
338
339  public Settings setProperty(String key, @Nullable Integer value) {
340    return setProperty(key, value == null ? null : String.valueOf(value));
341  }
342
343  public Settings setProperty(String key, @Nullable Long value) {
344    return setProperty(key, value == null ? null : String.valueOf(value));
345  }
346
347  public Settings setProperty(String key, @Nullable Double value) {
348    return setProperty(key, value == null ? null : String.valueOf(value));
349  }
350
351  public Settings setProperty(String key, @Nullable Float value) {
352    return setProperty(key, value == null ? null : String.valueOf(value));
353  }
354
355  public Settings setProperty(String key, @Nullable Date date) {
356    return setProperty(key, date, false);
357  }
358
359  public Settings addProperties(Map<String, String> props) {
360    for (Map.Entry<String, String> entry : props.entrySet()) {
361      setProperty(entry.getKey(), entry.getValue());
362    }
363    return this;
364  }
365
366  public Settings addProperties(Properties props) {
367    for (Map.Entry<Object, Object> entry : props.entrySet()) {
368      setProperty(entry.getKey().toString(), entry.getValue().toString());
369    }
370    return this;
371  }
372
373  /**
374   * @deprecated since 4.4 For embedding purpose all properties should be provided by the bootstrapper
375   */
376  @Deprecated
377  public Settings addSystemProperties() {
378    return addProperties(System.getProperties());
379  }
380
381  /**
382   * @deprecated since 4.4 For embedding purpose all properties should be provided by the bootstrapper
383   */
384  @Deprecated
385  public Settings addEnvironmentVariables() {
386    return addProperties(System.getenv());
387  }
388
389  public Settings setProperties(Map<String, String> props) {
390    clear();
391    return addProperties(props);
392  }
393
394  public Settings setProperty(String key, @Nullable Date date, boolean includeTime) {
395    if (date == null) {
396      return removeProperty(key);
397    }
398    return setProperty(key, includeTime ? DateUtils.formatDateTime(date) : DateUtils.formatDate(date));
399  }
400
401  public Settings removeProperty(String key) {
402    return setProperty(key, (String) null);
403  }
404
405  public Settings clear() {
406    properties.clear();
407    doOnClearProperties();
408    return this;
409  }
410
411  /**
412   * @return immutable properties
413   */
414  public Map<String, String> getProperties() {
415    return ImmutableMap.copyOf(properties);
416  }
417
418  public PropertyDefinitions getDefinitions() {
419    return definitions;
420  }
421
422  /**
423   * Create empty settings. Definition of available properties is loaded from the given annotated class.
424   * This method is usually used by unit tests.
425   */
426  public static Settings createForComponent(Object component) {
427    return new Settings(new PropertyDefinitions(component));
428  }
429
430  protected void doOnSetProperty(String key, @Nullable String value) {
431    // can be overridden
432  }
433
434  protected void doOnRemoveProperty(String key) {
435    // can be overridden
436  }
437
438  protected void doOnClearProperties() {
439    // can be overridden
440  }
441
442  protected void doOnGetProperties(String key) {
443    // can be overridden
444  }
445}