]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-983 first implementation of the search engine of projects, directories and...
authorSimon Brandhof <simon.brandhof@gmail.com>
Mon, 19 Dec 2011 14:00:29 +0000 (15:00 +0100)
committerSimon Brandhof <simon.brandhof@gmail.com>
Mon, 19 Dec 2011 14:18:09 +0000 (15:18 +0100)
* The POST request to the URL /search/reset starts the indexation of resources

* The search engine is available at /search

23 files changed:
sonar-core/src/main/java/org/sonar/persistence/DaoUtils.java
sonar-core/src/main/java/org/sonar/persistence/MyBatis.java
sonar-core/src/main/java/org/sonar/persistence/resource/ResourceDto.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/persistence/resource/ResourceIndexDao.java
sonar-core/src/main/java/org/sonar/persistence/resource/ResourceIndexDto.java
sonar-core/src/main/java/org/sonar/persistence/resource/ResourceIndexMapper.java
sonar-core/src/main/java/org/sonar/persistence/resource/ResourceIndexerFilter.java [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/persistence/resource/ResourceIndexMapper.xml
sonar-core/src/main/resources/org/sonar/persistence/schema-derby.ddl
sonar-core/src/test/java/org/sonar/core/plugin/AbstractPluginRepositoryTest.java [deleted file]
sonar-core/src/test/java/org/sonar/persistence/resource/ResourceIndexDaoTest.java
sonar-core/src/test/resources/logback-test.xml
sonar-core/src/test/resources/org/sonar/persistence/resource/ResourceIndexDaoTest/testIndex-result.xml
sonar-core/src/test/resources/org/sonar/persistence/resource/ResourceIndexDaoTest/testIndexAll-result.xml [new file with mode: 0644]
sonar-core/src/test/resources/org/sonar/persistence/resource/ResourceIndexDaoTest/testIndexAll.xml [new file with mode: 0644]
sonar-core/src/test/resources/org/sonar/persistence/resource/ResourceIndexDaoTest/testSearch.xml
sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java
sonar-server/src/main/webapp/WEB-INF/app/controllers/search_controller.rb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/helpers/application_helper.rb
sonar-server/src/main/webapp/WEB-INF/app/models/resource_index.rb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/views/search/index.html.erb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/db/migrate/237_create_table_resource_index.rb
sonar-server/src/main/webapp/stylesheets/style.css

index ce9df3619d6268611a939779b3340205ab0f5048..64fb1b649808e6e73a9cbba0c36036f7570a6fd2 100644 (file)
  */
 package org.sonar.persistence;
 
-import java.util.Arrays;
-import java.util.List;
-
 import org.sonar.persistence.dashboard.ActiveDashboardDao;
 import org.sonar.persistence.dashboard.DashboardDao;
 import org.sonar.persistence.duplication.DuplicationDao;
+import org.sonar.persistence.resource.ResourceIndexDao;
 import org.sonar.persistence.review.ReviewDao;
 import org.sonar.persistence.rule.RuleDao;
 import org.sonar.persistence.template.LoadedTemplateDao;
 
+import java.util.Arrays;
+import java.util.List;
+
 public final class DaoUtils {
 
   private DaoUtils() {
   }
 
   public static List<Class<?>> getDaoClasses() {
-    return Arrays.<Class<?>> asList(RuleDao.class, DuplicationDao.class, ReviewDao.class, ActiveDashboardDao.class, DashboardDao.class,
-        LoadedTemplateDao.class);
+    return Arrays.asList(
+      ActiveDashboardDao.class,
+      DashboardDao.class,
+      DuplicationDao.class,
+      LoadedTemplateDao.class,
+      ResourceIndexDao.class,
+      ReviewDao.class,
+      RuleDao.class);
   }
 }
index 59331636f0998ce6ef1b81d60f4e1d04b4024d71..1edfd4e32363905dd2498485250cd44aa826fddb 100644 (file)
@@ -30,6 +30,7 @@ import org.sonar.api.ServerComponent;
 import org.sonar.persistence.dashboard.*;
 import org.sonar.persistence.duplication.DuplicationMapper;
 import org.sonar.persistence.duplication.DuplicationUnitDto;
+import org.sonar.persistence.resource.ResourceDto;
 import org.sonar.persistence.resource.ResourceIndexDto;
 import org.sonar.persistence.resource.ResourceIndexMapper;
 import org.sonar.persistence.review.ReviewDto;
@@ -62,6 +63,7 @@ public class MyBatis implements BatchComponent, ServerComponent {
     loadAlias(conf, "DuplicationUnit", DuplicationUnitDto.class);
     loadAlias(conf, "LoadedTemplate", LoadedTemplateDto.class);
     loadAlias(conf, "Review", ReviewDto.class);
+    loadAlias(conf, "Resource", ResourceDto.class);
     loadAlias(conf, "ResourceIndex", ResourceIndexDto.class);
     loadAlias(conf, "Rule", RuleDto.class);
     loadAlias(conf, "Widget", WidgetDto.class);
diff --git a/sonar-core/src/main/java/org/sonar/persistence/resource/ResourceDto.java b/sonar-core/src/main/java/org/sonar/persistence/resource/ResourceDto.java
new file mode 100644 (file)
index 0000000..9793712
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2011 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.persistence.resource;
+
+public final class ResourceDto {
+
+  private Integer id;
+  private String name;
+  private String longName;
+  private Integer rootId;
+
+  public Integer getId() {
+    return id;
+  }
+
+  public ResourceDto setId(Integer id) {
+    this.id = id;
+    return this;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public ResourceDto setName(String name) {
+    this.name = name;
+    return this;
+  }
+
+  public Integer getRootId() {
+    return rootId;
+  }
+
+  public ResourceDto setRootId(Integer rootId) {
+    this.rootId = rootId;
+    return this;
+  }
+
+  public String getLongName() {
+    return longName;
+  }
+
+  public ResourceDto setLongName(String longName) {
+    this.longName = longName;
+    return this;
+  }
+}
index 395be04b431d932c47d168855ebb845b307681eb..83c86353e239d73f32bb59817e4eb1ac7f5b177c 100644 (file)
  */
 package org.sonar.persistence.resource;
 
+import org.apache.commons.lang.ObjectUtils;
 import org.apache.commons.lang.StringUtils;
 import org.apache.ibatis.session.ExecutorType;
+import org.apache.ibatis.session.ResultContext;
+import org.apache.ibatis.session.ResultHandler;
 import org.apache.ibatis.session.SqlSession;
+import org.sonar.api.utils.TimeProfiler;
 import org.sonar.persistence.MyBatis;
 
 import java.util.Collections;
@@ -38,47 +42,87 @@ public class ResourceIndexDao {
     this.mybatis = mybatis;
   }
 
-  public List<ResourceIndexDto> search(String input) {
-    if (StringUtils.isBlank(input) || input.length() < MINIMUM_SEARCH_SIZE) {
+  public List<ResourceIndexDto> search(String keyword) {
+    if (StringUtils.isBlank(keyword) || keyword.length() < MINIMUM_SEARCH_SIZE) {
       return Collections.emptyList();
     }
     SqlSession sqlSession = mybatis.openSession();
     try {
       ResourceIndexMapper mapper = sqlSession.getMapper(ResourceIndexMapper.class);
-      return mapper.selectLikeKey(normalize(input) + "%");
+      return mapper.selectByKeyword(normalize(keyword) + "%");
     } finally {
       sqlSession.close();
     }
   }
 
-  public void index(String resourceName, int resourceId, int projectId) {
-    if (StringUtils.isBlank(resourceName)) {
+  void index(ResourceDto resource, SqlSession session) {
+    String name = resource.getName();
+    if (StringUtils.isBlank(name)) {
       return;
     }
-    String normalizedName = normalize(resourceName);
+    String normalizedName = normalize(name);
     if (normalizedName.length() >= MINIMUM_KEY_SIZE) {
-      SqlSession sqlSession = mybatis.openSession(ExecutorType.BATCH);
-      try {
-        ResourceIndexMapper mapper = sqlSession.getMapper(ResourceIndexMapper.class);
-        ResourceIndexDto dto = new ResourceIndexDto().setResourceId(resourceId).setProjectId(projectId);
-
-        for (int position = 0; position <= normalizedName.length() - MINIMUM_KEY_SIZE; position++) {
-          dto.setPosition(position);
-          dto.setKey(StringUtils.substring(normalizedName, position));
-          mapper.insert(dto);
+      ResourceIndexMapper mapper = session.getMapper(ResourceIndexMapper.class);
+
+      Integer rootId;
+      if (resource.getRootId() != null) {
+        ResourceDto root = mapper.selectRootId(resource.getRootId());
+        if (root != null) {
+          rootId = (Integer) ObjectUtils.defaultIfNull(root.getRootId(), root.getId());
+        } else {
+          rootId = resource.getRootId();
         }
+      } else {
+        rootId = resource.getId();
+      }
 
-        sqlSession.commit();
+      ResourceIndexDto dto = new ResourceIndexDto()
+        .setResourceId(resource.getId())
+        .setProjectId(rootId)
+        .setNameSize(name.length());
 
-      } finally {
-        sqlSession.close();
+      for (int position = 0; position <= normalizedName.length() - MINIMUM_KEY_SIZE; position++) {
+        dto.setPosition(position);
+        dto.setKey(StringUtils.substring(normalizedName, position));
+        mapper.insert(dto);
       }
+
+      session.commit();
+    }
+  }
+
+  public void index(String resourceName, int resourceId, int projectId) {
+    SqlSession sqlSession = mybatis.openSession();
+    try {
+      index(new ResourceDto().setId(resourceId).setName(resourceName).setRootId(projectId), sqlSession);
+
+    } finally {
+      sqlSession.close();
+    }
+  }
+
+
+  public void index(ResourceIndexerFilter filter) {
+    TimeProfiler profiler = new TimeProfiler().start("Index resources");
+    final SqlSession sqlSession = mybatis.openSession(ExecutorType.BATCH);
+    try {
+      sqlSession.select("selectResourcesToIndex", filter, new ResultHandler() {
+        public void handleResult(ResultContext context) {
+          ResourceDto resource = (ResourceDto) context.getResultObject();
+          index(resource, sqlSession);
+        }
+      });
+    } finally {
+      sqlSession.close();
+      profiler.stop();
     }
   }
 
   static String normalize(String input) {
-    String result = StringUtils.trim(input);
-    result = StringUtils.lowerCase(result);
-    return result;
+    return StringUtils.lowerCase(input);
+  }
+
+  public static boolean isValidInput(String input) {
+    return StringUtils.isNotBlank(input) && input.length() >= MINIMUM_SEARCH_SIZE;
   }
 }
index 1a351cd0bb5912faf881484d1705d14f0c1753c3..5272747b0ec734e1226702182e67f7c7aec3c88b 100644 (file)
@@ -23,6 +23,7 @@ public final class ResourceIndexDto {
 
   private String key;
   private int position;
+  private int nameSize;
   private int resourceId;
   private int projectId;
 
@@ -39,8 +40,8 @@ public final class ResourceIndexDto {
     return position;
   }
 
-  public ResourceIndexDto setPosition(int position) {
-    this.position = position;
+  public ResourceIndexDto setPosition(int i) {
+    this.position = i;
     return this;
   }
 
@@ -48,8 +49,8 @@ public final class ResourceIndexDto {
     return resourceId;
   }
 
-  public ResourceIndexDto setResourceId(int resourceId) {
-    this.resourceId = resourceId;
+  public ResourceIndexDto setResourceId(int i) {
+    this.resourceId = i;
     return this;
   }
 
@@ -57,8 +58,17 @@ public final class ResourceIndexDto {
     return projectId;
   }
 
-  public ResourceIndexDto setProjectId(int projectId) {
-    this.projectId = projectId;
+  public ResourceIndexDto setProjectId(int i) {
+    this.projectId = i;
+    return this;
+  }
+
+  public int getNameSize() {
+    return nameSize;
+  }
+
+  public ResourceIndexDto setNameSize(int i) {
+    this.nameSize = i;
     return this;
   }
 }
index 110195b37ab631f0925267a072d96da69e3921e0..2d28ed4cc957f399c17210976abf4b8b6b63f96b 100644 (file)
@@ -23,7 +23,9 @@ import java.util.List;
 
 public interface ResourceIndexMapper {
 
-  List<ResourceIndexDto> selectLikeKey(String key);
+  List<ResourceIndexDto> selectByKeyword(String keyword);
+
+  ResourceDto selectRootId(int id);
 
   void insert(ResourceIndexDto dto);
 }
diff --git a/sonar-core/src/main/java/org/sonar/persistence/resource/ResourceIndexerFilter.java b/sonar-core/src/main/java/org/sonar/persistence/resource/ResourceIndexerFilter.java
new file mode 100644 (file)
index 0000000..0152737
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2011 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.persistence.resource;
+
+import org.sonar.api.resources.Scopes;
+
+public final class ResourceIndexerFilter {
+  private boolean enabled = true;
+  private String[] scopes = new String[]{Scopes.PROJECT, Scopes.DIRECTORY, Scopes.FILE};
+
+  public boolean isEnabled() {
+    return enabled;
+  }
+
+  public String[] getScopes() {
+    return scopes;
+  }
+}
index f2a98725242cab2adafbd0dde78500643d1afa19..c521824b6bc160efb3c939ca7a06bed25d5ecb5c 100644 (file)
@@ -6,6 +6,7 @@
   <resultMap id="resourceIndexMap" type="ResourceIndex">
     <result property="key" column="kee"/>
     <result property="position" column="position"/>
+    <result property="nameSize" column="name_size"/>
     <result property="resourceId" column="resource_id"/>
     <result property="projectId" column="project_id"/>
   </resultMap>
@@ -14,7 +15,7 @@
     kee, position, resource_id, project_id
   </sql>
 
-  <select id="selectLikeKey" parameterType="String" resultMap="resourceIndexMap">
+  <select id="selectByKeyword" parameterType="String" resultMap="resourceIndexMap">
     select
     <include refid="resourceIndexColumns"/>
     from resource_index
     order by position asc
   </select>
 
+  <select id="selectResourcesToIndex" parameterType="map" resultType="resource">
+    select id, root_id as "rootId", name
+    from projects
+    where
+    enabled=#{enabled} and
+    copy_resource_id is null and
+    scope in
+    <foreach item="scope" index="index" collection="scopes"
+             open="(" separator="," close=")">#{scope}
+    </foreach>
+  </select>
+
+  <select id="selectRootId" parameterType="int" resultType="resource">
+    select id, root_id as "rootId"
+    from projects
+    where id=#{id}
+  </select>
+
+
   <insert id="insert" parameterType="ResourceIndex" useGeneratedKeys="false">
-    insert into resource_index (kee, position, resource_id, project_id)
-    values (#{key}, #{position}, #{resourceId}, #{projectId})
+    insert into resource_index (kee, position, name_size, resource_id, project_id)
+    values (#{key}, #{position}, #{nameSize}, #{resourceId}, #{projectId})
   </insert>
 </mapper>
 
index 25324f9e366fb0750572383e1a1b0960932ed4d9..693b15d635c965f9dc23ce1c5d54d65e5ba84cc3 100644 (file)
@@ -463,6 +463,7 @@ CREATE TABLE "LOADED_TEMPLATES" (
 CREATE TABLE "RESOURCE_INDEX" (
   "KEE" VARCHAR(100) NOT NULL,
   "POSITION" INTEGER NOT NULL,
+  "NAME_SIZE" INTEGER NOT NULL,
   "RESOURCE_ID" INTEGER NOT NULL,
   "PROJECT_ID" INTEGER NOT NULL
 );
diff --git a/sonar-core/src/test/java/org/sonar/core/plugin/AbstractPluginRepositoryTest.java b/sonar-core/src/test/java/org/sonar/core/plugin/AbstractPluginRepositoryTest.java
deleted file mode 100644 (file)
index 7d08e63..0000000
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * Sonar, open source software quality management tool.
- * Copyright (C) 2008-2011 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 Sonar; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
- */
-package org.sonar.core.plugin;
-
-import org.junit.Ignore;
-import org.junit.Test;
-import org.picocontainer.MutablePicoContainer;
-import org.picocontainer.PicoContainer;
-import org.sonar.api.BatchExtension;
-import org.sonar.api.ExtensionProvider;
-import org.sonar.api.Plugin;
-import org.sonar.api.ServerExtension;
-
-import java.util.Arrays;
-import java.util.Collection;
-
-import static org.hamcrest.Matchers.endsWith;
-import static org.hamcrest.Matchers.notNullValue;
-import static org.hamcrest.core.Is.is;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-@Ignore
-public class AbstractPluginRepositoryTest {
-
-//  @Test
-//  public void testIsType() {
-//    assertThat(AbstractPluginRepository.isType(FakeServerExtension.class, ServerExtension.class), is(true));
-//    assertThat(AbstractPluginRepository.isType(new FakeServerExtension(), ServerExtension.class), is(true));
-//
-//    assertThat(AbstractPluginRepository.isType(FakeBatchExtension.class, ServerExtension.class), is(false));
-//    assertThat(AbstractPluginRepository.isType(new FakeBatchExtension(), ServerExtension.class), is(false));
-//    assertThat(AbstractPluginRepository.isType(String.class, ServerExtension.class), is(false));
-//    assertThat(AbstractPluginRepository.isType("foo", ServerExtension.class), is(false));
-//  }
-//
-//  @Test
-//  public void extensionKeyshouldBeClassNameIfClass() {
-//    assertEquals(AbstractPluginRepository.getExtensionKey(FakeServerExtension.class), FakeServerExtension.class);
-//  }
-//
-//  @Test
-//  public void extensionKeyshouldBeUniqueIfObject() {
-//    assertThat((String) AbstractPluginRepository.getExtensionKey(new FakeServerExtension()), endsWith("FakeServerExtension-instance"));
-//  }
-//
-//  @Test
-//  public void shouldBeExtensionProvider() {
-//    assertThat(AbstractPluginRepository.isExtensionProvider(BProvider.class), is(true));
-//    assertThat(AbstractPluginRepository.isExtensionProvider(new BProvider(new A())), is(true));
-//  }
-//
-//  @Test
-//  public void shouldRegisterExtensionProviders() {
-//    MutablePicoContainer pico = IocContainer.buildPicoContainer();
-//    AbstractPluginRepository repository = new AbstractPluginRepository() {
-//      @Override
-//      protected boolean shouldRegisterExtension(PicoContainer container, String pluginKey, Object extension) {
-//        return isType(extension, ServerExtension.class);
-//      }
-//    };
-//
-//    Plugin plugin = mock(Plugin.class);
-//    when(plugin.getExtensions()).thenReturn(Arrays.asList(A.class, BProvider.class, B.class, C.class, D.class));
-//    repository.registerPlugin(pico, plugin, "foo");
-//    repository.invokeExtensionProviders(pico);
-//    pico.start();
-//
-//    assertThat(pico.getComponent(A.class), is(A.class));
-//    assertThat(pico.getComponent(C.class), is(C.class));
-//    assertThat(pico.getComponent(D.class), is(D.class));
-//    assertThat(pico.getComponent(C.class).getBees().length, is(3));// 1 in plugin.getExtensions() + 2 created by BProvider
-//    assertThat(pico.getComponent(D.class).getBees().length, is(3));
-//    assertThat(pico.getComponent(BProvider.class).calls, is(1)); // do not create B instances two times (C and D dependencies)
-//    assertThat(pico.getComponents(B.class).size(), is(3));
-//  }
-//
-//  public static class FakeServerExtension implements ServerExtension {
-//    @Override
-//    public String toString() {
-//      return "instance";
-//    }
-//  }
-//
-//  public static class FakeBatchExtension implements BatchExtension {
-//
-//  }
-//
-//  public static class A implements ServerExtension {
-//  }
-//
-//  public static class B implements ServerExtension {
-//    private A a;
-//
-//    public B(A a) {
-//      this.a = a;
-//    }
-//  }
-//
-//
-//  public static class C implements ServerExtension {
-//    private B[] bees;
-//
-//    public C(B[] bees) {
-//      this.bees = bees;
-//    }
-//
-//    public B[] getBees() {
-//      return bees;
-//    }
-//  }
-//
-//  public static class D implements ServerExtension {
-//    private B[] bees;
-//
-//    public D(B[] bees) {
-//      this.bees = bees;
-//    }
-//
-//    public B[] getBees() {
-//      return bees;
-//    }
-//  }
-//
-//  public static class BProvider extends ExtensionProvider implements ServerExtension {
-//
-//    private int calls = 0;
-//    private A a;
-//
-//    public BProvider(A a) {
-//      this.a = a;
-//    }
-//
-//    public Collection<B> provide() {
-//      calls++;
-//      return Arrays.asList(new B(a), new B(a));
-//    }
-//  }
-
-
-}
index 8d8ec54c7977e0771d579a967790559723380ec2..c803812edb4f1d8ab61f5af368aa5e9c6fdeb03c 100644 (file)
@@ -83,4 +83,13 @@ public class ResourceIndexDaoTest extends DaoTestCase {
     checkTables("testIndex", "resource_index");
   }
 
+  @Test
+  public void testIndexAll() {
+    setupData("testIndexAll");
+
+    dao.index(new ResourceIndexerFilter());
+
+    checkTables("testIndexAll", "resource_index");
+  }
+
 }
index a0c5d2f364f7c05b787aada76cb769eb65625402..9c0d4b1c3ba8133d82b23fed3aa8285d054189e3 100644 (file)
     <level value="WARN"/>
   </logger>
 
+  <!-- set to level DEBUG to log SQL requests executed by MyBatis -->
+  <logger name="java.sql">
+    <level value="WARN"/>
+  </logger>
+
   <root>
     <level value="INFO"/>
     <appender-ref ref="STDOUT"/>
index 6b11fda8cb540f1effeb9d3066706e3f0c3fb51b..3c1544c8f9a299db876f54b91e5320cdb26684bf 100644 (file)
@@ -1,8 +1,8 @@
 <dataset>
-  <resource_index kee="ziputils" position="0" resource_id="10" project_id="8"/>
-  <resource_index kee="iputils" position="1" resource_id="10" project_id="8"/>
-  <resource_index kee="putils" position="2" resource_id="10" project_id="8"/>
-  <resource_index kee="utils" position="3" resource_id="10" project_id="8"/>
-  <resource_index kee="tils" position="4" resource_id="10" project_id="8"/>
-  <resource_index kee="ils" position="5" resource_id="10" project_id="8"/>
+  <resource_index kee="ziputils" position="0" name_size="8" resource_id="10" project_id="8"/>
+  <resource_index kee="iputils" position="1" name_size="8" resource_id="10" project_id="8"/>
+  <resource_index kee="putils" position="2" name_size="8" resource_id="10" project_id="8"/>
+  <resource_index kee="utils" position="3" name_size="8" resource_id="10" project_id="8"/>
+  <resource_index kee="tils" position="4" name_size="8" resource_id="10" project_id="8"/>
+  <resource_index kee="ils" position="5" name_size="8" resource_id="10" project_id="8"/>
 </dataset>
\ No newline at end of file
diff --git a/sonar-core/src/test/resources/org/sonar/persistence/resource/ResourceIndexDaoTest/testIndexAll-result.xml b/sonar-core/src/test/resources/org/sonar/persistence/resource/ResourceIndexDaoTest/testIndexAll-result.xml
new file mode 100644 (file)
index 0000000..c1dd1bf
--- /dev/null
@@ -0,0 +1,32 @@
+<dataset>
+
+  <projects long_name="[null]" id="1" scope="PRJ" qualifier="TRK" kee="org.struts:struts" name="Struts"
+            root_id="[null]"
+            description="[null]"
+            enabled="[true]" language="java" copy_resource_id="[null]"/>
+
+  <projects long_name="org.struts.RequestContext" id="3" scope="FIL" qualifier="CLA" kee="org.struts:struts:org.struts.RequestContext"
+            name="RequestContext" root_id="1"
+            description="[null]"
+            enabled="[true]" language="java" copy_resource_id="[null]"/>
+
+  <!-- Struts -->
+  <resource_index kee="struts" position="0" name_size="6" resource_id="1" project_id="1"/>
+  <resource_index kee="truts" position="1" name_size="6" resource_id="1" project_id="1"/>
+  <resource_index kee="ruts" position="2" name_size="6" resource_id="1" project_id="1"/>
+  <resource_index kee="uts" position="3" name_size="6" resource_id="1" project_id="1"/>
+
+  <!-- RequestContext -->
+  <resource_index kee="requestcontext" position="0" name_size="14" resource_id="3" project_id="1"/>
+  <resource_index kee="equestcontext" position="1" name_size="14" resource_id="3" project_id="1"/>
+  <resource_index kee="questcontext" position="2" name_size="14" resource_id="3" project_id="1"/>
+  <resource_index kee="uestcontext" position="3" name_size="14" resource_id="3" project_id="1"/>
+  <resource_index kee="estcontext" position="4" name_size="14" resource_id="3" project_id="1"/>
+  <resource_index kee="stcontext" position="5" name_size="14" resource_id="3" project_id="1"/>
+  <resource_index kee="tcontext" position="6" name_size="14" resource_id="3" project_id="1"/>
+  <resource_index kee="context" position="7" name_size="14" resource_id="3" project_id="1"/>
+  <resource_index kee="ontext" position="8" name_size="14" resource_id="3" project_id="1"/>
+  <resource_index kee="ntext" position="9" name_size="14" resource_id="3" project_id="1"/>
+  <resource_index kee="text" position="10" name_size="14" resource_id="3" project_id="1"/>
+  <resource_index kee="ext" position="11" name_size="14" resource_id="3" project_id="1"/>
+</dataset>
diff --git a/sonar-core/src/test/resources/org/sonar/persistence/resource/ResourceIndexDaoTest/testIndexAll.xml b/sonar-core/src/test/resources/org/sonar/persistence/resource/ResourceIndexDaoTest/testIndexAll.xml
new file mode 100644 (file)
index 0000000..f89a5e6
--- /dev/null
@@ -0,0 +1,13 @@
+<dataset>
+
+  <projects long_name="[null]" id="1" scope="PRJ" qualifier="TRK" kee="org.struts:struts" name="Struts"
+            root_id="[null]"
+            description="[null]"
+            enabled="[true]" language="java" copy_resource_id="[null]"/>
+
+  <projects long_name="org.struts.RequestContext" id="3" scope="FIL" qualifier="CLA" kee="org.struts:struts:org.struts.RequestContext"
+            name="RequestContext" root_id="1"
+            description="[null]"
+            enabled="[true]" language="java" copy_resource_id="[null]"/>
+
+</dataset>
index 95dc517e22ca64288d0a16b9e8c7fe18eeda6872..a885e5ffa722b7594b9477adec511e9051f688dd 100644 (file)
@@ -1,18 +1,18 @@
 <dataset>
 
   <!-- ZipUtils -->
-  <resource_index kee="ziputils" position="0" resource_id="10" project_id="8"/>
-  <resource_index kee="iputils" position="1" resource_id="10" project_id="8"/>
-  <resource_index kee="putils" position="2" resource_id="10" project_id="8"/>
-  <resource_index kee="utils" position="3" resource_id="10" project_id="8"/>
-  <resource_index kee="tils" position="4" resource_id="10" project_id="8"/>
+  <resource_index kee="ziputils" position="0" name_size="8" resource_id="10" project_id="8"/>
+  <resource_index kee="iputils" position="1" name_size="8" resource_id="10" project_id="8"/>
+  <resource_index kee="putils" position="2" name_size="8" resource_id="10" project_id="8"/>
+  <resource_index kee="utils" position="3" name_size="8" resource_id="10" project_id="8"/>
+  <resource_index kee="tils" position="4" name_size="8" resource_id="10" project_id="8"/>
 
   <!-- DateUtils -->
-  <resource_index kee="dateutils" position="0" resource_id="130" project_id="120"/>
-  <resource_index kee="ateutils" position="1" resource_id="130" project_id="120"/>
-  <resource_index kee="teutils" position="2" resource_id="130" project_id="120"/>
-  <resource_index kee="eutils" position="3" resource_id="130" project_id="120"/>
-  <resource_index kee="utils" position="4" resource_id="130" project_id="120"/>
-  <resource_index kee="tils" position="5" resource_id="130" project_id="120"/>
+  <resource_index kee="dateutils" position="0" name_size="9" resource_id="130" project_id="120"/>
+  <resource_index kee="ateutils" position="1" name_size="9" resource_id="130" project_id="120"/>
+  <resource_index kee="teutils" position="2" name_size="9" resource_id="130" project_id="120"/>
+  <resource_index kee="eutils" position="3" name_size="9" resource_id="130" project_id="120"/>
+  <resource_index kee="utils" position="4" name_size="9" resource_id="130" project_id="120"/>
+  <resource_index kee="tils" position="5" name_size="9" resource_id="130" project_id="120"/>
 
 </dataset>
\ No newline at end of file
index 0514b55e8b7cb6ca9cee064bfa0f60f9abbafd6a..424dbef486855743ab1cd6acc295981aa7c8cc8e 100644 (file)
@@ -37,6 +37,8 @@ import org.sonar.core.i18n.RuleI18nManager;
 import org.sonar.markdown.Markdown;
 import org.sonar.persistence.Database;
 import org.sonar.persistence.DatabaseMigrator;
+import org.sonar.persistence.resource.ResourceIndexDao;
+import org.sonar.persistence.resource.ResourceIndexerFilter;
 import org.sonar.server.configuration.Backup;
 import org.sonar.server.configuration.ProfilesManager;
 import org.sonar.server.filters.Filter;
@@ -378,4 +380,14 @@ public final class JRubyFacade {
   public ComponentContainer getContainer() {
     return Platform.getInstance().getContainer();
   }
+
+
+  // RESOURCE SEARCH ENGINE
+  public void indexResources() {
+    getContainer().getComponentByType(ResourceIndexDao.class).index(new ResourceIndexerFilter());
+  }
+
+  public boolean isValidResourceSearchInput(String input) {
+    return ResourceIndexDao.isValidInput(input);
+  }
 }
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/search_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/search_controller.rb
new file mode 100644 (file)
index 0000000..d68792f
--- /dev/null
@@ -0,0 +1,63 @@
+#
+# Sonar, entreprise quality control tool.
+# Copyright (C) 2008-2011 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 Sonar; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+#
+class SearchController < ApplicationController
+
+  SECTION=Navigation::SECTION_HOME
+
+  verify :method => :post, :only => [:reset]
+  before_filter :admin_required, :except => ['index']
+
+  # Do not exceed 1000 because of the Oracle limition on IN statements
+  MAX_RESULTS = 50
+
+  def index
+    @start_time = Time.now
+    @search = params[:s]
+    if @search
+      if java_facade.isValidResourceSearchInput(@search.to_s)
+        normalized_search = @search.downcase
+        @results = ResourceIndex.find(:all,
+                                      :conditions => ["resource_index.kee like ?", normalized_search + '%'],
+                                      :order => 'name_size, position')
+
+        @results = select_authorized(:user, @results)
+        @total = @results.size
+        @results = @results[0...MAX_RESULTS]
+
+        @resources_by_id = {}
+        unless @results.empty?
+          Project.find(:all, :conditions => ['id in (?)', @results.map { |resource_index| resource_index.resource_id }]).each do |resource|
+            @resources_by_id[resource.id]=resource
+          end
+        end
+      else
+        flash[:warning]='Please refine your search'
+      end
+    end
+  end
+
+  # Start indexing resources
+  #
+  # curl -v -u admin:admin -X POST http://localhost:9000/search/reset
+  def reset
+    java_facade.indexResources()
+    render :text => 'indexing'
+  end
+end
index 2321c8a6b00f2ea27a0390add1c6ad3e9d595851..62f4d8b44f2ce45babfdaf83de1ac847d3a57b1a 100644 (file)
@@ -93,7 +93,7 @@ module ApplicationHelper
         if date
           label = message('since_version_detailed', :params => [mode_param.to_s, date.strftime("%Y %b %d").to_s])
         else
-        label = message('since_version', :params => mode_param.to_s)
+          label = message('since_version', :params => mode_param.to_s)
         end
       elsif mode=='previous_analysis'
         if !date.nil?
@@ -232,7 +232,7 @@ module ApplicationHelper
     end
 
     url_params={:controller => 'drilldown', :action => 'measures', :metric => metric_key, :id => options[:resource]||@project.id}
-    
+
     url_for(options.merge(url_params))
   end
 
@@ -341,6 +341,20 @@ module ApplicationHelper
   end
 
 
+  def link_to_resource_home(resource, options={})
+    period_index=options[:period]
+    period_index=nil if period_index && period_index<=0
+    if resource.display_dashboard?
+      link_to(options[:name] || resource.name, {:controller => 'dashboard', :action => 'index', :id => (resource.copy_resource_id||resource.id), :period => period_index, :tab => options[:tab], :rule => options[:rule]}, :title => options[:title])
+    else
+      if options[:line]
+        anchor= 'L' + options[:line].to_s
+      end
+      link_to(options[:name] || resource.name, {:controller => 'resource', :action => 'index', :anchor => anchor, :id => resource.id, :period => period_index, :tab => options[:tab], :rule => options[:rule], :metric => options[:metric]}, :popup => ['resource', 'height=800,width=900,scrollbars=1,resizable=1'], :title => options[:title])
+    end
+  end
+
+
   #
   #
   # JFree Eastwood is a partial implementation of Google Chart Api
@@ -423,8 +437,8 @@ module ApplicationHelper
       initial_tooltip=message('click_to_remove_from_favourites')
     end
 
-    link_to_remote('', :url => { :controller => 'favourites', :action => 'toggle', :id => resource_id, :elt => html_id},
-      :method => :post, :html => {:class => initial_class, :id => html_id, :alt => initial_tooltip, :title => initial_tooltip})
+    link_to_remote('', :url => {:controller => 'favourites', :action => 'toggle', :id => resource_id, :elt => html_id},
+                   :method => :post, :html => {:class => initial_class, :id => html_id, :alt => initial_tooltip, :title => initial_tooltip})
   end
 
   #
@@ -453,12 +467,12 @@ module ApplicationHelper
     filename = m.tendency.to_s
 
     case m.tendency_qualitative
-    when 0
-      filename+= '-black'
-    when -1
-      filename+= '-red'
-    when 1
-      filename+= '-green'
+      when 0
+        filename+= '-black'
+      when -1
+        filename+= '-red'
+      when 1
+        filename+= '-green'
     end
     image_tag("tendency/#{filename}-small.png")
   end
@@ -550,7 +564,7 @@ module ApplicationHelper
     html = options[:default].to_s if html.nil? && options[:default]
     html
   end
-  
+
   #
   # Creates a pagination section for the given array (items_array) if its size exceeds the pagination size (default: 20).
   # Upon completion of this method, the HTML is returned and the given array contains only the selected elements.
@@ -563,8 +577,8 @@ module ApplicationHelper
   # 
   def paginate(items_array, options={})
     html = items_array.size.to_s + " " + message('results').downcase
-    
-    page_size = options[:page_size] || 20    
+
+    page_size = options[:page_size] || 20
     if items_array.size > page_size
       # computes the pagination elements
       page_id = (params[:page_id] ? params[:page_id].to_i : 1)
@@ -573,7 +587,7 @@ module ApplicationHelper
       from = (page_id-1) * page_size
       to = (page_id*page_size)-1
       to = items_array.size-1 if to >= items_array.size
-      
+
       # render the pagination links
       html += " | "
       html += link_to_if page_id>1, message('paging_previous'), {:overwrite_params => {:page_id => page_id-1}}
@@ -583,14 +597,14 @@ module ApplicationHelper
         html += " "
       end
       html += link_to_if page_id<page_count, message('paging_next'), {:overwrite_params => {:page_id => 1+page_id}}
-      
+
       # and adapt the items_array object according to the pagination
       items_to_keep = items_array[from..to]
       items_array.clear
-      items_to_keep.each {|i| items_array << i} 
+      items_to_keep.each { |i| items_array << i }
     end
-    
-    html    
+
+    html
   end
-  
+
 end
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/models/resource_index.rb b/sonar-server/src/main/webapp/WEB-INF/app/models/resource_index.rb
new file mode 100644 (file)
index 0000000..1db1a1c
--- /dev/null
@@ -0,0 +1,30 @@
+#
+# Sonar, entreprise quality control tool.
+# Copyright (C) 2008-2011 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 Sonar; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+#
+class ResourceIndex < ActiveRecord::Base
+
+  set_table_name 'resource_index'
+
+  belongs_to :resource, :class_name => 'Project', :foreign_key => 'resource_id'
+  belongs_to :project, :class_name => 'Project', :foreign_key => 'project_id'
+
+  def resource_id_for_authorization
+    project_id
+  end
+end
\ No newline at end of file
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/search/index.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/search/index.html.erb
new file mode 100644 (file)
index 0000000..d9b0de3
--- /dev/null
@@ -0,0 +1,46 @@
+<form method="GET" action="index">
+  <input type="text" name="s" id="searchText" value="<%= @search -%>">
+  <input type="submit" value="Search" id="submitSearch">
+</form>
+
+<% if @results %>
+  <table class="data width100" id="searchResults">
+    <thead>
+    <tr>
+      <th colspan="3"></th>
+    </tr>
+    </thead>
+    <tbody>
+    <% @results.each do |resource_index|
+      resource=@resources_by_id[resource_index.resource_id]
+    %>
+      <tr class="<%= cycle('even', 'odd') -%>">
+        <td class="thin nowrap">
+          <% if resource.display_dashboard? %>
+            <a href="<%= ApplicationController.root_context -%>/components/index/<%= resource.id -%>"><img src="<%= ApplicationController.root_context -%>/images/zoom.png"></a>
+          <% end %>
+        </td>
+        <td class="thin nowrap">
+          <%= qualifier_icon resource -%>
+        </td>
+        <td>
+          <%= link_to_resource_home resource, :name => highlight(resource.name(true), @search) -%>
+        </td>
+      </tr>
+    <% end %>
+    </tbody>
+    <tfoot>
+    <tr>
+      <td colspan="3">
+        <% if @total>@results.size %>
+          <%= @results.size -%> among
+        <% end %>
+        <%= @total -%> results (<%= Time.now-@start_time -%> seconds)
+      </td>
+    </tr>
+    </tfoot>
+  </table>
+<% end %>
+<script type="text/javascript">
+  $('searchText').focus();
+</script>
\ No newline at end of file
index 8b651d244f1adef2b53c2e118e80a3173f78e953..c6190f0cfe691dcc89f3fd8bdf318880df614bae 100644 (file)
@@ -27,10 +27,12 @@ class CreateTableResourceIndex < ActiveRecord::Migration
     create_table 'resource_index', :id => false do |t|
       t.column 'kee', :string, :null => false, :limit => 100
       t.column 'position', :integer, :null => false
+      t.column 'name_size', :integer, :null => false
       t.column 'resource_id', :integer, :null => false
       t.column 'project_id', :integer, :null => false
     end
     add_index 'resource_index', 'kee', :name => 'resource_index_key'
+    add_index 'resource_index', 'resource_id', :name => 'resource_index_rid'
   end
 
 end
index 9fb6b224d750f8d087fb7cf11b7bd9e0ee1873b4..c45ce56b77a8ef85bca162acefaf1c7e56b33651 100644 (file)
@@ -491,6 +491,10 @@ h4, .h4 {
   color: #777;
 }
 
+.highlight {
+  font-weight: bold;
+}
+
 .subtitle {
   color: #777;
   font-size: 85%;
@@ -1239,7 +1243,9 @@ div.progress td {
 }
 
 div.progress td a {
-  display: block; width: 100%; height: 100%;
+  display: block;
+  width: 100%;
+  height: 100%;
 }
 
 div.progress td.resolved {
@@ -1257,7 +1263,6 @@ div.progress div.note {
   white-space: nowrap;
 }
 
-
 /* AUTOCOMPLETE FIELDS */
 div.autocomplete {
   position: absolute;