001/*
002 * Sonar, open source software quality management tool.
003 * Copyright (C) 2008-2012 SonarSource
004 * mailto:contact AT sonarsource DOT com
005 *
006 * Sonar 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 * Sonar 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
017 * License along with Sonar; if not, write to the Free Software
018 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
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.Lists;
026import com.google.common.collect.Maps;
027import org.apache.commons.lang.ArrayUtils;
028import org.apache.commons.lang.StringUtils;
029import org.sonar.api.BatchComponent;
030import org.sonar.api.ServerComponent;
031import org.sonar.api.utils.DateUtils;
032
033import javax.annotation.Nullable;
034
035import java.util.Collections;
036import java.util.Date;
037import java.util.List;
038import java.util.Map;
039import java.util.Properties;
040
041/**
042 * Project Settings on batch side, 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 * This component replaces the deprecated org.apache.commons.configuration.Configuration
047 * </p>
048 *
049 * @since 2.12
050 */
051public class Settings implements BatchComponent, ServerComponent {
052
053  protected final Map<String, String> properties;
054  protected final PropertyDefinitions definitions;
055  private final Encryption encryption;
056
057  public Settings() {
058    this(new PropertyDefinitions());
059  }
060
061  public Settings(PropertyDefinitions definitions) {
062    this.properties = Maps.newHashMap();
063    this.definitions = definitions;
064    this.encryption = new Encryption(this);
065  }
066
067  /**
068   * Clone settings. Actions are not propagated to cloned settings.
069   *
070   * @since 3.1
071   */
072  public Settings(Settings other) {
073    this.properties = Maps.newHashMap(other.properties);
074    this.definitions = other.definitions;
075    this.encryption = other.encryption;
076  }
077
078  public final Encryption getEncryption() {
079    return encryption;
080  }
081
082  public final String getDefaultValue(String key) {
083    return definitions.getDefaultValue(key);
084  }
085
086  public final boolean hasKey(String key) {
087    return properties.containsKey(key);
088  }
089
090  public final boolean hasDefaultValue(String key) {
091    return StringUtils.isNotEmpty(getDefaultValue(key));
092  }
093
094  public final String getString(String key) {
095    String value = properties.get(key);
096    if (value == null) {
097      value = getDefaultValue(key);
098    } else if (encryption.isEncrypted(value)) {
099      try {
100        value = encryption.decrypt(value);
101      } catch (Exception e) {
102        throw new IllegalStateException("Fail to decrypt the property " + key + ". Please check your secret key.", e);
103      }
104    }
105    return value;
106  }
107
108  /**
109   * Does not decrypt value.
110   */
111  protected String getClearString(String key) {
112    String value = properties.get(key);
113    if (value == null) {
114      value = getDefaultValue(key);
115    }
116    return value;
117  }
118
119  public final boolean getBoolean(String key) {
120    String value = getString(key);
121    return StringUtils.isNotEmpty(value) && Boolean.parseBoolean(value);
122  }
123
124  /**
125   * @return the value as int. If the property does not exist and has no default value, then 0 is returned.
126   */
127  public final int getInt(String key) {
128    String value = getString(key);
129    if (StringUtils.isNotEmpty(value)) {
130      return Integer.parseInt(value);
131    }
132    return 0;
133  }
134
135  public final long getLong(String key) {
136    String value = getString(key);
137    if (StringUtils.isNotEmpty(value)) {
138      return Long.parseLong(value);
139    }
140    return 0L;
141  }
142
143  public final Date getDate(String key) {
144    String value = getString(key);
145    if (StringUtils.isNotEmpty(value)) {
146      return DateUtils.parseDate(value);
147    }
148    return null;
149  }
150
151  public final Date getDateTime(String key) {
152    String value = getString(key);
153    if (StringUtils.isNotEmpty(value)) {
154      return DateUtils.parseDateTime(value);
155    }
156    return null;
157  }
158
159  /**
160   * Value is split by comma and trimmed.
161   * <p/>
162   * Examples :
163   * <ul>
164   * <li>"one,two,three " -> ["one", "two", "three"]</li>
165   * <li>"  one, two, three " -> ["one", "two", "three"]</li>
166   * <li>"one, , three" -> ["one", "", "three"]</li>
167   * </ul>
168   */
169  public final String[] getStringArray(String key) {
170    PropertyDefinition property = getDefinitions().get(key);
171    if ((null != property) && (property.isMultiValues())) {
172      String value = getString(key);
173      if (value == null) {
174        return ArrayUtils.EMPTY_STRING_ARRAY;
175      }
176
177      List<String> values = Lists.newArrayList();
178      for (String v : Splitter.on(",").trimResults().split(value)) {
179        values.add(v.replace("%2C", ","));
180      }
181      return values.toArray(new String[values.size()]);
182    }
183
184    return getStringArrayBySeparator(key, ",");
185  }
186
187  /**
188   * Value is split by carriage returns.
189   *
190   * @return non-null array of lines. The line termination characters are excluded.
191   * @since 3.2
192   */
193  public final String[] getStringLines(String key) {
194    String value = getString(key);
195    if (Strings.isNullOrEmpty(value)) {
196      return ArrayUtils.EMPTY_STRING_ARRAY;
197    }
198    return value.split("\r?\n|\r", -1);
199  }
200
201  /**
202   * Value is splitted and trimmed.
203   */
204  public final String[] getStringArrayBySeparator(String key, String separator) {
205    String value = getString(key);
206    if (value != null) {
207      String[] strings = StringUtils.splitByWholeSeparator(value, separator);
208      String[] result = new String[strings.length];
209      for (int index = 0; index < strings.length; index++) {
210        result[index] = StringUtils.trim(strings[index]);
211      }
212      return result;
213    }
214    return ArrayUtils.EMPTY_STRING_ARRAY;
215  }
216
217  public final List<String> getKeysStartingWith(String prefix) {
218    List<String> result = Lists.newArrayList();
219    for (String key : properties.keySet()) {
220      if (StringUtils.startsWith(key, prefix)) {
221        result.add(key);
222      }
223    }
224    return result;
225  }
226
227  public final Settings appendProperty(String key, String value) {
228    String newValue = properties.get(key);
229    if (StringUtils.isEmpty(newValue)) {
230      newValue = StringUtils.trim(value);
231    } else {
232      newValue += "," + StringUtils.trim(value);
233    }
234    return setProperty(key, newValue);
235  }
236
237  public final Settings setProperty(String key, @Nullable String[] values) {
238    PropertyDefinition property = getDefinitions().get(key);
239    if ((null == property) || (!property.isMultiValues())) {
240      throw new IllegalStateException("Fail to set multiple values on a single value property " + key);
241    }
242
243    if (values == null) {
244      properties.remove(key);
245      doOnRemoveProperty(key);
246    } else {
247      List<String> escaped = Lists.newArrayList();
248      for (String value : values) {
249        if (null != value) {
250          escaped.add(value.replace(",", "%2C"));
251        } else {
252          escaped.add("");
253        }
254      }
255
256      String escapedValue = Joiner.on(',').join(escaped);
257      properties.put(key, StringUtils.trim(escapedValue));
258      doOnSetProperty(key, escapedValue);
259    }
260    return this;
261  }
262
263  public final Settings setProperty(String key, @Nullable String value) {
264    if (value == null) {
265      properties.remove(key);
266      doOnRemoveProperty(key);
267    } else {
268      properties.put(key, StringUtils.trim(value));
269      doOnSetProperty(key, value);
270    }
271    return this;
272  }
273
274  public final Settings setProperty(String key, @Nullable Boolean value) {
275    return setProperty(key, String.valueOf(value));
276  }
277
278  public final Settings setProperty(String key, @Nullable Integer value) {
279    return setProperty(key, String.valueOf(value));
280  }
281
282  public final Settings setProperty(String key, @Nullable Long value) {
283    return setProperty(key, String.valueOf(value));
284  }
285
286  public final Settings setProperty(String key, @Nullable Double value) {
287    return setProperty(key, String.valueOf(value));
288  }
289
290  public final Settings setProperty(String key, @Nullable Date date) {
291    return setProperty(key, date, false);
292  }
293
294  public final Settings addProperties(Map<String, String> props) {
295    for (Map.Entry<String, String> entry : props.entrySet()) {
296      setProperty(entry.getKey(), entry.getValue());
297    }
298    return this;
299  }
300
301  public final Settings addProperties(Properties props) {
302    for (Map.Entry<Object, Object> entry : props.entrySet()) {
303      setProperty(entry.getKey().toString(), entry.getValue().toString());
304    }
305    return this;
306  }
307
308  public final Settings addSystemProperties() {
309    return addProperties(System.getProperties());
310  }
311
312  public final Settings addEnvironmentVariables() {
313    return addProperties(System.getenv());
314  }
315
316  public final Settings setProperties(Map<String, String> props) {
317    clear();
318    return addProperties(props);
319  }
320
321  public final Settings setProperty(String key, @Nullable Date date, boolean includeTime) {
322    return setProperty(key, includeTime ? DateUtils.formatDateTime(date) : DateUtils.formatDate(date));
323  }
324
325  public final Settings removeProperty(String key) {
326    return setProperty(key, (String) null);
327  }
328
329  public final Settings clear() {
330    properties.clear();
331    doOnClearProperties();
332    return this;
333  }
334
335  /**
336   * @return unmodifiable properties
337   */
338  public final Map<String, String> getProperties() {
339    return Collections.unmodifiableMap(properties);
340  }
341
342  public final PropertyDefinitions getDefinitions() {
343    return definitions;
344  }
345
346  /**
347   * Create empty settings. Definition of available properties is loaded from the given annotated class.
348   * This method is usually used by unit tests.
349   */
350  public static Settings createForComponent(Object component) {
351    return new Settings(new PropertyDefinitions(component));
352  }
353
354  protected void doOnSetProperty(String key, @Nullable String value) {
355  }
356
357  protected void doOnRemoveProperty(String key) {
358  }
359
360  protected void doOnClearProperties() {
361  }
362}