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.measures;
021
022import com.google.common.base.Function;
023import com.google.common.collect.SortedMultiset;
024import com.google.common.collect.TreeMultiset;
025import java.util.Arrays;
026import java.util.Collections;
027import java.util.Map;
028import java.util.Set;
029import javax.annotation.Nullable;
030import org.apache.commons.lang.NumberUtils;
031import org.sonar.api.utils.KeyValueFormat;
032
033import static com.google.common.base.Preconditions.checkArgument;
034import static java.util.Objects.requireNonNull;
035
036/**
037 * Utility to build a distribution based on defined ranges
038 * <br>
039 * <p>An example of usage : you wish to record the percentage of lines of code that belong to method
040 * with pre-defined ranges of complexity.
041 *
042 * @since 1.10
043 * @deprecated since 5.2 use {@link org.sonar.api.ce.measure.RangeDistributionBuilder instead}
044 */
045@Deprecated
046public class RangeDistributionBuilder implements MeasureBuilder {
047
048  private final Metric<String> metric;
049  private final SortedMultiset countBag = TreeMultiset.create();
050  private boolean isEmpty = true;
051  private Number[] bottomLimits;
052  private RangeTransformer rangeValueTransformer;
053  private boolean isValid = true;
054
055  /**
056   * RangeDistributionBuilder for a metric and a defined range
057   * Each entry is initialized at zero
058   *
059   * @param metric       the metric to record the measure against
060   * @param bottomLimits the bottom limits of ranges to be used
061   */
062  public RangeDistributionBuilder(Metric<String> metric, Number[] bottomLimits) {
063    requireNonNull(metric, "Metric must not be null");
064    checkArgument(metric.isDataType(), "Metric %s must have data type", metric.key());
065    this.metric = metric;
066    init(bottomLimits);
067  }
068
069  public RangeDistributionBuilder(Metric<String> metric) {
070    this.metric = metric;
071  }
072
073  private void init(Number[] bottomLimits) {
074    this.bottomLimits = new Number[bottomLimits.length];
075    System.arraycopy(bottomLimits, 0, this.bottomLimits, 0, this.bottomLimits.length);
076    Arrays.sort(this.bottomLimits);
077    changeDoublesToInts();
078    doClear();
079    this.rangeValueTransformer = new RangeTransformer();
080  }
081
082  private void changeDoublesToInts() {
083    boolean onlyInts = true;
084    for (Number bottomLimit : bottomLimits) {
085      if (NumberUtils.compare(bottomLimit.intValue(), bottomLimit.doubleValue()) != 0) {
086        onlyInts = false;
087      }
088    }
089    if (onlyInts) {
090      for (int i = 0; i < bottomLimits.length; i++) {
091        bottomLimits[i] = bottomLimits[i].intValue();
092      }
093    }
094  }
095
096  /**
097   * Gives the bottom limits of ranges used
098   *
099   * @return the bottom limits of defined range for the distribution
100   */
101  public Number[] getBottomLimits() {
102    return bottomLimits;
103  }
104
105  /**
106   * Increments an entry by 1
107   *
108   * @param value the value to use to pick the entry to increment
109   * @return the current object
110   */
111  public RangeDistributionBuilder add(Number value) {
112    return add(value, 1);
113  }
114
115  /**
116   * Increments an entry
117   *
118   * @param value the value to use to pick the entry to increment
119   * @param count the number by which to increment
120   * @return the current object
121   */
122  public RangeDistributionBuilder add(@Nullable Number value, int count) {
123    if (value != null && greaterOrEqualsThan(value, bottomLimits[0])) {
124      this.countBag.add(rangeValueTransformer.apply(value), count);
125      isEmpty = false;
126    }
127    return this;
128  }
129
130  private RangeDistributionBuilder addLimitCount(Number limit, int count) {
131    for (Number bottomLimit : bottomLimits) {
132      if (NumberUtils.compare(bottomLimit.doubleValue(), limit.doubleValue()) == 0) {
133        this.countBag.add(rangeValueTransformer.apply(limit), count);
134        isEmpty = false;
135        return this;
136      }
137    }
138    isValid = false;
139    return this;
140  }
141
142  /**
143   * Adds an existing Distribution to the current one.
144   * It will create the entries if they don't exist.
145   * Can be used to add the values of children resources for example
146   * <br>
147   * Since 2.2, the distribution returned will be invalidated in case the
148   * measure given does not use the same bottom limits
149   *
150   * @param measure the measure to add to the current one
151   * @return the current object
152   */
153  public RangeDistributionBuilder add(@Nullable Measure<String> measure) {
154    if (measure != null && measure.getData() != null) {
155      Map<Double, Double> map = KeyValueFormat.parse(measure.getData(), KeyValueFormat.newDoubleConverter(), KeyValueFormat.newDoubleConverter());
156      Number[] limits = map.keySet().toArray(new Number[map.size()]);
157      if (bottomLimits == null) {
158        init(limits);
159
160      } else if (!areSameLimits(bottomLimits, map.keySet())) {
161        isValid = false;
162      }
163
164      if (isValid) {
165        for (Map.Entry<Double, Double> entry : map.entrySet()) {
166          addLimitCount(entry.getKey(), entry.getValue().intValue());
167        }
168      }
169    }
170    return this;
171  }
172
173  private static boolean areSameLimits(Number[] bottomLimits, Set<Double> limits) {
174    if (limits.size() == bottomLimits.length) {
175      for (Number l : bottomLimits) {
176        if (!limits.contains(l.doubleValue())) {
177          return false;
178        }
179      }
180      return true;
181    }
182    return false;
183  }
184
185  /**
186   * Resets all entries to zero
187   *
188   * @return the current object
189   */
190  public RangeDistributionBuilder clear() {
191    doClear();
192    return this;
193  }
194
195  private void doClear() {
196    countBag.clear();
197    if (bottomLimits != null) {
198      Collections.addAll(countBag, bottomLimits);
199    }
200    isEmpty = true;
201  }
202
203  /**
204   * @return whether the current object is empty or not
205   */
206  public boolean isEmpty() {
207    return isEmpty;
208  }
209
210  /**
211   * Shortcut for <code>build(true)</code>
212   *
213   * @return the built measure
214   */
215  @Override
216  public Measure<String> build() {
217    return build(true);
218  }
219
220  /**
221   * Used to build a measure from the current object
222   *
223   * @param allowEmptyData should be built if current object is empty
224   * @return the built measure
225   */
226  public Measure<String> build(boolean allowEmptyData) {
227    if (isValid && (!isEmpty || allowEmptyData)) {
228      return new Measure<>(metric, MultisetDistributionFormat.format(countBag));
229    }
230    return null;
231  }
232
233  private class RangeTransformer implements Function<Number, Number> {
234    @Override
235    public Number apply(Number n) {
236      for (int i = bottomLimits.length - 1; i >= 0; i--) {
237        if (greaterOrEqualsThan(n, bottomLimits[i])) {
238          return bottomLimits[i];
239        }
240      }
241      return null;
242    }
243  }
244
245  private static boolean greaterOrEqualsThan(Number n1, Number n2) {
246    return NumberUtils.compare(n1.doubleValue(), n2.doubleValue()) >= 0;
247  }
248}