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