3 * Copyright (C) 2009-2017 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 package org.sonar.server.qualityprofile;
22 import com.google.common.collect.ArrayListMultimap;
23 import com.google.common.collect.ImmutableList;
24 import com.google.common.collect.ListMultimap;
25 import com.google.common.collect.Multimaps;
26 import java.security.MessageDigest;
27 import java.util.Collection;
28 import java.util.LinkedHashMap;
29 import java.util.List;
30 import java.util.Locale;
32 import java.util.Optional;
34 import javax.annotation.Nullable;
35 import org.apache.commons.codec.digest.DigestUtils;
36 import org.sonar.api.profiles.ProfileDefinition;
37 import org.sonar.api.profiles.RulesProfile;
38 import org.sonar.api.resources.Languages;
39 import org.sonar.api.utils.ValidationMessages;
40 import org.sonar.api.utils.log.Logger;
41 import org.sonar.api.utils.log.Loggers;
42 import org.sonar.api.utils.log.Profiler;
43 import org.sonar.core.util.stream.MoreCollectors;
45 import static com.google.common.base.Preconditions.checkArgument;
46 import static com.google.common.base.Preconditions.checkState;
47 import static org.apache.commons.lang.StringUtils.isNotEmpty;
48 import static org.apache.commons.lang.StringUtils.lowerCase;
50 public class DefinedQProfileRepositoryImpl implements DefinedQProfileRepository {
51 private static final Logger LOGGER = Loggers.get(DefinedQProfileRepositoryImpl.class);
52 private static final String DEFAULT_PROFILE_NAME = "Sonar way";
54 private final Languages languages;
55 private final List<ProfileDefinition> definitions;
56 private Map<String, List<DefinedQProfile>> qProfilesByLanguage;
59 * Requires for pico container when no {@link ProfileDefinition} is defined at all
61 public DefinedQProfileRepositoryImpl(Languages languages) {
62 this(languages, new ProfileDefinition[0]);
65 public DefinedQProfileRepositoryImpl(Languages languages, ProfileDefinition... definitions) {
66 this.languages = languages;
67 this.definitions = ImmutableList.copyOf(definitions);
71 public void initialize() {
72 checkState(qProfilesByLanguage == null, "initialize must be called only once");
74 Profiler profiler = Profiler.create(Loggers.get(getClass())).startInfo("Load quality profiles");
75 ListMultimap<String, RulesProfile> rulesProfilesByLanguage = buildRulesProfilesByLanguage();
76 validateAndClean(rulesProfilesByLanguage);
77 this.qProfilesByLanguage = toQualityProfilesByLanguage(rulesProfilesByLanguage);
82 public Map<String, List<DefinedQProfile>> getQProfilesByLanguage() {
83 checkState(qProfilesByLanguage != null, "initialize must be called first");
85 return qProfilesByLanguage;
89 * @return profiles by language
91 private ListMultimap<String, RulesProfile> buildRulesProfilesByLanguage() {
92 ListMultimap<String, RulesProfile> byLang = ArrayListMultimap.create();
93 for (ProfileDefinition definition : definitions) {
94 ValidationMessages validation = ValidationMessages.create();
95 RulesProfile profile = definition.createProfile(validation);
96 validation.log(LOGGER);
97 if (profile != null && !validation.hasErrors()) {
98 checkArgument(isNotEmpty(profile.getName()), "Profile created by Definition %s can't have a blank name", definition);
99 byLang.put(lowerCase(profile.getLanguage(), Locale.ENGLISH), profile);
105 private void validateAndClean(ListMultimap<String, RulesProfile> byLang) {
106 byLang.asMap().entrySet()
108 String language = entry.getKey();
109 if (languages.get(language) == null) {
110 LOGGER.info("Language {} is not installed, related Quality profiles are ignored", language);
113 Collection<RulesProfile> profiles = entry.getValue();
114 if (profiles.isEmpty()) {
115 LOGGER.warn("No Quality profiles defined for language: {}", language);
122 private static Map<String, List<DefinedQProfile>> toQualityProfilesByLanguage(ListMultimap<String, RulesProfile> rulesProfilesByLanguage) {
123 Map<String, List<DefinedQProfile.Builder>> buildersByLanguage = Multimaps.asMap(rulesProfilesByLanguage)
126 .collect(MoreCollectors.uniqueIndex(Map.Entry::getKey, DefinedQProfileRepositoryImpl::toQualityProfileBuilders));
127 return buildersByLanguage
130 .filter(DefinedQProfileRepositoryImpl::ensureAtMostOneDeclaredDefault)
131 .collect(MoreCollectors.uniqueIndex(Map.Entry::getKey, entry -> toQualityProfiles(entry.getValue()), buildersByLanguage.size()));
135 * Creates {@link DefinedQProfile.Builder} for each unique quality profile name for a given language.
136 * Builders will have the following properties populated:
138 * <li>{@link DefinedQProfile.Builder#language language}: key of the method's parameter</li>
139 * <li>{@link DefinedQProfile.Builder#name name}: {@link RulesProfile#getName()}</li>
140 * <li>{@link DefinedQProfile.Builder#declaredDefault declaredDefault}: {@code true} if at least one RulesProfile
141 * with a given name has {@link RulesProfile#getDefaultProfile()} is {@code true}</li>
142 * <li>{@link DefinedQProfile.Builder#activeRules activeRules}: the concatenate of the active rules of all
143 * RulesProfile with a given name</li>
146 private static List<DefinedQProfile.Builder> toQualityProfileBuilders(Map.Entry<String, List<RulesProfile>> rulesProfilesByLanguage) {
147 String language = rulesProfilesByLanguage.getKey();
148 // use a LinkedHashMap to keep order of insertion of RulesProfiles
149 Map<String, DefinedQProfile.Builder> qualityProfileBuildersByName = new LinkedHashMap<>();
150 for (RulesProfile rulesProfile : rulesProfilesByLanguage.getValue()) {
151 qualityProfileBuildersByName.compute(
152 rulesProfile.getName(),
153 (name, existingBuilder) -> updateOrCreateBuilder(language, existingBuilder, rulesProfile, name));
155 return ImmutableList.copyOf(qualityProfileBuildersByName.values());
159 * Fails if more than one {@link DefinedQProfile.Builder#declaredDefault} is {@code true}, otherwise returns {@code true}.
161 private static boolean ensureAtMostOneDeclaredDefault(Map.Entry<String, List<DefinedQProfile.Builder>> entry) {
162 Set<String> declaredDefaultProfileNames = entry.getValue().stream()
163 .filter(DefinedQProfile.Builder::isDeclaredDefault)
164 .map(DefinedQProfile.Builder::getName)
165 .collect(MoreCollectors.toSet());
166 checkState(declaredDefaultProfileNames.size() <= 1, "Several Quality profiles are flagged as default for the language %s: %s", entry.getKey(), declaredDefaultProfileNames);
170 private static DefinedQProfile.Builder updateOrCreateBuilder(String language, @Nullable DefinedQProfile.Builder existingBuilder, RulesProfile rulesProfile, String name) {
171 DefinedQProfile.Builder builder = existingBuilder;
172 if (builder == null) {
173 builder = new DefinedQProfile.Builder()
174 .setLanguage(language)
177 Boolean defaultProfile = rulesProfile.getDefaultProfile();
178 boolean declaredDefault = defaultProfile != null && defaultProfile;
180 // if there is multiple RulesProfiles with the same name, if at least one is declared default,
181 // then QualityProfile is flagged as declared default
182 .setDeclaredDefault(builder.isDeclaredDefault() || declaredDefault)
183 .addRules(rulesProfile.getActiveRules());
186 private static List<DefinedQProfile> toQualityProfiles(List<DefinedQProfile.Builder> builders) {
187 if (builders.stream().noneMatch(DefinedQProfile.Builder::isDeclaredDefault)) {
188 Optional<DefinedQProfile.Builder> sonarWayProfile = builders.stream().filter(builder -> builder.getName().equals(DEFAULT_PROFILE_NAME)).findFirst();
189 if (sonarWayProfile.isPresent()) {
190 sonarWayProfile.get().setComputedDefault(true);
192 builders.iterator().next().setComputedDefault(true);
195 MessageDigest md5Digest = DigestUtils.getMd5Digest();
196 return builders.stream()
197 .map(builder -> builder.build(md5Digest))
198 .collect(MoreCollectors.toList(builders.size()));