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.Lists;
027import com.google.common.collect.Maps;
028import org.apache.commons.lang.ArrayUtils;
029import org.apache.commons.lang.StringUtils;
030import org.sonar.api.BatchComponent;
031import org.sonar.api.ServerComponent;
032import org.sonar.api.utils.DateUtils;
033
034import javax.annotation.Nullable;
035
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 */
088public 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}