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
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<String> resourceScopes = Lists.newArrayList();
- private List<String> resourceQualifiers = Lists.newArrayList();
- private List<String> 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;
return onBaseResourceChildren;
}
- public MeasureFilter setResourceScopes(@Nullable List<String> l) {
- this.resourceScopes = (l != null ? l : Collections.<String>emptyList());
+ public MeasureFilter setResourceScopes(String... l) {
+ this.resourceScopes = (l != null ? l : EMPTY);
return this;
}
- public MeasureFilter setResourceQualifiers(List<String> l) {
- this.resourceQualifiers = (l != null ? l : Collections.<String>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<String> l) {
- this.resourceLanguages = (l != null ? l : Collections.<String>emptyList());
+ public MeasureFilter setResourceScopes(@Nullable List<String> 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<String> l) {
+ this.resourceQualifiers = (l != null ? l.toArray(new String[l.size()]) : EMPTY);
+ return this;
+ }
+
+ public MeasureFilter setResourceLanguages(@Nullable List<String> l) {
+ this.resourceLanguages = (l != null ? l.toArray(new String[l.size()]) : EMPTY);
return this;
}
return toDate;
}
- public List<String> getResourceScopes() {
+ public String[] getResourceScopes() {
return resourceScopes;
}
- public List<String> getResourceQualifiers() {
+ public String[] getResourceQualifiers() {
return resourceQualifiers;
}
- public List<String> getResourceLanguages() {
+ public String[] getResourceLanguages() {
return resourceLanguages;
}
return sort;
}
+ public static MeasureFilter create(Map<String, String> 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;
+ }
+
}
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");
return execute(filterJson, userId, FILTER_LOG);
}
+ public List<MeasureFilterRow> execute2(Map<String, String> 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<MeasureFilterRow> 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<MeasureFilterRow> execute(String filterJson, @Nullable Long userId, Logger logger) {
MeasureFilterContext context = new MeasureFilterContext();
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
-import java.util.Collection;
import java.util.List;
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);
}
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);
}
}
- private static void appendInStatement(Collection<String> values, StringBuilder to) {
+ private static void appendInStatement(String[] values, StringBuilder to) {
to.append(" ('");
to.append(StringUtils.join(values, "','"));
to.append("') ");
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);
return get(MeasureFilterEngine.class).execute(json, userId);
}
+ public List<MeasureFilterRow> executeMeasureFilter2(Map<String,String> map, @Nullable Long userId) throws ParseException {
+ return get(MeasureFilterEngine.class).execute2(map, userId);
+ }
+
public Collection<ResourceType> getResourceTypesForFilter() {
return get(ResourceTypes.class).getAll(ResourceTypes.AVAILABLE_FOR_FILTERS);
}
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) {
// 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());
}
}
#
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
--- /dev/null
+#
+# 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<rows.size)
+ snapshot_ids
+ end
+
+ def paginate_snapshot_ids(snapshot_ids, options)
+ @pagination = Api::Pagination.new({
+ :per_page => 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
--- /dev/null
+<table class="data">
+ <thead>
+ <tr>
+ <% @filter.columns.each do |column| %>
+ <% if column.metric %>
+ <th class="right"><%= Api::Utils.message("metric.#{column.metric.key}.name", :default => column.metric.short_name) -%></th>
+ <% elsif column.key=='name' %>
+ <th class="left"><%= Api::Utils.message("filters.col.name") -%></th>
+ <% else %>
+ <th class="right"><%= Api::Utils.message("filters.col.#{column.key}", :default => column.key) -%></th>
+ <% end %>
+ <% end %>
+ </tr>
+ </thead>
+ <tbody>
+ <% @filter.data.each do |data| %>
+ <tr class="<%= cycle 'even', 'odd' -%>">
+ <% @filter.columns.each do |column| %>
+ <% if column.metric %>
+ <td class="right">
+ <%= format_measure(data.measure(column.metric)) -%>
+ </td>
+ <% elsif column.key=='name' %>
+ <td class="left">
+ <%= qualifier_icon(data.snapshot) %> <%= link_to_resource(data.snapshot.resource, h(data.snapshot.resource.name(true)), {:title => data.snapshot.resource.key}) -%>
+ </td>
+ <% elsif column.key=='date' %>
+ <td class="right">
+ <%= human_short_date(data.snapshot.created_at) -%>
+ </td>
+ <% elsif column.key=='key' %>
+ <td class="left"><span class="small"><%= data.snapshot.resource.kee -%></span></td>
+ <% else %>
+ <td></td>
+ <% end %>
+ <% end %>
+ </tr>
+ <% end %>
+ </tbody>
+ <%= render :partial => 'utils/tfoot_pagination', :locals => {:pagination => @filter.pagination, :colspan => @filter.columns.size} %>
+</table>
\ No newline at end of file
--- /dev/null
+<% content_for :style do %>
+ <style type="text/css">
+ #filter-form {
+ float: left;
+ width: 240px;
+ }
+
+ #filter-result {
+ padding-left: 250px;
+ }
+ </style>
+<% end %>
+
+<% url_options = params.reject { |k, v| v.empty? || k=='search' }.merge({:controller => 'measures', :action => 'search'}) %>
+
+<div id="measure-filter">
+ <div id="filter-form">
+ <form method="POST" action="<%= ApplicationController.root_context -%>/measures/search">
+ <table>
+ <tbody>
+ <tr>
+ <td>Base:</td>
+ <td><input type="text" name="base"></td>
+ </tr>
+ <tr>
+ <td>Qualifiers:</td>
+ <td>
+ <select name="qualifiers">
+ <option value="">Any</option>
+ <option value="TRK">Project</option>
+ <option value="TRK,BRC">Project</option>
+ <option value="DIR,PAC">Directory/Package</option>
+ <option value="FIL,CLA">File</option>
+ <option value="UTS">Unit Test File</option>
+ </select>
+ </td>
+ </tr>
+ <tr>
+ <td>Language:</td>
+ <td><input type="text" name="language"></td>
+ </tr>
+ <tr>
+ <td>Name:</td>
+ <td><input type="text" name="nameRegexp"></td>
+ </tr>
+ <tr>
+ <td>Key:</td>
+ <td><input type="text" name="keyRegexp"></td>
+ </tr>
+ <tr>
+ <td><input type="submit" name="search" value="Search"></td>
+ <td></td>
+ </tr>
+ </tbody>
+ </table>
+ </form>
+ </div>
+ <% if @filter %>
+ <div id="filter-result">
+ <% 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" -%>
+ <p>
+ <% permalink = url_for(url_options) %>
+ <a href="<%= permalink -%>">Permalink</a>: <input type="text" value="<%= permalink -%>" size="100">
+ </p>
+ </div>
+ <% end %>
+
+</div>
\ No newline at end of file