]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-4419 Roles page: improve search engine to add ability to search by project key
authorJulien Lancelot <julien.lancelot@gmail.com>
Mon, 15 Jul 2013 06:33:56 +0000 (08:33 +0200)
committerJulien Lancelot <julien.lancelot@gmail.com>
Mon, 15 Jul 2013 14:19:14 +0000 (16:19 +0200)
18 files changed:
plugins/sonar-core-plugin/src/main/resources/org/sonar/l10n/core.properties
sonar-core/src/main/java/org/sonar/core/resource/ResourceDao.java
sonar-server/src/main/java/org/sonar/server/component/ComponentQuery.java [new file with mode: 0644]
sonar-server/src/main/java/org/sonar/server/component/ComponentsFinderSort.java [new file with mode: 0644]
sonar-server/src/main/java/org/sonar/server/component/DefaultComponentFinder.java [new file with mode: 0644]
sonar-server/src/main/java/org/sonar/server/component/DefaultComponentQueryResult.java [new file with mode: 0644]
sonar-server/src/main/java/org/sonar/server/component/DefaultRubyComponentService.java
sonar-server/src/main/java/org/sonar/server/platform/Platform.java
sonar-server/src/main/webapp/WEB-INF/app/controllers/roles_controller.rb
sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_layout.html.erb
sonar-server/src/main/webapp/WEB-INF/app/views/permission_templates/index.html.erb
sonar-server/src/main/webapp/WEB-INF/app/views/roles/_tabs.html.erb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/views/roles/projects.html.erb
sonar-server/src/main/webapp/stylesheets/style.css
sonar-server/src/test/java/org/sonar/server/component/ComponentQueryTest.java [new file with mode: 0644]
sonar-server/src/test/java/org/sonar/server/component/ComponentsFinderSortTest.java [new file with mode: 0644]
sonar-server/src/test/java/org/sonar/server/component/DefaultComponentFinderTest.java [new file with mode: 0644]
sonar-server/src/test/java/org/sonar/server/component/DefaultRubyComponentServiceTest.java

index 35e3eb87d0c18027a70114b222fb80520899ba25..405e144c325b5c36a0d8b6de782b029948ab1831 100644 (file)
@@ -140,6 +140,7 @@ title=Title
 to=To
 treemap=Treemap
 true=True
+type=Type
 unfollow=Unfollow
 unit_test=Unit test
 unit_tests=Unit tests
@@ -343,7 +344,7 @@ manual_metrics.page=Manual Metrics
 manual_measures.page=Manual Measures
 manual_rules.page=Manual Rules
 my_profile.page=My Profile
-roles.page=Roles
+roles.page=Project Permissions
 project_settings.page=Settings
 project_links.page=Links
 project_history.page=History
@@ -2244,6 +2245,8 @@ global_permissions.dryRunScan.desc=Ability to execute local (dry run) analyses w
 # PROJECTS ROLES
 #
 #------------------------------------------------------------------------------
+projects_role.criteria.name=Name contains
+projects_role.criteria.key=Key contains
 projects_role.role=Role Membership For New
 projects_role.users=Users
 projects_role.groups=Groups
index e18f0e4bfb12b5b4cad8df34d1b73313fc00561f..9c9b4bd71f3d36613a0ab367591a36671b26108d 100644 (file)
@@ -19,6 +19,8 @@
  */
 package org.sonar.core.resource;
 
+import com.google.common.base.Function;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import org.apache.ibatis.session.SqlSession;
 import org.sonar.api.component.Component;
@@ -199,7 +201,7 @@ public class ResourceDao {
     }
   }
 
-  public ComponentDto toComponent(ResourceDto resourceDto){
+  public static ComponentDto toComponent(ResourceDto resourceDto){
     return new ComponentDto()
       .setKey(resourceDto.getKey())
       .setLongName(resourceDto.getLongName())
@@ -207,6 +209,15 @@ public class ResourceDao {
       .setQualifier(resourceDto.getQualifier());
   }
 
+  public static List<ComponentDto> toComponents(List<ResourceDto> resourceDto){
+    return newArrayList(Iterables.transform(resourceDto, new Function<ResourceDto, ComponentDto>() {
+      @Override
+      public ComponentDto apply(ResourceDto resourceDto) {
+        return toComponent(resourceDto);
+      }
+    }));
+  }
+
   public void insertUsingExistingSession(ResourceDto resourceDto, SqlSession session) {
     ResourceMapper resourceMapper = session.getMapper(ResourceMapper.class);
     resourceMapper.insert(resourceDto);
diff --git a/sonar-server/src/main/java/org/sonar/server/component/ComponentQuery.java b/sonar-server/src/main/java/org/sonar/server/component/ComponentQuery.java
new file mode 100644 (file)
index 0000000..32bcb35
--- /dev/null
@@ -0,0 +1,190 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2013 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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 this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.component;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableSet;
+import org.apache.commons.lang.builder.ReflectionToStringBuilder;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * @since 3.7
+ */
+public class ComponentQuery {
+
+  public static final int DEFAULT_PAGE_INDEX = 1;
+  public static final int DEFAULT_PAGE_SIZE = 100;
+
+  public static final String SORT_BY_NAME = "NAME";
+  public static final Set<String> SORTS = ImmutableSet.of(SORT_BY_NAME);
+
+  private final Collection<String> keys;
+  private final Collection<String> names;
+  private final Collection<String> qualifiers;
+  private final String sort;
+  private final Boolean asc;
+
+  // max results per page
+  private final int pageSize;
+
+  // index of selected page. Start with 1.
+  private final int pageIndex;
+
+  private ComponentQuery(Builder builder) {
+    this.keys = defaultCollection(builder.keys);
+    this.names = defaultCollection(builder.names);
+    this.qualifiers = defaultCollection(builder.qualifiers);
+
+    this.sort = builder.sort;
+    this.asc = builder.asc;
+    this.pageSize = builder.pageSize;
+    this.pageIndex = builder.pageIndex;
+  }
+
+  /**
+   * Pattern of component keys to search. Can contain a sub part of keys.
+   * Example : 'org.codehaus' will return 'org.codehaus.sonar', 'org.codehaus.tike', etc.
+   */
+  public Collection<String> keys() {
+    return keys;
+  }
+
+  /**
+   * Pattern of component name to search. Can contain a sub part of names.
+   * Example : 'Sona' will return 'Sonar', 'SonarJ', etc.
+   */
+  public Collection<String> names() {
+    return names;
+  }
+
+  /**
+   * Qualifiers of components to search.
+   */
+  public Collection<String> qualifiers() {
+    return qualifiers;
+  }
+
+  @CheckForNull
+  public String sort() {
+    return sort;
+  }
+
+  @CheckForNull
+  public Boolean asc() {
+    return asc;
+  }
+
+  public int pageSize() {
+    return pageSize;
+  }
+
+  public int pageIndex() {
+    return pageIndex;
+  }
+
+  @Override
+  public String toString() {
+    return ReflectionToStringBuilder.toString(this);
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+    private Collection<String> keys;
+    private Collection<String> names;
+    private Collection<String> qualifiers;
+    private String sort = SORT_BY_NAME;
+    private Boolean asc = true;
+    private Integer pageSize;
+    private Integer pageIndex;
+
+    private Builder() {
+    }
+
+    public Builder keys(@Nullable Collection<String> l) {
+      this.keys = l;
+      return this;
+    }
+
+    public Builder names(@Nullable Collection<String> l) {
+      this.names = l;
+      return this;
+    }
+
+    public Builder qualifiers(@Nullable Collection<String> l) {
+      this.qualifiers = l;
+      return this;
+    }
+
+    public Builder sort(@Nullable String s) {
+      if (s != null && !SORTS.contains(s)) {
+        throw new IllegalArgumentException("Bad sort field: " + s);
+      }
+      this.sort = s;
+      return this;
+    }
+
+    public Builder asc(@Nullable Boolean asc) {
+      this.asc = asc;
+      return this;
+    }
+
+    public Builder pageSize(@Nullable Integer i) {
+      this.pageSize = i;
+      return this;
+    }
+
+    public Builder pageIndex(@Nullable Integer i) {
+      this.pageIndex = i;
+      return this;
+    }
+
+    public ComponentQuery build() {
+      initPageIndex();
+      initPageSize();
+      return new ComponentQuery(this);
+    }
+
+    private void initPageSize() {
+      if (pageSize == null) {
+        pageSize = DEFAULT_PAGE_SIZE;
+      }
+    }
+
+    private void initPageIndex() {
+      if (pageIndex == null) {
+        pageIndex = DEFAULT_PAGE_INDEX;
+      }
+      Preconditions.checkArgument(pageIndex > 0, "Page index must be greater than 0 (got " + pageIndex + ")");
+    }
+  }
+
+  private static <T> Collection<T> defaultCollection(@Nullable Collection<T> c) {
+    return c == null ? Collections.<T>emptyList() : Collections.unmodifiableCollection(c);
+  }
+}
diff --git a/sonar-server/src/main/java/org/sonar/server/component/ComponentsFinderSort.java b/sonar-server/src/main/java/org/sonar/server/component/ComponentsFinderSort.java
new file mode 100644 (file)
index 0000000..fa6b169
--- /dev/null
@@ -0,0 +1,98 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2013 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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 this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.component;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Ordering;
+import org.sonar.api.component.Component;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * @since 3.7
+ */
+class ComponentsFinderSort {
+
+  private Collection<? extends Component> components;
+  private ComponentQuery query;
+
+  public ComponentsFinderSort(Collection<? extends Component> components, ComponentQuery query) {
+    this.components = components;
+    this.query = query;
+  }
+
+  public Collection<? extends Component> sort() {
+    String sort = query.sort();
+    Boolean asc = query.asc();
+    if (sort != null && asc != null) {
+      return getComponentProcessor(sort).sort(components, asc);
+    }
+    return components;
+  }
+
+  private ComponentProcessor getComponentProcessor(String sort) {
+    if (ComponentQuery.SORT_BY_NAME.equals(sort)) {
+      return new NameSort();
+    }
+    throw new IllegalArgumentException("Cannot sort on field : " + sort);
+  }
+
+  abstract static class ComponentProcessor {
+    abstract Function sortFieldFunction();
+
+    abstract Ordering sortFieldOrdering(boolean ascending);
+
+    final List<? extends Component> sort(Collection<? extends Component> components, boolean ascending) {
+      Ordering<Component> ordering = sortFieldOrdering(ascending).onResultOf(sortFieldFunction());
+      return ordering.immutableSortedCopy(components);
+    }
+  }
+
+  abstract static class TextSort extends ComponentProcessor {
+    @Override
+    Function sortFieldFunction() {
+      return new Function<Component, String>() {
+        public String apply(Component component) {
+          return sortField(component);
+        }
+      };
+    }
+
+    abstract String sortField(Component component);
+
+    @Override
+    Ordering sortFieldOrdering(boolean ascending) {
+      Ordering<String> ordering = Ordering.from(String.CASE_INSENSITIVE_ORDER).nullsLast();
+      if (!ascending) {
+        ordering = ordering.reverse();
+      }
+      return ordering;
+    }
+  }
+
+  static class NameSort extends TextSort {
+    @Override
+    String sortField(Component component) {
+      return component.name();
+    }
+  }
+
+}
diff --git a/sonar-server/src/main/java/org/sonar/server/component/DefaultComponentFinder.java b/sonar-server/src/main/java/org/sonar/server/component/DefaultComponentFinder.java
new file mode 100644 (file)
index 0000000..ff5164e
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2013 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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 this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.component;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.component.Component;
+import org.sonar.api.utils.Paging;
+import org.sonar.core.component.ComponentDto;
+import org.sonar.core.resource.ResourceDao;
+import org.sonar.core.resource.ResourceQuery;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+import static com.google.common.collect.Lists.newArrayList;
+
+/**
+ * @since 3.7
+ */
+public class DefaultComponentFinder {
+
+  private static final Logger LOG = LoggerFactory.getLogger(DefaultComponentFinder.class);
+  private final ResourceDao resourceDao;
+
+  public DefaultComponentFinder(ResourceDao resourceDao) {
+    this.resourceDao = resourceDao;
+  }
+
+  public DefaultComponentQueryResult find(ComponentQuery query) {
+    LOG.debug("ComponentQuery : {}", query);
+    long start = System.currentTimeMillis();
+    try {
+      // 1. Search components
+      ResourceQuery resourceQuery = ResourceQuery.create().setQualifiers(query.qualifiers().toArray(new String[]{}));
+      List<ComponentDto> dtos = ResourceDao.toComponents(resourceDao.getResources(resourceQuery));
+      Collection<Component> foundComponents = search(query, dtos);
+
+      // 2. Sort components
+      Collection<? extends Component> sortedComponents = new ComponentsFinderSort(foundComponents, query).sort();
+
+      // 3. Apply pagination
+      Paging paging = Paging.create(query.pageSize(), query.pageIndex(), foundComponents.size());
+      Collection<? extends Component> pagedComponents = pagedComponents(sortedComponents, paging);
+
+      return new DefaultComponentQueryResult(pagedComponents).setPaging(paging).setQuery(query);
+    } finally {
+      LOG.debug("ComponentQuery execution time : {} ms", System.currentTimeMillis() - start);
+    }
+  }
+
+  private Collection<Component> search(final ComponentQuery query, List<? extends Component> allComponents) {
+    return newArrayList(Iterables.filter(allComponents, new Predicate<Component>() {
+      @Override
+      public boolean apply(Component component) {
+        return new KeyFilter().accept(component, query.keys()) &&
+          new NameFilter().accept(component, query.names());
+      }
+    }));
+  }
+
+  abstract static class Filter {
+
+    abstract String field(Component component);
+
+    final boolean accept(Component component, Collection<String> collections) {
+      if (!collections.isEmpty()) {
+        for (String item : collections) {
+          if (field(component).toLowerCase().contains(item.toLowerCase())) {
+            return true;
+          }
+        }
+        return false;
+      }
+      return true;
+    }
+  }
+
+  static class NameFilter extends Filter {
+    @Override
+    String field(Component component) {
+      return component.name();
+    }
+  }
+
+  static class KeyFilter extends Filter {
+    @Override
+    String field(Component component) {
+      return component.key();
+    }
+  }
+
+  private Collection<? extends Component> pagedComponents(Collection<? extends Component> components, Paging paging) {
+    Set<Component> pagedComponents = Sets.newLinkedHashSet();
+    int index = 0;
+    for (Component component : components) {
+      if (index >= paging.offset() && pagedComponents.size() < paging.pageSize()) {
+        pagedComponents.add(component);
+      } else if (pagedComponents.size() >= paging.pageSize()) {
+        break;
+      }
+      index++;
+    }
+    return pagedComponents;
+  }
+
+}
diff --git a/sonar-server/src/main/java/org/sonar/server/component/DefaultComponentQueryResult.java b/sonar-server/src/main/java/org/sonar/server/component/DefaultComponentQueryResult.java
new file mode 100644 (file)
index 0000000..7cd99f4
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2013 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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 this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.component;
+
+import org.sonar.api.component.Component;
+import org.sonar.api.utils.Paging;
+
+import java.util.Collection;
+
+public class DefaultComponentQueryResult {
+
+  private Collection<? extends Component> components;
+  private ComponentQuery query;
+  private Paging paging;
+
+  public DefaultComponentQueryResult(Collection<? extends Component> components) {
+    this.components = components;
+  }
+
+  public DefaultComponentQueryResult setPaging(Paging paging) {
+    this.paging = paging;
+    return this;
+  }
+
+  public ComponentQuery query() {
+    return query;
+  }
+
+  public DefaultComponentQueryResult setQuery(ComponentQuery query) {
+    this.query = query;
+    return this;
+  }
+
+  public Collection<? extends Component> components() {
+    return components;
+  }
+
+  public Paging paging() {
+    return paging;
+  }
+
+}
index da9139465557795e154fc05c152f271695e83c82..2dce322b90a969a900932d9b3e3087cdca6b7bf0 100644 (file)
  */
 package org.sonar.server.component;
 
+import com.google.common.base.Strings;
 import org.sonar.api.component.Component;
 import org.sonar.api.component.RubyComponentService;
 import org.sonar.core.resource.ResourceDao;
+import org.sonar.server.util.RubyUtils;
+
+import java.util.Map;
 
 public class DefaultRubyComponentService implements RubyComponentService {
 
   private final ResourceDao resourceDao;
+  private final DefaultComponentFinder finder;
 
-  public DefaultRubyComponentService(ResourceDao resourceDao) {
+  public DefaultRubyComponentService(ResourceDao resourceDao, DefaultComponentFinder finder) {
     this.resourceDao = resourceDao;
+    this.finder = finder;
   }
 
   @Override
@@ -36,4 +42,23 @@ public class DefaultRubyComponentService implements RubyComponentService {
     return resourceDao.findByKey(key);
   }
 
+  public DefaultComponentQueryResult find(Map<String, Object> params) {
+    return finder.find(toQuery(params));
+  }
+
+  static ComponentQuery toQuery(Map<String, Object> props) {
+    ComponentQuery.Builder builder = ComponentQuery.builder()
+      .keys(RubyUtils.toStrings(props.get("keys")))
+      .names(RubyUtils.toStrings(props.get("names")))
+      .qualifiers(RubyUtils.toStrings(props.get("qualifiers")))
+      .pageSize(RubyUtils.toInteger(props.get("pageSize")))
+      .pageIndex(RubyUtils.toInteger(props.get("pageIndex")));
+    String sort = (String) props.get("sort");
+    if (!Strings.isNullOrEmpty(sort)) {
+      builder.sort(sort);
+      builder.asc(RubyUtils.toBoolean(props.get("asc")));
+    }
+    return builder.build();
+  }
+
 }
index 00218059afb0859d4e5a664945c050f2d9d6feaf..554de95b5216c4474d78bbf6af6173b386359c1c 100644 (file)
@@ -70,6 +70,7 @@ import org.sonar.jpa.session.DatabaseSessionProvider;
 import org.sonar.jpa.session.DefaultDatabaseConnector;
 import org.sonar.jpa.session.ThreadLocalDatabaseSessionFactory;
 import org.sonar.server.charts.ChartFactory;
+import org.sonar.server.component.DefaultComponentFinder;
 import org.sonar.server.component.DefaultRubyComponentService;
 import org.sonar.server.configuration.Backup;
 import org.sonar.server.configuration.ProfilesManager;
@@ -263,6 +264,7 @@ public final class Platform {
     servicesContainer.addSingleton(InternalPermissionTemplateService.class);
 
     // components
+    servicesContainer.addSingleton(DefaultComponentFinder.class);
     servicesContainer.addSingleton(DefaultRubyComponentService.class);
 
     // issues
index 893983837057a53eba7f2c83764e7617d1e9d2d2..e6c9b3a947ca5853eaa93843d3c7781b84282ecb 100644 (file)
@@ -32,38 +32,19 @@ class RolesController < ApplicationController
   end
 
   def projects
-    # for backward-compatibility with versions of views plugin that do not depend on sonar 3.0
-    if java_facade.hasPlugin('views')
-      @qualifiers = (['VW'] + java_facade.getQualifiersWithProperty('hasRolePolicy').to_a).compact.uniq
-    else
-      @qualifiers = java_facade.getQualifiersWithProperty('hasRolePolicy')
-    end
-    @qualifier = params[:qualifier] || 'TRK'
-
-
-    conditions_sql = 'projects.enabled=:enabled and projects.qualifier=:qualifier and projects.copy_resource_id is null'
-    conditions_values = {:enabled => true, :qualifier => @qualifier}
-    joins = "INNER JOIN resource_index on resource_index.resource_id=projects.id "
-    joins += "and resource_index.qualifier=#{ActiveRecord::Base::sanitize(@qualifier)} "
-    if params[:q].present?
-      query = params[:q].downcase + '%'
-      joins +="and resource_index.kee like #{ActiveRecord::Base::sanitize(query)}"
-    else
-      joins += "and resource_index.position=0"
-    end
-
-    @pagination = Api::Pagination.new(params)
-    @projects=Project.all(:select => 'distinct(resource_index.resource_id),projects.id,projects.kee,projects.name,resource_index.kee as resource_index_key',
-                           :include => ['user_roles','group_roles'],
-                           :joins => joins,
-                           :conditions => [conditions_sql, conditions_values],
-                           :order => 'resource_index.kee',
-                           :offset => @pagination.offset,
-                           :limit => @pagination.limit)
-    @pagination.count=Project.count(
-        :select => 'distinct(projects.id)',
-        :joins => joins,
-        :conditions => [conditions_sql, conditions_values])
+    params['pageSize'] = 25
+    params['qualifiers'] ||= 'TRK'
+    @query_result = Internal.component_api.find(params)
+
+    @available_qualifiers = java_facade.getQualifiersWithProperty('hasRolePolicy').collect { |qualifier| [message("qualifiers.#{qualifier}"), qualifier] }.sort
+
+    # For the moment, we return projects from rails models, but it should be replaced to return java components (this will need methods on ComponentQueryResult to return roles from component)
+    @projects = Project.all(
+        :include => ['user_roles','group_roles'],
+        :conditions => ['kee in (?)', @query_result.components().to_a.collect{|component| component.key()}],
+        # Even if components are already sorted, we must sort them again as this SQL query will not keep order
+        :order => 'name'
+    )
   end
 
   def edit_users
index a5c7f070de66d1c53f73667dea249f1ba04c5e70..c324217b90bc948ac06ce95514dd2dd451cc4598 100644 (file)
               <a href="<%= ApplicationController.root_context -%>/groups/index"><%= message('user_groups.page') -%></a></li>
             <li class="<%= 'active' if request.request_uri.include?('/roles/global') -%>">
               <a href="<%= ApplicationController.root_context -%>/roles/global"><%= message('global_permissions.page') -%></a></li>
-            <li class="<%= 'active' if request.request_uri.include?('/permission_templates') -%>">
-                <a href="<%= ApplicationController.root_context -%>/permission_templates"><%= message('permission_templates.page') -%></a></li>
-            <li class="<%= 'active' if request.request_uri.include?('/roles/projects') -%>">
+            <li class="<%= 'active' if request.request_uri.include?('/roles/projects') || request.request_uri.include?('/permission_templates') -%>">
               <a href="<%= ApplicationController.root_context -%>/roles/projects"><%= message('roles.page') -%></a></li>
 
             <li class="spacer"></li>
index 7d0360d83f3ab108558ffc2384c1eea4d0a17238..2ab96d108670a3d2da39c3f257d21d8df82cd8e5 100644 (file)
@@ -1,4 +1,4 @@
-<h1><%= message('permission_templates') -%></h1>
+<%= render :partial => 'roles/tabs', :locals => {:selected_tab=>'Permission templates'} %>
 <br/>
 <h2>Default templates</h2>
 <div class="box">
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/roles/_tabs.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/roles/_tabs.html.erb
new file mode 100644 (file)
index 0000000..0de8a00
--- /dev/null
@@ -0,0 +1,13 @@
+<%
+  selected_tab = nil unless defined?(:selected_tab)
+%>
+<ul class="tabs">
+  <li>
+    <a href="<%= url_for :controller => 'roles', :action => 'projects' %>" <%= "class='selected'" if selected_tab=='Projects' -%>
+       id="tab-projects"><%= message('projects') -%></a>
+  </li>
+  <li>
+    <a href="<%= url_for :controller => 'permission_templates', :action => 'index' -%>" <%= "class='selected'" if selected_tab=='Permission templates' -%>
+       id="tab-perm-templates"><%= message('permission_templates') -%></a>
+  </li>
+</ul>
\ No newline at end of file
index 4311b9721e78adf48cef4b815e58107f06b7e6c4..d2bea8d3bfe65d64b9f3a431a68ced22b65aabe0 100644 (file)
@@ -1,17 +1,32 @@
-<h1><%= message('roles.page') -%></h1>
+<%= render :partial => 'roles/tabs', :locals => {:selected_tab=>'Projects'} %>
 
-<% if @qualifiers.size>1 %>
-  <ul class="tabs" id="qualifier-tabs">
-    <% @qualifiers.each do |q|
-      css_class = (q==@qualifier ? 'selected' : '')
-    %>
-      <li>
-        <%= link_to message("qualifiers.#{q}"), {:action => 'projects', :qualifier => q}, {:class => css_class, :id => "tab-#{u q}"} -%>
-      </li>
-    <% end %>
-  </ul>
-  <div class="tabs-panel">
-<% end %>
+<div class="tabs-panel marginbottom10 background-gray">
+  <% form_tag({:action => 'projects'}, {:id => 'project-search-form', :method => 'get'}) do %>
+    <div class="table">
+      <div class="project-search top">
+        <span class="note"><%= message('projects_role.criteria.name') -%></span><br/>
+        <% selected_name = @query_result.query.names.to_a.first if @query_result.query.names && @query_result.query.names.size == 1 %>
+        <%= text_field_tag 'names', selected_name, :id => 'search-text' %>
+      </div>
+      <div class="project-search top">
+        <span class="note"><%= message('projects_role.criteria.key') -%></span><br/>
+        <% selected_key = @query_result.query.keys.to_a.first if @query_result.query.keys && @query_result.query.keys.size == 1 %>
+        <%= text_field_tag 'keys', selected_key, :id => 'search-text' %>
+      </div>
+      <div class="project-search top">
+        <span class="note"><%= message('type') -%></span><br/>
+        <% selected_qualifier = @query_result.query.qualifiers.to_a.first if @query_result.query.qualifiers && @query_result.query.qualifiers.size == 1 %>
+        <%= dropdown_tag 'qualifiers', options_for_select(@available_qualifiers, selected_qualifier), {
+            :width => '150px'
+        }, {:id => 'search-qualifier'} -%>
+      </div>
+      <div class="project-search">
+        <br/>
+        <%= submit_tag message('search_verb'), :id => 'submit-search', :onclick => 'submitSearch();' %>
+      </div>
+    </div>
+  <% end %>
+</div>
 
 <div id="project-roles-operations" style="float: right;">
   <ul class="operations">
   </ul>
 </div>
 
-  <table class="data width100" id="projects">
-    <thead>
-    <tr>
-      <th>
-        <form action="<%= url_for :action => 'projects', :qualifier => @qualifier -%>" method="GET">
-          <input type="hidden" name="qualifier" value="<%= @qualifier -%>"/>
-          <input type="text" name="q" value="<%= params[:q] -%>" id="search_text"/>
-          <input type="submit" value="<%= message('search_verb') -%>" id="search_submit"/>
-        </form>
-      </th>
-      <th><%= message('projects_role.admin') -%></th>
-      <th><%= message('projects_role.user') -%></th>
-      <th><%= message('projects_role.codeviewer') -%></th>
-    </tr>
-    </thead>
+<table class="data width100" id="projects">
+  <thead>
+  <tr>
+    <th>&nbsp;</th>
+    <th><%= message('projects_role.admin') -%></th>
+    <th><%= message('projects_role.user') -%></th>
+    <th><%= message('projects_role.codeviewer') -%></th>
+  </tr>
+  </thead>
 
-    <%= table_pagination(@pagination, :colspan => 4) { |label, page_id| link_to(label, params.merge({:page => page_id}))} -%>
+  <%= paginate_java(@query_result.paging, :colspan => 4, :id => 'project-roles-foot', :include_loading_icon => true) { |label, page_id|
+      link_to(label, params.merge({:pageIndex => page_id}))
+    }
+  %>
 
-    <tbody>
-    <% if @projects.empty? %>
-      <tr class="even">
-        <td colspan="4" align="left"><%= message('no_results') %></td>
-      </tr>
-    <% end
+  <tbody>
+  <% if @projects.empty? %>
+    <tr class="even">
+      <td colspan="4" align="left"><%= message('no_results') %></td>
+    </tr>
+  <% end
 
-       @projects.each do |project|
-    %>
-      <tr class="<%= cycle('even', 'odd') -%>">
-        <td valign="top"><b><%= h project.name %></b><br/>
-          <span class="small gray"><%= h project.key -%></span>
-        </td>
-        <td valign="top">
-          <%
-             users=Api::Utils.insensitive_sort(project.user_roles.select { |ur| ur.role=='admin' }.map { |ur| ur.user.name })
-             groups=Api::Utils.insensitive_sort(project.group_roles.select { |gr| gr.role=='admin' }.map { |gr| group_name(gr.group) })
-          %>
-          <span id="u-admin-<%= u project.kee -%>"><%= users.join(', ') %></span>
-          (<a href="<%= ApplicationController.root_context -%>/roles/edit_users?redirect=projects&role=admin&resource=<%= project.id -%>&q=<%= u params[:q] -%>&qualifier=<%= @qualifier -%>&page=<%= params[:page] -%>" class="link-action" id="selectu-admin-<%= u project.kee -%>">select users</a>)<br/>
-          <span id="g-admin-<%= u project.kee -%>"><%= groups.join(', ') %></span>
-          (<a href="<%= ApplicationController.root_context -%>/roles/edit_groups?redirect=projects&role=admin&resource=<%= project.id -%>&q=<%= u params[:q] -%>&qualifier=<%= @qualifier -%>&page=<%= params[:page] -%>" class="link-action" id="selectg-admin-<%= u project.kee -%>">select groups</a>)
-        </td>
-        <td valign="top">
-          <%
-             users=Api::Utils.insensitive_sort(project.user_roles.select { |ur| ur.role=='user' }.map { |ur| ur.user.name })
-             groups=Api::Utils.insensitive_sort(project.group_roles.select { |gr| gr.role=='user' }.map { |gr| group_name(gr.group) })
-          %>
-          <span id="u-user-<%= u project.kee -%>"><%= users.join(', ') %></span>
-          (<a href="<%= ApplicationController.root_context -%>/roles/edit_users?redirect=projects&role=user&resource=<%= project.id -%>&q=<%= u params[:q] -%>&qualifier=<%= @qualifier -%>&page=<%= params[:page] -%>" class="link-action" id="selectu-user-<%= u project.kee -%>">select users</a>)<br/>
-          <span id="g-user-<%= u project.kee -%>"><%= groups.join(', ') %></span>
-          (<a href="<%= ApplicationController.root_context -%>/roles/edit_groups?redirect=projects&role=user&resource=<%= project.id -%>&q=<%= u params[:q] -%>&qualifier=<%= @qualifier -%>&page=<%= params[:page] -%>" class="link-action" id="selectg-user-<%= u project.kee -%>">select groups</a>)
-        </td>
-        <td valign="top">
-          <%
-             users=Api::Utils.insensitive_sort(project.user_roles.select { |ur| ur.role=='codeviewer' }.map { |ur| ur.user.name })
-             groups=Api::Utils.insensitive_sort(project.group_roles.select { |gr| gr.role=='codeviewer' }.map { |gr| group_name(gr.group) })
-          %>
-          <span id="u-codeviewer-<%= u project.kee -%>"><%= users.join(', ') %></span>
-          (<a href="<%= ApplicationController.root_context -%>/roles/edit_users?redirect=projects&role=codeviewer&resource=<%= project.id -%>&q=<%= u params[:q] -%>&qualifier=<%= @qualifier -%>&page=<%= params[:page] -%>" class="link-action" id="selectu-codeviewer-<%= u project.kee -%>">select users</a>)<br/>
-          <span id="g-codeviewer-<%= u project.kee -%>"><%= groups.join(', ') %></span>
-          (<a href="<%= ApplicationController.root_context -%>/roles/edit_groups?redirect=projects&role=codeviewer&resource=<%= project.id -%>&q=<%= u params[:q] -%>&qualifier=<%= @qualifier -%>&page=<%= params[:page] -%>" class="link-action" id="selectg-codeviewer-<%= u project.kee -%>">select groups</a>)
-        </td>
-      </tr>
-    <%
-       end %>
-    </tbody>
-  </table>
+     @projects.each do |project|
+  %>
+    <tr class="<%= cycle('even', 'odd') -%>">
+      <td valign="top"><b><%= h project.name %></b><br/>
+        <span class="small gray"><%= h project.key -%></span>
+      </td>
+      <td valign="top">
+        <%
+           users=Api::Utils.insensitive_sort(project.user_roles.select { |ur| ur.role=='admin' }.map { |ur| ur.user.name })
+           groups=Api::Utils.insensitive_sort(project.group_roles.select { |gr| gr.role=='admin' }.map { |gr| group_name(gr.group) })
+        %>
+        <span id="u-admin-<%= u project.kee -%>"><%= users.join(', ') %></span>
+        (<a href="<%= ApplicationController.root_context -%>/roles/edit_users?redirect=projects&role=admin&resource=<%= project.id -%>&q=<%= u params[:q] -%>&qualifier=<%= @qualifier -%>&page=<%= params[:page] -%>" class="link-action" id="selectu-admin-<%= u project.kee -%>">select users</a>)<br/>
+        <span id="g-admin-<%= u project.kee -%>"><%= groups.join(', ') %></span>
+        (<a href="<%= ApplicationController.root_context -%>/roles/edit_groups?redirect=projects&role=admin&resource=<%= project.id -%>&q=<%= u params[:q] -%>&qualifier=<%= @qualifier -%>&page=<%= params[:page] -%>" class="link-action" id="selectg-admin-<%= u project.kee -%>">select groups</a>)
+      </td>
+      <td valign="top">
+        <%
+           users=Api::Utils.insensitive_sort(project.user_roles.select { |ur| ur.role=='user' }.map { |ur| ur.user.name })
+           groups=Api::Utils.insensitive_sort(project.group_roles.select { |gr| gr.role=='user' }.map { |gr| group_name(gr.group) })
+        %>
+        <span id="u-user-<%= u project.kee -%>"><%= users.join(', ') %></span>
+        (<a href="<%= ApplicationController.root_context -%>/roles/edit_users?redirect=projects&role=user&resource=<%= project.id -%>&q=<%= u params[:q] -%>&qualifier=<%= @qualifier -%>&page=<%= params[:page] -%>" class="link-action" id="selectu-user-<%= u project.kee -%>">select users</a>)<br/>
+        <span id="g-user-<%= u project.kee -%>"><%= groups.join(', ') %></span>
+        (<a href="<%= ApplicationController.root_context -%>/roles/edit_groups?redirect=projects&role=user&resource=<%= project.id -%>&q=<%= u params[:q] -%>&qualifier=<%= @qualifier -%>&page=<%= params[:page] -%>" class="link-action" id="selectg-user-<%= u project.kee -%>">select groups</a>)
+      </td>
+      <td valign="top">
+        <%
+           users=Api::Utils.insensitive_sort(project.user_roles.select { |ur| ur.role=='codeviewer' }.map { |ur| ur.user.name })
+           groups=Api::Utils.insensitive_sort(project.group_roles.select { |gr| gr.role=='codeviewer' }.map { |gr| group_name(gr.group) })
+        %>
+        <span id="u-codeviewer-<%= u project.kee -%>"><%= users.join(', ') %></span>
+        (<a href="<%= ApplicationController.root_context -%>/roles/edit_users?redirect=projects&role=codeviewer&resource=<%= project.id -%>&q=<%= u params[:q] -%>&qualifier=<%= @qualifier -%>&page=<%= params[:page] -%>" class="link-action" id="selectu-codeviewer-<%= u project.kee -%>">select users</a>)<br/>
+        <span id="g-codeviewer-<%= u project.kee -%>"><%= groups.join(', ') %></span>
+        (<a href="<%= ApplicationController.root_context -%>/roles/edit_groups?redirect=projects&role=codeviewer&resource=<%= project.id -%>&q=<%= u params[:q] -%>&qualifier=<%= @qualifier -%>&page=<%= params[:page] -%>" class="link-action" id="selectg-codeviewer-<%= u project.kee -%>">select groups</a>)
+      </td>
+    </tr>
+  <%
+     end %>
+  </tbody>
+</table>
 
-<% if @qualifiers.size>1 %>
-  </div>
-<% end %>
 
 <script>
-  $('search_text').focus();
+  function submitSearch() {
+    $j("#project-search-form").submit();
+  }
+
+  $j('#search-text').focus();
 </script>
\ No newline at end of file
index 9f2edf7596a70c61dbe7ec64b498be62bd9f6e0c..afe528792a273a93a9ff82f0068bd2057b812e41 100644 (file)
@@ -2425,3 +2425,11 @@ textarea.width100 {
   font-size: 85%;
   color: #777;
 }
+
+/* ------------------- Role search styles ------------------- */
+
+.project-search {
+  display: inline-block;
+  line-height: 16px;
+  padding: 4px 2px;
+}
\ No newline at end of file
diff --git a/sonar-server/src/test/java/org/sonar/server/component/ComponentQueryTest.java b/sonar-server/src/test/java/org/sonar/server/component/ComponentQueryTest.java
new file mode 100644 (file)
index 0000000..f3dfdd8
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2013 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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 this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.component;
+
+import org.junit.Test;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static org.fest.assertions.Assertions.assertThat;
+
+public class ComponentQueryTest {
+
+  @Test
+  public void should_build_query() throws Exception {
+    ComponentQuery query = ComponentQuery.builder()
+      .keys(newArrayList("org.codehaus"))
+      .names(newArrayList("Sona"))
+      .qualifiers(newArrayList("TRK"))
+      .pageSize(10)
+      .pageIndex(2)
+      .sort(ComponentQuery.SORT_BY_NAME)
+      .asc(true)
+      .build();
+    assertThat(query.keys()).containsOnly("org.codehaus");
+    assertThat(query.names()).containsOnly("Sona");
+    assertThat(query.qualifiers()).containsOnly("TRK");
+    assertThat(query.sort()).isEqualTo(ComponentQuery.SORT_BY_NAME);
+    assertThat(query.asc()).isTrue();
+    assertThat(query.pageSize()).isEqualTo(10);
+    assertThat(query.pageIndex()).isEqualTo(2);
+  }
+
+  @Test
+  public void should_accept_null_sort() throws Exception {
+    ComponentQuery query = ComponentQuery.builder().sort(null).build();
+    assertThat(query.sort()).isNull();
+  }
+
+  @Test
+  public void should_sort_by_name_asc_by_default() throws Exception {
+    ComponentQuery query = ComponentQuery.builder().build();
+    assertThat(query.sort()).isEqualTo(ComponentQuery.SORT_BY_NAME);
+    assertThat(query.asc()).isTrue();
+  }
+
+  @Test
+  public void should_throw_exception_if_sort_is_not_valid() throws Exception {
+    try {
+      ComponentQuery.builder()
+        .sort("UNKNOWN")
+        .build();
+    } catch (Exception e) {
+      assertThat(e).isInstanceOf(IllegalArgumentException.class).hasMessage("Bad sort field: UNKNOWN");
+    }
+  }
+
+  @Test
+  public void test_default_page_index_and_size() throws Exception {
+    ComponentQuery query = ComponentQuery.builder().build();
+    assertThat(query.pageSize()).isEqualTo(ComponentQuery.DEFAULT_PAGE_SIZE);
+    assertThat(query.pageIndex()).isEqualTo(ComponentQuery.DEFAULT_PAGE_INDEX);
+  }
+
+}
diff --git a/sonar-server/src/test/java/org/sonar/server/component/ComponentsFinderSortTest.java b/sonar-server/src/test/java/org/sonar/server/component/ComponentsFinderSortTest.java
new file mode 100644 (file)
index 0000000..688009c
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2013 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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 this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.component;
+
+import org.junit.Test;
+import org.sonar.api.component.Component;
+import org.sonar.core.component.ComponentDto;
+
+import java.util.List;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static org.fest.assertions.Assertions.assertThat;
+
+public class ComponentsFinderSortTest {
+
+  @Test
+  public void should_sort_by_name() {
+    List<? extends Component> dtoList = newArrayList(
+      new ComponentDto().setKey("org.codehaus.sonar").setName("Sonar"),
+      new ComponentDto().setKey("org.apache.tika:tika").setName("Apache Tika"),
+      new ComponentDto().setKey("org.picocontainer:picocontainer-parent").setName("PicoContainer Parent"),
+      new ComponentDto().setKey("org.codehaus.sample")
+    );
+
+    ComponentQuery query = ComponentQuery.builder().sort(ComponentQuery.SORT_BY_NAME).asc(true).build();
+    ComponentsFinderSort issuesFinderSort = new ComponentsFinderSort(dtoList, query);
+
+    List<Component> result = newArrayList(issuesFinderSort.sort());
+
+    assertThat(result).hasSize(4);
+    assertThat(result.get(0).name()).isEqualTo("Apache Tika");
+    assertThat(result.get(1).name()).isEqualTo("PicoContainer Parent");
+    assertThat(result.get(2).name()).isEqualTo("Sonar");
+    assertThat(result.get(3).name()).isNull();
+  }
+
+}
diff --git a/sonar-server/src/test/java/org/sonar/server/component/DefaultComponentFinderTest.java b/sonar-server/src/test/java/org/sonar/server/component/DefaultComponentFinderTest.java
new file mode 100644 (file)
index 0000000..bba0fb7
--- /dev/null
@@ -0,0 +1,124 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2013 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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 this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.component;
+
+import org.junit.Test;
+import org.sonar.api.component.Component;
+import org.sonar.core.resource.ResourceDao;
+import org.sonar.core.resource.ResourceDto;
+import org.sonar.core.resource.ResourceQuery;
+
+import java.util.Iterator;
+import java.util.List;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class DefaultComponentFinderTest {
+
+  ResourceDao dao = mock(ResourceDao.class);
+  DefaultComponentFinder finder = new DefaultComponentFinder(dao);
+
+  @Test
+  public void should_return_all_components_when_no_parameter() {
+    List<ResourceDto> dtoList = newArrayList(
+      new ResourceDto().setKey("org.codehaus.sonar").setName("Sonar").setQualifier("TRK"),
+      new ResourceDto().setKey("org.apache.tika:tika").setName("Apache Tika").setQualifier("TRK"),
+      new ResourceDto().setKey("org.picocontainer:picocontainer-parent").setName("PicoContainer Parent").setQualifier("TRK")
+    );
+    when(dao.getResources(any(ResourceQuery.class))).thenReturn(dtoList);
+
+    ComponentQuery query = ComponentQuery.builder().build();
+    DefaultComponentQueryResult results = finder.find(query);
+
+    assertThat(results.components()).hasSize(3);
+    Component component = results.components().iterator().next();
+    assertThat(component.name()).isEqualTo("Apache Tika");
+    assertThat(component.key()).isEqualTo("org.apache.tika:tika");
+    assertThat(component.qualifier()).isEqualTo("TRK");
+  }
+
+  @Test
+  public void should_find_components_by_key_pattern() {
+    List<ResourceDto> dtoList = newArrayList(
+      new ResourceDto().setKey("org.codehaus.sonar").setName("Sonar").setQualifier("TRK"),
+      new ResourceDto().setKey("org.apache.tika:tika").setName("Apache Tika").setQualifier("TRK"),
+      new ResourceDto().setKey("org.apache.jackrabbit:jackrabbit").setName("Apache Jackrabbit").setQualifier("TRK")
+    );
+    when(dao.getResources(any(ResourceQuery.class))).thenReturn(dtoList);
+
+    ComponentQuery query = ComponentQuery.builder().keys(newArrayList("org.apache")).build();
+    assertThat(finder.find(query).components()).hasSize(2);
+  }
+
+  @Test
+  public void should_find_components_by_name_pattern() {
+    List<ResourceDto> dtoList = newArrayList(
+      new ResourceDto().setKey("org.codehaus.sonar").setName("Sonar").setQualifier("TRK"),
+      new ResourceDto().setKey("org.apache.tika:tika").setName("Apache Tika").setQualifier("TRK"),
+      new ResourceDto().setKey("org.apache.jackrabbit:jackrabbit").setName("Apache Jackrabbit").setQualifier("TRK")
+    );
+    when(dao.getResources(any(ResourceQuery.class))).thenReturn(dtoList);
+
+    ComponentQuery query = ComponentQuery.builder().names(newArrayList("Apache")).build();
+    assertThat(finder.find(query).components()).hasSize(2);
+  }
+
+  @Test
+  public void should_sort_result_by_name() {
+    List<ResourceDto> dtoList = newArrayList(
+      new ResourceDto().setKey("org.codehaus.sonar").setName("Sonar").setQualifier("TRK"),
+      new ResourceDto().setKey("org.apache.tika:tika").setName("Apache Tika").setQualifier("TRK"),
+      new ResourceDto().setKey("org.picocontainer:picocontainer-parent").setName("PicoContainer Parent").setQualifier("TRK")
+    );
+    when(dao.getResources(any(ResourceQuery.class))).thenReturn(dtoList);
+
+    ComponentQuery query = ComponentQuery.builder().build();
+    DefaultComponentQueryResult results = finder.find(query);
+
+    assertThat(results.components()).hasSize(3);
+    Iterator<? extends Component> iterator = results.components().iterator();
+    assertThat(iterator.next().name()).isEqualTo("Apache Tika");
+    assertThat(iterator.next().name()).isEqualTo("PicoContainer Parent");
+    assertThat(iterator.next().name()).isEqualTo("Sonar");
+  }
+
+  @Test
+  public void should_find_paginate_result() {
+    ComponentQuery query = ComponentQuery.builder().pageSize(1).pageIndex(1).build();
+
+    List<ResourceDto> dtoList = newArrayList(
+      new ResourceDto().setKey("org.codehaus.sonar").setName("Sonar").setQualifier("TRK"),
+      new ResourceDto().setKey("org.apache.tika:tika").setName("Apache Tika").setQualifier("TRK"),
+      new ResourceDto().setKey("org.picocontainer:picocontainer-parent").setName("PicoContainer Parent").setQualifier("TRK")
+    );
+    when(dao.getResources(any(ResourceQuery.class))).thenReturn(dtoList);
+
+    DefaultComponentQueryResult results = finder.find(query);
+    assertThat(results.paging().offset()).isEqualTo(0);
+    assertThat(results.paging().pages()).isEqualTo(3);
+    assertThat(results.paging().total()).isEqualTo(3);
+  }
+
+}
index d9911b33775b3bbedc32dbb2ea4bc197a2aba9aa..2a5cdb3051b09e63c575d1a9a11f19a972264c1f 100644 (file)
 package org.sonar.server.component;
 
 import org.junit.Before;
+import org.junit.Test;
 import org.sonar.api.component.Component;
 import org.sonar.core.resource.ResourceDao;
 
+import java.util.Map;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static com.google.common.collect.Maps.newHashMap;
 import static org.fest.assertions.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.*;
 
 public class DefaultRubyComponentServiceTest {
 
   private ResourceDao resourceDao;
+  private DefaultComponentFinder finder;
   private DefaultRubyComponentService componentService;
 
   @Before
-  public void before(){
+  public void before() {
     resourceDao = mock(ResourceDao.class);
-    componentService = new DefaultRubyComponentService(resourceDao);
+    finder = mock(DefaultComponentFinder.class);
+    componentService = new DefaultRubyComponentService(resourceDao, finder);
   }
 
-  @Before
+  @Test
   public void should_find_by_key() {
     Component component = mock(Component.class);
     when(resourceDao.findByKey("struts")).thenReturn(component);
 
     assertThat(componentService.findByKey("struts")).isEqualTo(component);
   }
+
+  @Test
+  public void should_find() {
+    Map<String, Object> map = newHashMap();
+    map.put("keys", newArrayList("org.codehaus.sonar"));
+    map.put("names", newArrayList("Sonar"));
+    map.put("qualifiers", newArrayList("TRK"));
+    map.put("pageSize", 10l);
+    map.put("pageIndex", 50);
+    map.put("sort", "NAME");
+    map.put("asc", true);
+
+    componentService.find(map);
+    verify(finder).find(any(ComponentQuery.class));
+  }
+
+  @Test
+  public void should_create_query_from_parameters() {
+    Map<String, Object> map = newHashMap();
+    map.put("keys", newArrayList("org.codehaus.sonar"));
+    map.put("names", newArrayList("Sonar"));
+    map.put("qualifiers", newArrayList("TRK"));
+    map.put("pageSize", 10l);
+    map.put("pageIndex", 50);
+    map.put("sort", "NAME");
+    map.put("asc", true);
+
+    ComponentQuery query = DefaultRubyComponentService.toQuery(map);
+    assertThat(query.keys()).containsOnly("org.codehaus.sonar");
+    assertThat(query.names()).containsOnly("Sonar");
+    assertThat(query.qualifiers()).containsOnly("TRK");
+    assertThat(query.pageSize()).isEqualTo(10);
+    assertThat(query.pageIndex()).isEqualTo(50);
+    assertThat(query.sort()).isEqualTo(ComponentQuery.SORT_BY_NAME);
+    assertThat(query.asc()).isTrue();
+  }
 }