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.util.Collection;
27 import java.util.LinkedHashMap;
28 import java.util.List;
29 import java.util.Locale;
31 import java.util.Optional;
33 import javax.annotation.Nullable;
34 import org.sonar.api.profiles.ProfileDefinition;
35 import org.sonar.api.profiles.RulesProfile;
36 import org.sonar.api.resources.Languages;
37 import org.sonar.api.utils.ValidationMessages;
38 import org.sonar.api.utils.log.Logger;
39 import org.sonar.api.utils.log.Loggers;
40 import org.sonar.api.utils.log.Profiler;
41 import org.sonar.core.util.stream.MoreCollectors;
43 import static com.google.common.base.Preconditions.checkArgument;
44 import static com.google.common.base.Preconditions.checkState;
45 import static java.lang.String.format;
46 import static org.apache.commons.lang.StringUtils.isNotEmpty;
47 import static org.apache.commons.lang.StringUtils.lowerCase;
49 public class BuiltInQProfileRepositoryImpl implements BuiltInQProfileRepository {
50 private static final Logger LOGGER = Loggers.get(BuiltInQProfileRepositoryImpl.class);
51 private static final String DEFAULT_PROFILE_NAME = "Sonar way";
53 private final Languages languages;
54 private final List<ProfileDefinition> definitions;
55 private List<BuiltInQProfile> qProfiles;
58 * Requires for pico container when no {@link ProfileDefinition} is defined at all
60 public BuiltInQProfileRepositoryImpl(Languages languages) {
61 this(languages, new ProfileDefinition[0]);
64 public BuiltInQProfileRepositoryImpl(Languages languages, ProfileDefinition... definitions) {
65 this.languages = languages;
66 this.definitions = ImmutableList.copyOf(definitions);
70 public void initialize() {
71 checkState(qProfiles == null, "initialize must be called only once");
73 Profiler profiler = Profiler.create(Loggers.get(getClass())).startInfo("Load quality profiles");
74 ListMultimap<String, RulesProfile> rulesProfilesByLanguage = buildRulesProfilesByLanguage();
75 validateAndClean(rulesProfilesByLanguage);
76 this.qProfiles = toFlatList(rulesProfilesByLanguage);
81 public List<BuiltInQProfile> get() {
82 checkState(qProfiles != null, "initialize must be called first");
88 * @return profiles by language
90 private ListMultimap<String, RulesProfile> buildRulesProfilesByLanguage() {
91 ListMultimap<String, RulesProfile> byLang = ArrayListMultimap.create();
92 Profiler profiler = Profiler.create(Loggers.get(getClass()));
93 for (ProfileDefinition definition : definitions) {
95 ValidationMessages validation = ValidationMessages.create();
96 RulesProfile profile = definition.createProfile(validation);
97 validation.log(LOGGER);
98 if (profile == null) {
99 profiler.stopDebug(format("Loaded definition %s that return no profile", definition));
101 if (!validation.hasErrors()) {
102 checkArgument(isNotEmpty(profile.getName()), "Profile created by Definition %s can't have a blank name", definition);
103 byLang.put(lowerCase(profile.getLanguage(), Locale.ENGLISH), profile);
105 profiler.stopDebug(format("Loaded definition %s for language %s", profile.getName(), profile.getLanguage()));
111 private void validateAndClean(ListMultimap<String, RulesProfile> byLang) {
112 byLang.asMap().entrySet()
114 String language = entry.getKey();
115 if (languages.get(language) == null) {
116 LOGGER.info("Language {} is not installed, related Quality profiles are ignored", language);
119 Collection<RulesProfile> profiles = entry.getValue();
120 if (profiles.isEmpty()) {
121 LOGGER.warn("No Quality profiles defined for language: {}", language);
128 private static List<BuiltInQProfile> toFlatList(ListMultimap<String, RulesProfile> rulesProfilesByLanguage) {
129 Map<String, List<BuiltInQProfile.Builder>> buildersByLanguage = Multimaps.asMap(rulesProfilesByLanguage)
132 .collect(MoreCollectors.uniqueIndex(Map.Entry::getKey, BuiltInQProfileRepositoryImpl::toQualityProfileBuilders));
133 return buildersByLanguage
136 .filter(BuiltInQProfileRepositoryImpl::ensureAtMostOneDeclaredDefault)
137 .map(entry -> toQualityProfiles(entry.getValue()))
138 .flatMap(Collection::stream)
139 .collect(MoreCollectors.toList());
143 * Creates {@link BuiltInQProfile.Builder} for each unique quality profile name for a given language.
144 * Builders will have the following properties populated:
146 * <li>{@link BuiltInQProfile.Builder#language language}: key of the method's parameter</li>
147 * <li>{@link BuiltInQProfile.Builder#name name}: {@link RulesProfile#getName()}</li>
148 * <li>{@link BuiltInQProfile.Builder#declaredDefault declaredDefault}: {@code true} if at least one RulesProfile
149 * with a given name has {@link RulesProfile#getDefaultProfile()} is {@code true}</li>
150 * <li>{@link BuiltInQProfile.Builder#activeRules activeRules}: the concatenate of the active rules of all
151 * RulesProfile with a given name</li>
154 private static List<BuiltInQProfile.Builder> toQualityProfileBuilders(Map.Entry<String, List<RulesProfile>> rulesProfilesByLanguage) {
155 String language = rulesProfilesByLanguage.getKey();
156 // use a LinkedHashMap to keep order of insertion of RulesProfiles
157 Map<String, BuiltInQProfile.Builder> qualityProfileBuildersByName = new LinkedHashMap<>();
158 for (RulesProfile rulesProfile : rulesProfilesByLanguage.getValue()) {
159 qualityProfileBuildersByName.compute(
160 rulesProfile.getName(),
161 (name, existingBuilder) -> updateOrCreateBuilder(language, existingBuilder, rulesProfile));
163 return ImmutableList.copyOf(qualityProfileBuildersByName.values());
167 * Fails if more than one {@link BuiltInQProfile.Builder#declaredDefault} is {@code true}, otherwise returns {@code true}.
169 private static boolean ensureAtMostOneDeclaredDefault(Map.Entry<String, List<BuiltInQProfile.Builder>> entry) {
170 Set<String> declaredDefaultProfileNames = entry.getValue().stream()
171 .filter(BuiltInQProfile.Builder::isDeclaredDefault)
172 .map(BuiltInQProfile.Builder::getName)
173 .collect(MoreCollectors.toSet());
174 checkState(declaredDefaultProfileNames.size() <= 1, "Several Quality profiles are flagged as default for the language %s: %s", entry.getKey(), declaredDefaultProfileNames);
178 private static BuiltInQProfile.Builder updateOrCreateBuilder(String language, @Nullable BuiltInQProfile.Builder existingBuilder, RulesProfile rulesProfile) {
179 BuiltInQProfile.Builder builder = existingBuilder;
180 if (builder == null) {
181 builder = new BuiltInQProfile.Builder()
182 .setLanguage(language)
183 .setName(rulesProfile.getName());
185 Boolean defaultProfile = rulesProfile.getDefaultProfile();
186 boolean declaredDefault = defaultProfile != null && defaultProfile;
188 // if there is multiple RulesProfiles with the same name, if at least one is declared default,
189 // then QualityProfile is flagged as declared default
190 .setDeclaredDefault(builder.isDeclaredDefault() || declaredDefault)
191 .addRules(rulesProfile.getActiveRules());
194 private static List<BuiltInQProfile> toQualityProfiles(List<BuiltInQProfile.Builder> builders) {
195 if (builders.stream().noneMatch(BuiltInQProfile.Builder::isDeclaredDefault)) {
196 Optional<BuiltInQProfile.Builder> sonarWayProfile = builders.stream().filter(builder -> builder.getName().equals(DEFAULT_PROFILE_NAME)).findFirst();
197 if (sonarWayProfile.isPresent()) {
198 sonarWayProfile.get().setComputedDefault(true);
200 builders.iterator().next().setComputedDefault(true);
203 return builders.stream()
204 .map(BuiltInQProfile.Builder::build)
205 .collect(MoreCollectors.toList(builders.size()));