001/*
002 * SonarQube
003 * Copyright (C) 2009-2017 SonarSource SA
004 * mailto:info 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 java.util.ArrayList;
026import java.util.Date;
027import java.util.List;
028import java.util.Map;
029import java.util.Optional;
030import java.util.Properties;
031import java.util.stream.Collectors;
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.ScannerSide;
037import org.sonar.api.ce.ComputeEngineSide;
038import org.sonar.api.server.ServerSide;
039import org.sonar.api.utils.DateUtils;
040
041import static java.util.Objects.requireNonNull;
042import static org.apache.commons.lang.StringUtils.trim;
043
044/**
045 * Component to get effective settings. Values of properties depend on the runtime environment:
046 * <ul>
047 *   <li>project settings in scanner.</li>
048 *   <li>global settings in web server. It does not allow to get the settings overridden on projects.</li>
049 *   <li>project settings in Compute Engine.</li>
050 * </ul>
051 *
052 * <h3>Usage</h3>
053 * <pre>
054 * public class MyExtension {
055 *
056 *   private final Settings settings;
057 *
058 *   public MyExtension(Settings settings) {
059 *     this.settings = settings;
060 *   }
061 *   public void doSomething() {
062 *     String fooValue = settings.getString("sonar.foo");
063 *     // ..
064 *   }
065 * }
066 * </pre>
067 *
068 * <h3>Scanner example</h3>
069 * Scanner sensor can get the reference on Settings directly through SensorContext,
070 * without injecting the component into constructor.
071 *
072 * <pre>
073 * public class MySensor implements Sensor {
074 *   {@literal @}Override
075 *   public void execute(SensorContext context) {
076 *     String fooValue = context.settings().getString("sonar.foo");
077 *     // ..
078 *   }
079 * }
080 * </pre>
081 *
082 * <p>
083 * For testing, and only for testing, the in-memory implementation {@link MapSettings} can be used.
084 * <pre>
085 * {@literal @}Test
086 * public void my_test() {
087 *   Settings settings = new MapSettings();
088 *   settings.setProperty("foo", "bar");
089 *   MyExtension underTest = new MyExtension(settings);
090 *   // ...
091 * }
092 * </pre>
093 *
094 * History - this class is abstract since 6.1.
095 *
096 * @see MapSettings
097 * @see PropertyDefinition
098 * @since 2.12
099 */
100@ScannerSide
101@ServerSide
102@ComputeEngineSide
103public abstract class Settings {
104
105  private final PropertyDefinitions definitions;
106  private final Encryption encryption;
107
108  protected Settings(PropertyDefinitions definitions, Encryption encryption) {
109    this.definitions = requireNonNull(definitions);
110    this.encryption = requireNonNull(encryption);
111  }
112
113  protected abstract Optional<String> get(String key);
114
115  protected abstract void set(String key, String value);
116
117  protected abstract void remove(String key);
118
119  /**
120   * Immutable map of the properties that have non-default values.
121   * The default values defined by {@link PropertyDefinitions} are ignored,
122   * so the returned values are not the effective values. Basically only
123   * the non-empty results of {@link #getRawString(String)} are returned.
124   * <p>
125   * Values are not decrypted if they are encrypted with a secret key.
126   * </p>
127   */
128  public abstract Map<String, String> getProperties();
129
130  public Encryption getEncryption() {
131    return encryption;
132  }
133
134  /**
135   * The value that overrides the default value. It
136   * may be encrypted with a secret key. Use {@link #getString(String)} to get
137   * the effective and decrypted value.
138   *
139   * @since 6.1
140   */
141  public Optional<String> getRawString(String key) {
142    return get(definitions.validKey(requireNonNull(key)));
143  }
144
145  /**
146   * All the property definitions declared by core and plugins.
147   */
148  public PropertyDefinitions getDefinitions() {
149    return definitions;
150  }
151
152  /**
153   * The definition related to the specified property. It may
154   * be empty.
155   *
156   * @since 6.1
157   */
158  public Optional<PropertyDefinition> getDefinition(String key) {
159    return Optional.ofNullable(definitions.get(key));
160  }
161
162  /**
163   * @return {@code true} if the property has a non-default value, else {@code false}.
164   */
165  public boolean hasKey(String key) {
166    return getRawString(key).isPresent();
167  }
168
169  @CheckForNull
170  public String getDefaultValue(String key) {
171    return definitions.getDefaultValue(key);
172  }
173
174  public boolean hasDefaultValue(String key) {
175    return StringUtils.isNotEmpty(getDefaultValue(key));
176  }
177
178  /**
179   * The effective value of the specified property. Can return
180   * {@code null} if the property is not set and has no
181   * defined default value.
182   * <p>
183   * If the property is encrypted with a secret key,
184   * then the returned value is decrypted.
185   * </p>
186   *
187   * @throws IllegalStateException if value is encrypted but fails to be decrypted.
188   */
189  @CheckForNull
190  public String getString(String key) {
191    String effectiveKey = definitions.validKey(key);
192    Optional<String> value = getRawString(effectiveKey);
193    if (!value.isPresent()) {
194      // default values cannot be encrypted, so return value as-is.
195      return getDefaultValue(effectiveKey);
196    }
197    if (encryption.isEncrypted(value.get())) {
198      try {
199        return encryption.decrypt(value.get());
200      } catch (Exception e) {
201        throw new IllegalStateException("Fail to decrypt the property " + effectiveKey + ". Please check your secret key.", e);
202      }
203    }
204    return value.get();
205  }
206
207  /**
208   * Effective value as boolean. It is {@code false} if {@link #getString(String)}
209   * does not return {@code "true"}, even if it's not a boolean representation.
210   * @return {@code true} if the effective value is {@code "true"}, else {@code false}.
211   */
212  public boolean getBoolean(String key) {
213    String value = getString(key);
214    return StringUtils.isNotEmpty(value) && Boolean.parseBoolean(value);
215  }
216
217  /**
218   * Effective value as {@code int}.
219   * @return the value as {@code int}. If the property does not have value nor default value, then {@code 0} is returned.
220   * @throws NumberFormatException if value is not empty and is not a parsable integer
221   */
222  public int getInt(String key) {
223    String value = getString(key);
224    if (StringUtils.isNotEmpty(value)) {
225      return Integer.parseInt(value);
226    }
227    return 0;
228  }
229
230  /**
231   * Effective value as {@code long}.
232   * @return the value as {@code long}. If the property does not have value nor default value, then {@code 0L} is returned.
233   * @throws NumberFormatException if value is not empty and is not a parsable {@code long}
234   */
235  public long getLong(String key) {
236    String value = getString(key);
237    if (StringUtils.isNotEmpty(value)) {
238      return Long.parseLong(value);
239    }
240    return 0L;
241  }
242
243  /**
244   * Effective value as {@link Date}, without time fields. Format is {@link DateUtils#DATE_FORMAT}.
245   *
246   * @return the value as a {@link Date}. If the property does not have value nor default value, then {@code null} is returned.
247   * @throws RuntimeException if value is not empty and is not in accordance with {@link DateUtils#DATE_FORMAT}.
248   */
249  @CheckForNull
250  public Date getDate(String key) {
251    String value = getString(key);
252    if (StringUtils.isNotEmpty(value)) {
253      return DateUtils.parseDate(value);
254    }
255    return null;
256  }
257
258  /**
259   * Effective value as {@link Date}, with time fields. Format is {@link DateUtils#DATETIME_FORMAT}.
260   *
261   * @return the value as a {@link Date}. If the property does not have value nor default value, then {@code null} is returned.
262   * @throws RuntimeException if value is not empty and is not in accordance with {@link DateUtils#DATETIME_FORMAT}.
263   */
264  @CheckForNull
265  public Date getDateTime(String key) {
266    String value = getString(key);
267    if (StringUtils.isNotEmpty(value)) {
268      return DateUtils.parseDateTime(value);
269    }
270    return null;
271  }
272
273  /**
274   * Effective value as {@code Float}.
275   * @return the value as {@code Float}. If the property does not have value nor default value, then {@code null} is returned.
276   * @throws NumberFormatException if value is not empty and is not a parsable number
277   */
278  @CheckForNull
279  public Float getFloat(String key) {
280    String value = getString(key);
281    if (StringUtils.isNotEmpty(value)) {
282      try {
283        return Float.valueOf(value);
284      } catch (NumberFormatException e) {
285        throw new IllegalStateException(String.format("The property '%s' is not a float value", key));
286      }
287    }
288    return null;
289  }
290
291  /**
292   * Effective value as {@code Double}.
293   * @return the value as {@code Double}. If the property does not have value nor default value, then {@code null} is returned.
294   * @throws NumberFormatException if value is not empty and is not a parsable number
295   */
296  @CheckForNull
297  public Double getDouble(String key) {
298    String value = getString(key);
299    if (StringUtils.isNotEmpty(value)) {
300      try {
301        return Double.valueOf(value);
302      } catch (NumberFormatException e) {
303        throw new IllegalStateException(String.format("The property '%s' is not a double value", key));
304      }
305    }
306    return null;
307  }
308
309  /**
310   * Value is split by comma and trimmed. Never returns null.
311   * <br>
312   * Examples :
313   * <ul>
314   * <li>"one,two,three " -&gt; ["one", "two", "three"]</li>
315   * <li>"  one, two, three " -&gt; ["one", "two", "three"]</li>
316   * <li>"one, , three" -&gt; ["one", "", "three"]</li>
317   * </ul>
318   */
319  public String[] getStringArray(String key) {
320    Optional<PropertyDefinition> def = getDefinition(key);
321    if ((def.isPresent()) && (def.get().multiValues())) {
322      String value = getString(key);
323      if (value == null) {
324        return ArrayUtils.EMPTY_STRING_ARRAY;
325      }
326
327      List<String> values = new ArrayList<>();
328      for (String v : Splitter.on(",").trimResults().split(value)) {
329        values.add(v.replace("%2C", ","));
330      }
331      return values.toArray(new String[values.size()]);
332    }
333
334    return getStringArrayBySeparator(key, ",");
335  }
336
337  /**
338   * Value is split by carriage returns.
339   *
340   * @return non-null array of lines. The line termination characters are excluded.
341   * @since 3.2
342   */
343  public String[] getStringLines(String key) {
344    String value = getString(key);
345    if (Strings.isNullOrEmpty(value)) {
346      return ArrayUtils.EMPTY_STRING_ARRAY;
347    }
348    return value.split("\r?\n|\r", -1);
349  }
350
351  /**
352   * Value is split and trimmed.
353   */
354  public String[] getStringArrayBySeparator(String key, String separator) {
355    String value = getString(key);
356    if (value != null) {
357      String[] strings = StringUtils.splitByWholeSeparator(value, separator);
358      String[] result = new String[strings.length];
359      for (int index = 0; index < strings.length; index++) {
360        result[index] = trim(strings[index]);
361      }
362      return result;
363    }
364    return ArrayUtils.EMPTY_STRING_ARRAY;
365  }
366
367  public Settings appendProperty(String key, @Nullable String value) {
368    Optional<String> existingValue = getRawString(definitions.validKey(key));
369    String newValue;
370    if (!existingValue.isPresent()) {
371      newValue = trim(value);
372    } else {
373      newValue = existingValue.get() + "," + trim(value);
374    }
375    return setProperty(key, newValue);
376  }
377
378  public Settings setProperty(String key, @Nullable String[] values) {
379    Optional<PropertyDefinition> def = getDefinition(key);
380    if (!def.isPresent() || (!def.get().multiValues())) {
381      throw new IllegalStateException("Fail to set multiple values on a single value property " + key);
382    }
383
384    String text = null;
385    if (values != null) {
386      List<String> escaped = new ArrayList<>();
387      for (String value : values) {
388        if (null != value) {
389          escaped.add(value.replace(",", "%2C"));
390        } else {
391          escaped.add("");
392        }
393      }
394
395      String escapedValue = Joiner.on(',').join(escaped);
396      text = trim(escapedValue);
397    }
398    return setProperty(key, text);
399  }
400
401  /**
402   * Change a property value in a restricted scope only, depending on execution context. New value
403   * is <b>never</b> persisted. New value is ephemeral and kept in memory only:
404   * <ul>
405   *   <li>during current analysis in the case of scanner stack</li>
406   *   <li>during processing of current HTTP request in the case of web server stack</li>
407   *   <li>during execution of current task in the case of Compute Engine stack</li>
408   * </ul>
409   *
410   * Property is temporarily removed if the parameter {@code value} is {@code null}
411   */
412  public Settings setProperty(String key, @Nullable String value) {
413    String validKey = definitions.validKey(key);
414    if (value == null) {
415      removeProperty(validKey);
416
417    } else {
418      set(validKey, trim(value));
419    }
420    return this;
421  }
422
423  /**
424   * @see #setProperty(String, String)
425   */
426  public Settings setProperty(String key, @Nullable Boolean value) {
427    return setProperty(key, value == null ? null : String.valueOf(value));
428  }
429
430  /**
431   * @see #setProperty(String, String)
432   */
433  public Settings setProperty(String key, @Nullable Integer value) {
434    return setProperty(key, value == null ? null : String.valueOf(value));
435  }
436
437  /**
438   * @see #setProperty(String, String)
439   */
440  public Settings setProperty(String key, @Nullable Long value) {
441    return setProperty(key, value == null ? null : String.valueOf(value));
442  }
443
444  /**
445   * @see #setProperty(String, String)
446   */
447  public Settings setProperty(String key, @Nullable Double value) {
448    return setProperty(key, value == null ? null : String.valueOf(value));
449  }
450
451  /**
452   * @see #setProperty(String, String)
453   */
454  public Settings setProperty(String key, @Nullable Float value) {
455    return setProperty(key, value == null ? null : String.valueOf(value));
456  }
457
458  /**
459   * @see #setProperty(String, String)
460   */
461  public Settings setProperty(String key, @Nullable Date date) {
462    return setProperty(key, date, false);
463  }
464
465  public Settings addProperties(Map<String, String> props) {
466    for (Map.Entry<String, String> entry : props.entrySet()) {
467      setProperty(entry.getKey(), entry.getValue());
468    }
469    return this;
470  }
471
472  public Settings addProperties(Properties props) {
473    for (Map.Entry<Object, Object> entry : props.entrySet()) {
474      setProperty(entry.getKey().toString(), entry.getValue().toString());
475    }
476    return this;
477  }
478
479  /**
480   * @see #setProperty(String, String)
481   */
482  public Settings setProperty(String key, @Nullable Date date, boolean includeTime) {
483    if (date == null) {
484      return removeProperty(key);
485    }
486    return setProperty(key, includeTime ? DateUtils.formatDateTime(date) : DateUtils.formatDate(date));
487  }
488
489  public Settings removeProperty(String key) {
490    remove(key);
491    return this;
492  }
493
494  public List<String> getKeysStartingWith(String prefix) {
495    return getProperties().keySet().stream()
496      .filter(key -> StringUtils.startsWith(key, prefix))
497      .collect(Collectors.toList());
498  }
499
500}