From 2f2699e356f1a653148d552c56a730c2af0025fe Mon Sep 17 00:00:00 2001 From: Simon Brandhof Date: Wed, 21 Nov 2012 13:37:45 +0100 Subject: [PATCH] SONAR-3825 new page for measure filters --- .../resources/org/sonar/l10n/core.properties | 2 +- .../org/sonar/core/measure/MeasureFilter.java | 97 ++++++-- .../core/measure/MeasureFilterEngine.java | 18 ++ .../sonar/core/measure/MeasureFilterSql.java | 11 +- .../measure/MeasureFilterDecoderTest.java | 6 +- .../java/org/sonar/server/ui/JRubyFacade.java | 14 +- .../app/controllers/measures_controller.rb | 12 +- .../WEB-INF/app/models/measure_filter.rb | 207 ++++++++++++++++++ .../app/views/measures/_display_list.html.erb | 41 ++++ .../views/measures/_display_treemap.html.erb | 0 .../app/views/measures/search.html.erb | 72 ++++++ 11 files changed, 447 insertions(+), 33 deletions(-) create mode 100644 sonar-server/src/main/webapp/WEB-INF/app/models/measure_filter.rb create mode 100644 sonar-server/src/main/webapp/WEB-INF/app/views/measures/_display_list.html.erb create mode 100644 sonar-server/src/main/webapp/WEB-INF/app/views/measures/_display_treemap.html.erb create mode 100644 sonar-server/src/main/webapp/WEB-INF/app/views/measures/search.html.erb diff --git a/plugins/sonar-core-plugin/src/main/resources/org/sonar/l10n/core.properties b/plugins/sonar-core-plugin/src/main/resources/org/sonar/l10n/core.properties index e3e8e475c6b..c58cd5f6dc5 100644 --- a/plugins/sonar-core-plugin/src/main/resources/org/sonar/l10n/core.properties +++ b/plugins/sonar-core-plugin/src/main/resources/org/sonar/l10n/core.properties @@ -403,7 +403,7 @@ filters.display_form.as=Display as filters.display_form.table=Table filters.display_form.treemap=Treemap filters.build_date=Build date -filters.col.date=Build date +filters.col.date=Date filters.col.language=Language filters.col.name=Name filters.col.links=Links diff --git a/sonar-core/src/main/java/org/sonar/core/measure/MeasureFilter.java b/sonar-core/src/main/java/org/sonar/core/measure/MeasureFilter.java index 9b9c124aa13..42a4f3e0f0a 100644 --- a/sonar-core/src/main/java/org/sonar/core/measure/MeasureFilter.java +++ b/sonar-core/src/main/java/org/sonar/core/measure/MeasureFilter.java @@ -20,20 +20,26 @@ package org.sonar.core.measure; import com.google.common.collect.Lists; +import org.apache.commons.lang.StringUtils; import org.sonar.api.measures.Metric; +import org.sonar.api.utils.DateUtils; import javax.annotation.Nullable; -import java.util.Collections; + +import java.util.Calendar; import java.util.Date; import java.util.List; +import java.util.Map; public class MeasureFilter { + private static final String[] EMPTY = {}; + // conditions on resources private String baseResourceKey; private boolean onBaseResourceChildren = false; // only if getBaseResourceKey is set - private List resourceScopes = Lists.newArrayList(); - private List resourceQualifiers = Lists.newArrayList(); - private List resourceLanguages = Lists.newArrayList(); + private String[] resourceScopes = EMPTY; + private String[] resourceQualifiers = EMPTY; + private String[] resourceLanguages = EMPTY; private String resourceKeyRegexp; private String resourceName; private Date fromDate = null, toDate = null; @@ -63,28 +69,33 @@ public class MeasureFilter { return onBaseResourceChildren; } - public MeasureFilter setResourceScopes(@Nullable List l) { - this.resourceScopes = (l != null ? l : Collections.emptyList()); + public MeasureFilter setResourceScopes(String... l) { + this.resourceScopes = (l != null ? l : EMPTY); return this; } - public MeasureFilter setResourceQualifiers(List l) { - this.resourceQualifiers = (l != null ? l : Collections.emptyList()); + public MeasureFilter setResourceQualifiers(String... l) { + this.resourceQualifiers = l; return this; } - public MeasureFilter setResourceQualifiers(String... l) { - this.resourceQualifiers = Lists.newArrayList(l); + public MeasureFilter setResourceLanguages(String... l) { + this.resourceLanguages = (l != null ? l : EMPTY); return this; } - public MeasureFilter setResourceLanguages(List l) { - this.resourceLanguages = (l != null ? l : Collections.emptyList()); + public MeasureFilter setResourceScopes(@Nullable List l) { + this.resourceScopes = (l != null ? l.toArray(new String[l.size()]) : EMPTY); return this; } - public MeasureFilter setResourceLanguages(String... l) { - this.resourceLanguages = Lists.newArrayList(l); + public MeasureFilter setResourceQualifiers(@Nullable List l) { + this.resourceQualifiers = (l != null ? l.toArray(new String[l.size()]) : EMPTY); + return this; + } + + public MeasureFilter setResourceLanguages(@Nullable List l) { + this.resourceLanguages = (l != null ? l.toArray(new String[l.size()]) : EMPTY); return this; } @@ -159,15 +170,15 @@ public class MeasureFilter { return toDate; } - public List getResourceScopes() { + public String[] getResourceScopes() { return resourceScopes; } - public List getResourceQualifiers() { + public String[] getResourceQualifiers() { return resourceQualifiers; } - public List getResourceLanguages() { + public String[] getResourceLanguages() { return resourceLanguages; } @@ -179,4 +190,56 @@ public class MeasureFilter { return sort; } + public static MeasureFilter create(Map properties) { + MeasureFilter filter = new MeasureFilter(); + filter.setBaseResourceKey(properties.get("base")); + filter.setResourceScopes(toArray(properties.get("scopes"))); + filter.setResourceQualifiers(toArray(properties.get("qualifiers"))); + filter.setResourceLanguages(toArray(properties.get("languages"))); + if (properties.containsKey("onBaseChildren")) { + filter.setOnBaseResourceChildren(Boolean.valueOf(properties.get("onBaseChildren"))); + } + filter.setResourceName(properties.get("nameRegexp")); + filter.setResourceKeyRegexp(properties.get("keyRegexp")); + if (properties.containsKey("fromDate")) { + filter.setFromDate(toDate(properties.get("fromDate"))); + } else if (properties.containsKey("afterDays")) { + filter.setFromDate(toDays(properties.get("afterDays"))); + } + if (properties.containsKey("toDate")) { + filter.setToDate(toDate(properties.get("toDate"))); + } else if (properties.containsKey("beforeDays")) { + filter.setToDate(toDays(properties.get("beforeDays"))); + } + + if (properties.containsKey("favourites")) { + filter.setUserFavourites(Boolean.valueOf(properties.get("favourites"))); + } + return filter; + } + + private static String[] toArray(@Nullable String s) { + if (s == null) { + return EMPTY; + } + return StringUtils.split(s, ","); + } + + private static Date toDate(@Nullable String date) { + if (date != null) { + return DateUtils.parseDate(date); + } + return null; + } + + private static Date toDays(@Nullable String s) { + if (s != null) { + int days = Integer.valueOf(s); + Date date = org.apache.commons.lang.time.DateUtils.truncate(new Date(), Calendar.DATE); + date = org.apache.commons.lang.time.DateUtils.addDays(date, -days); + return date; + } + return null; + } + } diff --git a/sonar-core/src/main/java/org/sonar/core/measure/MeasureFilterEngine.java b/sonar-core/src/main/java/org/sonar/core/measure/MeasureFilterEngine.java index 253c2875d6f..fcebe106ea9 100644 --- a/sonar-core/src/main/java/org/sonar/core/measure/MeasureFilterEngine.java +++ b/sonar-core/src/main/java/org/sonar/core/measure/MeasureFilterEngine.java @@ -27,7 +27,9 @@ import org.slf4j.LoggerFactory; import org.sonar.api.ServerComponent; import javax.annotation.Nullable; + import java.util.List; +import java.util.Map; public class MeasureFilterEngine implements ServerComponent { private static final Logger FILTER_LOG = LoggerFactory.getLogger("org.sonar.MEASURE_FILTER"); @@ -44,6 +46,22 @@ public class MeasureFilterEngine implements ServerComponent { return execute(filterJson, userId, FILTER_LOG); } + public List execute2(Map filterMap, @Nullable Long userId) throws ParseException { + Logger logger = FILTER_LOG; + MeasureFilterContext context = new MeasureFilterContext(); + context.setJson(filterMap.toString()); + context.setUserId(userId); + try { + long start = System.currentTimeMillis(); + MeasureFilter filter = MeasureFilter.create(filterMap); + List rows = executor.execute(filter, context); + log(context, rows, (System.currentTimeMillis() - start), logger); + return rows; + } catch (Exception e) { + throw new IllegalStateException("Fail to execute filter: " + context, e); + } + } + @VisibleForTesting List execute(String filterJson, @Nullable Long userId, Logger logger) { MeasureFilterContext context = new MeasureFilterContext(); diff --git a/sonar-core/src/main/java/org/sonar/core/measure/MeasureFilterSql.java b/sonar-core/src/main/java/org/sonar/core/measure/MeasureFilterSql.java index c95717e2c2a..bb41a9c77ed 100644 --- a/sonar-core/src/main/java/org/sonar/core/measure/MeasureFilterSql.java +++ b/sonar-core/src/main/java/org/sonar/core/measure/MeasureFilterSql.java @@ -37,7 +37,6 @@ import java.sql.Date; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.Collection; import java.util.List; class MeasureFilterSql { @@ -150,15 +149,15 @@ class MeasureFilterSql { if (context.getBaseSnapshot() == null) { sql.append(" AND p.copy_resource_id IS NULL "); } - if (!filter.getResourceQualifiers().isEmpty()) { + if (filter.getResourceQualifiers().length > 0) { sql.append(" AND s.qualifier IN "); appendInStatement(filter.getResourceQualifiers(), sql); } - if (!filter.getResourceScopes().isEmpty()) { + if (filter.getResourceScopes().length > 0) { sql.append(" AND s.scope IN "); appendInStatement(filter.getResourceScopes(), sql); } - if (!filter.getResourceLanguages().isEmpty()) { + if (filter.getResourceLanguages().length > 0) { sql.append(" AND p.language IN "); appendInStatement(filter.getResourceLanguages(), sql); } @@ -217,7 +216,7 @@ class MeasureFilterSql { sql.append(" AND s.project_id IN (SELECT rindex.resource_id FROM resource_index rindex WHERE rindex.kee like '"); sql.append(StringEscapeUtils.escapeSql(StringUtils.lowerCase(filter.getResourceName()))); sql.append("%'"); - if (!filter.getResourceQualifiers().isEmpty()) { + if (filter.getResourceQualifiers().length > 0) { sql.append(" AND rindex.qualifier IN "); appendInStatement(filter.getResourceQualifiers(), sql); } @@ -259,7 +258,7 @@ class MeasureFilterSql { } - private static void appendInStatement(Collection values, StringBuilder to) { + private static void appendInStatement(String[] values, StringBuilder to) { to.append(" ('"); to.append(StringUtils.join(values, "','")); to.append("') "); diff --git a/sonar-core/src/test/java/org/sonar/core/measure/MeasureFilterDecoderTest.java b/sonar-core/src/test/java/org/sonar/core/measure/MeasureFilterDecoderTest.java index 3dedb2d09a1..ce7ed550464 100644 --- a/sonar-core/src/test/java/org/sonar/core/measure/MeasureFilterDecoderTest.java +++ b/sonar-core/src/test/java/org/sonar/core/measure/MeasureFilterDecoderTest.java @@ -62,9 +62,9 @@ public class MeasureFilterDecoderTest { assertThat(filter.getBaseResourceKey()).isEqualTo("org.struts"); assertThat(filter.isOnBaseResourceChildren()).isTrue(); - assertThat(filter.getResourceScopes()).containsExactly("PRJ"); - assertThat(filter.getResourceQualifiers()).containsExactly("TRK", "CLA"); - assertThat(filter.getResourceLanguages()).containsExactly("java", "php"); + assertThat(filter.getResourceScopes()).containsOnly("PRJ"); + assertThat(filter.getResourceQualifiers()).containsOnly("TRK", "CLA"); + assertThat(filter.getResourceLanguages()).containsOnly("java", "php"); assertThat(filter.getResourceName()).isEqualTo("Struts"); assertThat(filter.getResourceKeyRegexp()).isEqualTo("*foo*"); assertThat(filter.getFromDate().getYear()).isEqualTo(2012 - 1900); diff --git a/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java b/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java index 8fb4fc40ae0..cbedd251521 100644 --- a/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java +++ b/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java @@ -102,6 +102,10 @@ public final class JRubyFacade { return get(MeasureFilterEngine.class).execute(json, userId); } + public List executeMeasureFilter2(Map map, @Nullable Long userId) throws ParseException { + return get(MeasureFilterEngine.class).execute2(map, userId); + } + public Collection getResourceTypesForFilter() { return get(ResourceTypes.class).getAll(ResourceTypes.AVAILABLE_FOR_FILTERS); } @@ -314,7 +318,7 @@ public final class JRubyFacade { public void ruleSeverityChanged(int parentProfileId, int activeRuleId, int oldSeverityId, int newSeverityId, String userName) { getProfilesManager().ruleSeverityChanged(parentProfileId, activeRuleId, RulePriority.values()[oldSeverityId], - RulePriority.values()[newSeverityId], userName); + RulePriority.values()[newSeverityId], userName); } public void ruleDeactivated(int parentProfileId, int deactivatedRuleId, String userName) { @@ -510,10 +514,10 @@ public final class JRubyFacade { // notifier is null when creating the administrator in the migration script 011. if (notifier != null) { notifier.onNewUser(NewUserHandler.Context.builder() - .setLogin(fields.get("login")) - .setName(fields.get("name")) - .setEmail(fields.get("email")) - .build()); + .setLogin(fields.get("login")) + .setName(fields.get("name")) + .setEmail(fields.get("email")) + .build()); } } diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/measures_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/measures_controller.rb index 0b6662b207f..25a0248df1c 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/controllers/measures_controller.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/measures_controller.rb @@ -19,9 +19,19 @@ # class MeasuresController < ApplicationController + SECTION=Navigation::SECTION_HOME + def index - + render :action => 'search' end + def search + options = { + :user => current_user, + :page => (params[:page] || 1) + } + @filter = MeasureFilter.new(params) + @filter.execute(self, options) + end end diff --git a/sonar-server/src/main/webapp/WEB-INF/app/models/measure_filter.rb b/sonar-server/src/main/webapp/WEB-INF/app/models/measure_filter.rb new file mode 100644 index 00000000000..225f233c164 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/models/measure_filter.rb @@ -0,0 +1,207 @@ +# +# Sonar, entreprise quality control tool. +# Copyright (C) 2008-2012 SonarSource +# mailto:contact AT sonarsource DOT com +# +# Sonar is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# Sonar is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with {library}; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 +# +class MeasureFilter + # Row in the table of results + class Data + attr_reader :snapshot, :measures_by_metric, :links + + def initialize(snapshot) + @snapshot = snapshot + @measures_by_metric = {} + @links = nil + end + + def add_measure(measure) + @measures_by_metric[measure.metric] = measure + end + + def add_link(link) + @links ||= [] + @links << link + end + + def measure(metric) + @measures_by_metric[metric] + end + end + + # Column to be displayed + class Column + attr_reader :key, :metric + + def initialize(key) + @key = key + metric_key = @key.split(':')[1] + @metric = Metric.by_key(metric_key) if metric_key + end + + def name? + @key == 'name' + end + + def links? + @key == 'links' + end + end + + class Display + def init_filter(filter) + + end + end + + class ListDisplay < Display + def key + 'list' + end + + def init_filter(filter) + + end + end + + class TreemapDisplay < Display + def key + 'treemap' + end + end + + DEFAULT_OPTIONS = {:page => 1, :page_size => 50} + DEFAULT_COLUMNS = [Column.new('name'), Column.new('date'), Column.new('metric:ncloc'), Column.new('metric:violations')] + DISPLAYS = [ListDisplay.new, TreemapDisplay.new] + + # Simple hash {string key => fixnum or boolean or string} + attr_accessor :criteria + + # Configuration available after call to execute() + attr_reader :pagination, :security_exclusions, :columns, :display + + # Results : sorted array of Data + attr_reader :data + + def initialize(criteria={}) + @criteria = criteria + end + + # ==== Options + # 'page' : page id starting with 1. Used on table display. + # 'page_size' : number of results per page. + # 'user' : the authenticated user + # 'period' : index of the period between 1 and 5 + # + def execute(controller, options={}) + return reset_results if @criteria.empty? + init_columns + init_display(options) + + opts = DEFAULT_OPTIONS.merge(options) + user = opts[:user] + + rows=Api::Utils.java_facade.executeMeasureFilter2(@criteria, (user ? user.id : nil)) + snapshot_ids = filter_authorized_snapshot_ids(rows, controller) + snapshot_ids = paginate_snapshot_ids(snapshot_ids, opts) + init_data(snapshot_ids) + + self + end + + private + + def init_columns + fields = @criteria['columns'] + if fields.present? + @columns = fields.split(',').map { |field| Column.new(field) } + else + @columns = DEFAULT_COLUMNS.clone + end + end + + def init_display(options) + key = @criteria['display'] + if key.present? + @display = DISPLAYS.find { |display| display.key==key } + end + @display ||= DISPLAYS.first + end + + def reset_results + @pagination = nil + @security_exclusions = nil + @data = nil + self + end + + def filter_authorized_snapshot_ids(rows, controller) + project_ids = rows.map { |row| row.getResourceRootId() }.compact.uniq + authorized_project_ids = controller.select_authorized(:user, project_ids) + snapshot_ids = rows.map { |row| row.getSnapshotId() if authorized_project_ids.include?(row.getResourceRootId()) }.compact + @security_exclusions = (snapshot_ids.size options[:page_size], + :page => options[:page], + :count => snapshot_ids.size}) + from = (@pagination.page - 1) * @pagination.per_page + to = (@pagination.page * @pagination.per_page) - 1 + to = snapshot_ids.size - 1 if to >= snapshot_ids.size + snapshot_ids[from..to] + end + + def init_data(snapshot_ids) + @data = [] + if !snapshot_ids.empty? + data_by_snapshot_id = {} + snapshots = Snapshot.find(:all, :include => ['project'], :conditions => ['id in (?)', snapshot_ids]) + snapshots.each do |snapshot| + data = Data.new(snapshot) + data_by_snapshot_id[snapshot.id] = data + @data << data + end + + metric_ids = @columns.map { |column| column.metric }.compact.uniq.map { |metric| metric.id } + unless metric_ids.empty? + measures = ProjectMeasure.find(:all, :conditions => + ['rule_priority is null and rule_id is null and characteristic_id is null and person_id is null and snapshot_id in (?) and metric_id in (?)', snapshot_ids, metric_ids] + ) + measures.each do |measure| + data = data_by_snapshot_id[measure.snapshot_id] + data.add_measure measure + end + end + + if @columns.index { |column| column.links? } + project_ids = [] + data_by_project_id = {} + snapshots.each do |snapshot| + project_ids << snapshot.project_id + data_by_project_id[snapshot.project_id] = data_by_snapshot_id[snapshot.id] + end + links = ProjectLink.find(:all, :conditions => {:project_id => project_ids}, :order => 'link_type') + links.each do |link| + data_by_project_id[link.project_id].add_link(link) + end + end + end + end + +end \ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/measures/_display_list.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/measures/_display_list.html.erb new file mode 100644 index 00000000000..fd24b72fa60 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/measures/_display_list.html.erb @@ -0,0 +1,41 @@ + + + + <% @filter.columns.each do |column| %> + <% if column.metric %> + + <% elsif column.key=='name' %> + + <% else %> + + <% end %> + <% end %> + + + + <% @filter.data.each do |data| %> + + <% @filter.columns.each do |column| %> + <% if column.metric %> + + <% elsif column.key=='name' %> + + <% elsif column.key=='date' %> + + <% elsif column.key=='key' %> + + <% else %> + + <% end %> + <% end %> + + <% end %> + + <%= render :partial => 'utils/tfoot_pagination', :locals => {:pagination => @filter.pagination, :colspan => @filter.columns.size} %> +
<%= Api::Utils.message("metric.#{column.metric.key}.name", :default => column.metric.short_name) -%><%= Api::Utils.message("filters.col.name") -%><%= Api::Utils.message("filters.col.#{column.key}", :default => column.key) -%>
+ <%= format_measure(data.measure(column.metric)) -%> + + <%= qualifier_icon(data.snapshot) %> <%= link_to_resource(data.snapshot.resource, h(data.snapshot.resource.name(true)), {:title => data.snapshot.resource.key}) -%> + + <%= human_short_date(data.snapshot.created_at) -%> + <%= data.snapshot.resource.kee -%>
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/measures/_display_treemap.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/measures/_display_treemap.html.erb new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/measures/search.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/measures/search.html.erb new file mode 100644 index 00000000000..fd1fc74d54c --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/measures/search.html.erb @@ -0,0 +1,72 @@ +<% content_for :style do %> + +<% end %> + +<% url_options = params.reject { |k, v| v.empty? || k=='search' }.merge({:controller => 'measures', :action => 'search'}) %> + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Base:
Qualifiers: + +
Language:
Name:
Key:
+
+
+ <% if @filter %> +
+ <% MeasureFilter::DISPLAYS.each do |display| %> + <%= link_to display.key, url_options.merge(:display => display.key) -%> + <% end %> + + <%= render :partial => "measures/display_#{@filter.display.key}.html.erb" -%> +

+ <% permalink = url_for(url_options) %> + Permalink: +

+
+ <% end %> + +
\ No newline at end of file -- 2.39.5