From c5edb54175be630d973a3a994e6cb44c46d3edfb Mon Sep 17 00:00:00 2001 From: Simon Brandhof Date: Mon, 19 Dec 2011 15:00:29 +0100 Subject: [PATCH] SONAR-983 first implementation of the search engine of projects, directories and files * The POST request to the URL /search/reset starts the indexation of resources * The search engine is available at /search --- .../java/org/sonar/persistence/DaoUtils.java | 17 +- .../java/org/sonar/persistence/MyBatis.java | 2 + .../persistence/resource/ResourceDto.java | 64 +++++++ .../resource/ResourceIndexDao.java | 86 +++++++--- .../resource/ResourceIndexDto.java | 22 ++- .../resource/ResourceIndexMapper.java | 4 +- .../resource/ResourceIndexerFilter.java | 35 ++++ .../resource/ResourceIndexMapper.xml | 26 ++- .../org/sonar/persistence/schema-derby.ddl | 1 + .../plugin/AbstractPluginRepositoryTest.java | 160 ------------------ .../resource/ResourceIndexDaoTest.java | 9 + .../src/test/resources/logback-test.xml | 5 + .../ResourceIndexDaoTest/testIndex-result.xml | 12 +- .../testIndexAll-result.xml | 32 ++++ .../ResourceIndexDaoTest/testIndexAll.xml | 13 ++ .../ResourceIndexDaoTest/testSearch.xml | 22 +-- .../java/org/sonar/server/ui/JRubyFacade.java | 12 ++ .../app/controllers/search_controller.rb | 63 +++++++ .../WEB-INF/app/helpers/application_helper.rb | 52 +++--- .../WEB-INF/app/models/resource_index.rb | 30 ++++ .../WEB-INF/app/views/search/index.html.erb | 46 +++++ .../237_create_table_resource_index.rb | 2 + .../src/main/webapp/stylesheets/style.css | 9 +- 23 files changed, 490 insertions(+), 234 deletions(-) create mode 100644 sonar-core/src/main/java/org/sonar/persistence/resource/ResourceDto.java create mode 100644 sonar-core/src/main/java/org/sonar/persistence/resource/ResourceIndexerFilter.java delete mode 100644 sonar-core/src/test/java/org/sonar/core/plugin/AbstractPluginRepositoryTest.java create mode 100644 sonar-core/src/test/resources/org/sonar/persistence/resource/ResourceIndexDaoTest/testIndexAll-result.xml create mode 100644 sonar-core/src/test/resources/org/sonar/persistence/resource/ResourceIndexDaoTest/testIndexAll.xml create mode 100644 sonar-server/src/main/webapp/WEB-INF/app/controllers/search_controller.rb create mode 100644 sonar-server/src/main/webapp/WEB-INF/app/models/resource_index.rb create mode 100644 sonar-server/src/main/webapp/WEB-INF/app/views/search/index.html.erb diff --git a/sonar-core/src/main/java/org/sonar/persistence/DaoUtils.java b/sonar-core/src/main/java/org/sonar/persistence/DaoUtils.java index ce9df3619d6..64fb1b64980 100644 --- a/sonar-core/src/main/java/org/sonar/persistence/DaoUtils.java +++ b/sonar-core/src/main/java/org/sonar/persistence/DaoUtils.java @@ -19,23 +19,30 @@ */ 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> getDaoClasses() { - return Arrays.> 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); } } diff --git a/sonar-core/src/main/java/org/sonar/persistence/MyBatis.java b/sonar-core/src/main/java/org/sonar/persistence/MyBatis.java index 59331636f09..1edfd4e3236 100644 --- a/sonar-core/src/main/java/org/sonar/persistence/MyBatis.java +++ b/sonar-core/src/main/java/org/sonar/persistence/MyBatis.java @@ -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 index 00000000000..9793712838d --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/persistence/resource/ResourceDto.java @@ -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; + } +} diff --git a/sonar-core/src/main/java/org/sonar/persistence/resource/ResourceIndexDao.java b/sonar-core/src/main/java/org/sonar/persistence/resource/ResourceIndexDao.java index 395be04b431..83c86353e23 100644 --- a/sonar-core/src/main/java/org/sonar/persistence/resource/ResourceIndexDao.java +++ b/sonar-core/src/main/java/org/sonar/persistence/resource/ResourceIndexDao.java @@ -19,9 +19,13 @@ */ 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 search(String input) { - if (StringUtils.isBlank(input) || input.length() < MINIMUM_SEARCH_SIZE) { + public List 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; } } diff --git a/sonar-core/src/main/java/org/sonar/persistence/resource/ResourceIndexDto.java b/sonar-core/src/main/java/org/sonar/persistence/resource/ResourceIndexDto.java index 1a351cd0bb5..5272747b0ec 100644 --- a/sonar-core/src/main/java/org/sonar/persistence/resource/ResourceIndexDto.java +++ b/sonar-core/src/main/java/org/sonar/persistence/resource/ResourceIndexDto.java @@ -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; } } diff --git a/sonar-core/src/main/java/org/sonar/persistence/resource/ResourceIndexMapper.java b/sonar-core/src/main/java/org/sonar/persistence/resource/ResourceIndexMapper.java index 110195b37ab..2d28ed4cc95 100644 --- a/sonar-core/src/main/java/org/sonar/persistence/resource/ResourceIndexMapper.java +++ b/sonar-core/src/main/java/org/sonar/persistence/resource/ResourceIndexMapper.java @@ -23,7 +23,9 @@ import java.util.List; public interface ResourceIndexMapper { - List selectLikeKey(String key); + List 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 index 00000000000..01527375cba --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/persistence/resource/ResourceIndexerFilter.java @@ -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; + } +} diff --git a/sonar-core/src/main/resources/org/sonar/persistence/resource/ResourceIndexMapper.xml b/sonar-core/src/main/resources/org/sonar/persistence/resource/ResourceIndexMapper.xml index f2a98725242..c521824b6bc 100644 --- a/sonar-core/src/main/resources/org/sonar/persistence/resource/ResourceIndexMapper.xml +++ b/sonar-core/src/main/resources/org/sonar/persistence/resource/ResourceIndexMapper.xml @@ -6,6 +6,7 @@ + @@ -14,7 +15,7 @@ kee, position, resource_id, project_id - select from resource_index @@ -22,9 +23,28 @@ order by position asc + + + + + - 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}) diff --git a/sonar-core/src/main/resources/org/sonar/persistence/schema-derby.ddl b/sonar-core/src/main/resources/org/sonar/persistence/schema-derby.ddl index 25324f9e366..693b15d635c 100644 --- a/sonar-core/src/main/resources/org/sonar/persistence/schema-derby.ddl +++ b/sonar-core/src/main/resources/org/sonar/persistence/schema-derby.ddl @@ -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 index 7d08e633669..00000000000 --- a/sonar-core/src/test/java/org/sonar/core/plugin/AbstractPluginRepositoryTest.java +++ /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 provide() { -// calls++; -// return Arrays.asList(new B(a), new B(a)); -// } -// } - - -} diff --git a/sonar-core/src/test/java/org/sonar/persistence/resource/ResourceIndexDaoTest.java b/sonar-core/src/test/java/org/sonar/persistence/resource/ResourceIndexDaoTest.java index 8d8ec54c797..c803812edb4 100644 --- a/sonar-core/src/test/java/org/sonar/persistence/resource/ResourceIndexDaoTest.java +++ b/sonar-core/src/test/java/org/sonar/persistence/resource/ResourceIndexDaoTest.java @@ -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"); + } + } diff --git a/sonar-core/src/test/resources/logback-test.xml b/sonar-core/src/test/resources/logback-test.xml index a0c5d2f364f..9c0d4b1c3ba 100644 --- a/sonar-core/src/test/resources/logback-test.xml +++ b/sonar-core/src/test/resources/logback-test.xml @@ -19,6 +19,11 @@ + + + + + diff --git a/sonar-core/src/test/resources/org/sonar/persistence/resource/ResourceIndexDaoTest/testIndex-result.xml b/sonar-core/src/test/resources/org/sonar/persistence/resource/ResourceIndexDaoTest/testIndex-result.xml index 6b11fda8cb5..3c1544c8f9a 100644 --- a/sonar-core/src/test/resources/org/sonar/persistence/resource/ResourceIndexDaoTest/testIndex-result.xml +++ b/sonar-core/src/test/resources/org/sonar/persistence/resource/ResourceIndexDaoTest/testIndex-result.xml @@ -1,8 +1,8 @@ - - - - - - + + + + + + \ 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 index 00000000000..c1dd1bf9944 --- /dev/null +++ b/sonar-core/src/test/resources/org/sonar/persistence/resource/ResourceIndexDaoTest/testIndexAll-result.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + 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 index 00000000000..f89a5e67f25 --- /dev/null +++ b/sonar-core/src/test/resources/org/sonar/persistence/resource/ResourceIndexDaoTest/testIndexAll.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/sonar-core/src/test/resources/org/sonar/persistence/resource/ResourceIndexDaoTest/testSearch.xml b/sonar-core/src/test/resources/org/sonar/persistence/resource/ResourceIndexDaoTest/testSearch.xml index 95dc517e22c..a885e5ffa72 100644 --- a/sonar-core/src/test/resources/org/sonar/persistence/resource/ResourceIndexDaoTest/testSearch.xml +++ b/sonar-core/src/test/resources/org/sonar/persistence/resource/ResourceIndexDaoTest/testSearch.xml @@ -1,18 +1,18 @@ - - - - - + + + + + - - - - - - + + + + + + \ No newline at end of file diff --git a/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java b/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java index 0514b55e8b7..424dbef4868 100644 --- a/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java +++ b/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java @@ -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 index 00000000000..d68792f8143 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/search_controller.rb @@ -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 diff --git a/sonar-server/src/main/webapp/WEB-INF/app/helpers/application_helper.rb b/sonar-server/src/main/webapp/WEB-INF/app/helpers/application_helper.rb index 2321c8a6b00..62f4d8b44f2 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/helpers/application_helper.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/helpers/application_helper.rb @@ -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_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 index 00000000000..1db1a1cd361 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/models/resource_index.rb @@ -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 index 00000000000..d9b0de36c0d --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/search/index.html.erb @@ -0,0 +1,46 @@ +
+ + +
+ +<% if @results %> + + + + + + + + <% @results.each do |resource_index| + resource=@resources_by_id[resource_index.resource_id] + %> + + + + + + <% end %> + + + + + + +
+ <% if resource.display_dashboard? %> + + <% end %> + + <%= qualifier_icon resource -%> + + <%= link_to_resource_home resource, :name => highlight(resource.name(true), @search) -%> +
+ <% if @total>@results.size %> + <%= @results.size -%> among + <% end %> + <%= @total -%> results (<%= Time.now-@start_time -%> seconds) +
+<% end %> + \ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/237_create_table_resource_index.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/237_create_table_resource_index.rb index 8b651d244f1..c6190f0cfe6 100644 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/237_create_table_resource_index.rb +++ b/sonar-server/src/main/webapp/WEB-INF/db/migrate/237_create_table_resource_index.rb @@ -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 diff --git a/sonar-server/src/main/webapp/stylesheets/style.css b/sonar-server/src/main/webapp/stylesheets/style.css index 9fb6b224d75..c45ce56b77a 100644 --- a/sonar-server/src/main/webapp/stylesheets/style.css +++ b/sonar-server/src/main/webapp/stylesheets/style.css @@ -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; -- 2.39.5