]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6428 WS to get component navigation information
authorJean-Baptiste Lievremont <jean-baptiste.lievremont@sonarsource.com>
Tue, 21 Apr 2015 10:15:11 +0000 (12:15 +0200)
committerJean-Baptiste Lievremont <jean-baptiste.lievremont@sonarsource.com>
Tue, 28 Apr 2015 07:02:06 +0000 (09:02 +0200)
27 files changed:
server/sonar-server/src/main/java/org/sonar/server/measure/persistence/MeasureDao.java
server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java
server/sonar-server/src/main/java/org/sonar/server/ui/Views.java
server/sonar-server/src/main/java/org/sonar/server/ui/package-info.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/ui/ws/ComponentConfigurationPages.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/ui/ws/ComponentNavigationAction.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/ui/ws/package-info.java [new file with mode: 0644]
server/sonar-server/src/main/resources/org/sonar/server/ui/ws/example-component.json [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/ui/ws/ComponentConfigurationPagesTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/ui/ws/ComponentNavigationActionTest.java [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/ui/ws/ComponentNavigationActionTest/breadcrumbs.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/ui/ws/ComponentNavigationActionTest/no_snapshot.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/ui/ws/ComponentNavigationActionTest/no_snapshot_user_favourite.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/ui/ws/ComponentNavigationActionTest/quality_profile_admin.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/ui/ws/ComponentNavigationActionTest/with_admin_rights.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/ui/ws/ComponentNavigationActionTest/with_dashboards.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/ui/ws/ComponentNavigationActionTest/with_extensions.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/ui/ws/ComponentNavigationActionTest/with_snapshot_and_connected_user.json [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/dashboard/ActiveDashboardDao.java
sonar-core/src/main/java/org/sonar/core/dashboard/ActiveDashboardMapper.java
sonar-core/src/main/java/org/sonar/core/measure/db/MeasureMapper.java
sonar-core/src/main/resources/org/sonar/core/dashboard/ActiveDashboardMapper.xml
sonar-core/src/main/resources/org/sonar/core/measure/db/MeasureMapper.xml
sonar-core/src/test/java/org/sonar/core/dashboard/ActiveDashboardDaoTest.java
sonar-core/src/test/resources/org/sonar/core/dashboard/ActiveDashboardDaoTest/shouldSelectProjectDashboardsForAnonymous.xml [new file with mode: 0644]
sonar-core/src/test/resources/org/sonar/core/dashboard/ActiveDashboardDaoTest/shouldSelectProjectDashboardsForUser.xml [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/resources/ResourceTypes.java

index 7b1b40db23d3a6850a2d9c8acca9bacc61663c3c..9e0f38d405ea3a1645ad44a14b2cfbd1875f2f11 100644 (file)
@@ -29,6 +29,7 @@ import org.sonar.core.persistence.DaoUtils;
 import org.sonar.core.persistence.DbSession;
 
 import javax.annotation.CheckForNull;
+
 import java.util.List;
 
 public class MeasureDao implements ServerComponent, DaoComponent {
@@ -55,6 +56,10 @@ public class MeasureDao implements ServerComponent, DaoComponent {
     mapper(session).insert(measureDto);
   }
 
+  public List<String> selectMetricKeysForSnapshot(DbSession session, Long snapshotId) {
+    return mapper(session).selectMetricKeysForSnapshot(snapshotId);
+  }
+
   private MeasureMapper mapper(DbSession session) {
     return session.getMapper(MeasureMapper.class);
   }
index b0e7accb7619c71857ed4c650f94ef82b0f5ab60..e455d67390ab0abde3842b39dc3a8a4f2a74f27c 100644 (file)
@@ -354,6 +354,8 @@ import org.sonar.server.text.RubyTextService;
 import org.sonar.server.ui.JRubyI18n;
 import org.sonar.server.ui.PageDecorations;
 import org.sonar.server.ui.Views;
+import org.sonar.server.ui.ws.ComponentConfigurationPages;
+import org.sonar.server.ui.ws.ComponentNavigationAction;
 import org.sonar.server.ui.ws.GlobalNavigationAction;
 import org.sonar.server.ui.ws.NavigationWs;
 import org.sonar.server.ui.ws.SettingsNavigationAction;
@@ -923,6 +925,8 @@ class ServerComponents {
     // UI
     pico.addSingleton(GlobalNavigationAction.class);
     pico.addSingleton(SettingsNavigationAction.class);
+    pico.addSingleton(ComponentConfigurationPages.class);
+    pico.addSingleton(ComponentNavigationAction.class);
     pico.addSingleton(NavigationWs.class);
 
     for (Object components : level4AddedComponents) {
index bf9201934e24bdb3032de3b28aa2e997e9e5ba1c..a66890c29ceee199728ec13f9bd6a68250969cf6 100644 (file)
@@ -28,6 +28,8 @@ import org.sonar.api.web.Page;
 import org.sonar.api.web.View;
 import org.sonar.api.web.Widget;
 
+import javax.annotation.Nullable;
+
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -70,7 +72,8 @@ public class Views implements ServerComponent {
     return getPages(section, null, null, null, null);
   }
 
-  public List<ViewProxy<Page>> getPages(String section, String resourceScope, String resourceQualifier, String resourceLanguage, String[] availableMeasures) {
+  public List<ViewProxy<Page>> getPages(String section,
+    @Nullable String resourceScope, @Nullable String resourceQualifier, @Nullable String resourceLanguage, @Nullable String[] availableMeasures) {
     List<ViewProxy<Page>> result = Lists.newArrayList();
     for (ViewProxy<Page> proxy : pages) {
       if (accept(proxy, section, resourceScope, resourceQualifier, resourceLanguage, availableMeasures)) {
@@ -109,7 +112,8 @@ public class Views implements ServerComponent {
     return Lists.newArrayList(widgets);
   }
 
-  protected static boolean accept(ViewProxy proxy, String section, String resourceScope, String resourceQualifier, String resourceLanguage, String[] availableMeasures) {
+  protected static boolean accept(ViewProxy proxy,
+    @Nullable String section, @Nullable String resourceScope, @Nullable String resourceQualifier, @Nullable String resourceLanguage, @Nullable String[] availableMeasures) {
     return acceptNavigationSection(proxy, section)
       && acceptResourceScope(proxy, resourceScope)
       && acceptResourceQualifier(proxy, resourceQualifier)
@@ -117,23 +121,23 @@ public class Views implements ServerComponent {
       && acceptAvailableMeasures(proxy, availableMeasures);
   }
 
-  protected static boolean acceptResourceLanguage(ViewProxy proxy, String resourceLanguage) {
+  protected static boolean acceptResourceLanguage(ViewProxy proxy, @Nullable String resourceLanguage) {
     return resourceLanguage == null || ArrayUtils.isEmpty(proxy.getResourceLanguages()) || ArrayUtils.contains(proxy.getResourceLanguages(), resourceLanguage);
   }
 
-  protected static boolean acceptResourceScope(ViewProxy proxy, String resourceScope) {
+  protected static boolean acceptResourceScope(ViewProxy proxy, @Nullable String resourceScope) {
     return resourceScope == null || ArrayUtils.isEmpty(proxy.getResourceScopes()) || ArrayUtils.contains(proxy.getResourceScopes(), resourceScope);
   }
 
-  protected static boolean acceptResourceQualifier(ViewProxy proxy, String resourceQualifier) {
+  protected static boolean acceptResourceQualifier(ViewProxy proxy, @Nullable String resourceQualifier) {
     return resourceQualifier == null || ArrayUtils.isEmpty(proxy.getResourceQualifiers()) || ArrayUtils.contains(proxy.getResourceQualifiers(), resourceQualifier);
   }
 
-  protected static boolean acceptNavigationSection(ViewProxy proxy, String section) {
+  protected static boolean acceptNavigationSection(ViewProxy proxy, @Nullable String section) {
     return proxy.isWidget() || section == null || ArrayUtils.contains(proxy.getSections(), section);
   }
 
-  protected static boolean acceptAvailableMeasures(ViewProxy proxy, String[] availableMeasures) {
+  protected static boolean acceptAvailableMeasures(ViewProxy proxy, @Nullable String[] availableMeasures) {
     return availableMeasures == null || proxy.acceptsAvailableMeasures(availableMeasures);
   }
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/ui/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/ui/package-info.java
new file mode 100644 (file)
index 0000000..bdd8783
--- /dev/null
@@ -0,0 +1,24 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.ui;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/server/sonar-server/src/main/java/org/sonar/server/ui/ws/ComponentConfigurationPages.java b/server/sonar-server/src/main/java/org/sonar/server/ui/ws/ComponentConfigurationPages.java
new file mode 100644 (file)
index 0000000..4f0edc6
--- /dev/null
@@ -0,0 +1,156 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 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.ui.ws;
+
+import com.google.common.base.Charsets;
+import com.google.common.collect.Lists;
+import org.sonar.api.ServerComponent;
+import org.sonar.api.i18n.I18n;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.resources.ResourceType;
+import org.sonar.api.resources.ResourceTypes;
+import org.sonar.api.utils.text.JsonWriter;
+import org.sonar.api.web.UserRole;
+import org.sonar.core.component.ComponentDto;
+import org.sonar.server.user.UserSession;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.List;
+import java.util.Locale;
+
+public class ComponentConfigurationPages implements ServerComponent {
+
+  static final String PROPERTY_COMPARABLE = "comparable";
+  static final String PROPERTY_CONFIGURABLE = "configurable";
+  static final String PROPERTY_HAS_ROLE_POLICY = "hasRolePolicy";
+  static final String PROPERTY_MODIFIABLE_HISTORY = "modifiable_history";
+  static final String PROPERTY_UPDATABLE_KEY = "updatable_key";
+  static final String PROPERTY_DELETABLE = "deletable";
+
+  private final I18n i18n;
+  private final ResourceTypes resourceTypes;
+
+  public ComponentConfigurationPages(I18n i18n, ResourceTypes resourceTypes) {
+    this.i18n = i18n;
+    this.resourceTypes = resourceTypes;
+  }
+
+  List<ConfigPage> getConfigPages(ComponentDto component, UserSession userSession) {
+    boolean isAdmin = userSession.hasProjectPermissionByUuid(UserRole.ADMIN, component.projectUuid());
+    boolean isProject = Qualifiers.PROJECT.equals(component.qualifier());
+    Locale locale = userSession.locale();
+    String componentKey = encodeComponentKey(component);
+
+    List<ConfigPage> configPages = Lists.newArrayList();
+
+    configPages.add(new ConfigPage(
+      isAdmin && componentTypeHasProperty(component, PROPERTY_CONFIGURABLE),
+      String.format("/project/settings?id=%s", componentKey),
+      i18n.message(locale, "project_settings.page", null)));
+
+    configPages.add(new ConfigPage(
+      isProject,
+      String.format("/project/profile?id=%s", componentKey),
+      i18n.message(locale, "project_quality_profiles.page", null)));
+
+    configPages.add(new ConfigPage(
+      isProject,
+      String.format("/project/qualitygate?id=%s", componentKey),
+      i18n.message(locale, "project_quality_gate.page", null)));
+
+    configPages.add(new ConfigPage(
+      isAdmin,
+      String.format("/manual_measures/index?id=%s", componentKey),
+      i18n.message(locale, "manual_measures.page", null)));
+
+    configPages.add(new ConfigPage(
+      isAdmin && isProject,
+      String.format("/action_plans/index?id=%s", componentKey),
+      i18n.message(locale, "action_plans.page", null)));
+
+    configPages.add(new ConfigPage(
+      isAdmin && isProject,
+      String.format("/project/links?id=%s", componentKey),
+      i18n.message(locale, "action_plans.page", null)));
+
+    configPages.add(new ConfigPage(
+      componentTypeHasProperty(component, PROPERTY_HAS_ROLE_POLICY),
+      String.format("/project_roles/index?id=%s", componentKey),
+      i18n.message(locale, "permissions.page", null)));
+
+    configPages.add(new ConfigPage(
+      componentTypeHasProperty(component, PROPERTY_MODIFIABLE_HISTORY),
+      String.format("/project/history?id=%s", componentKey),
+      i18n.message(locale, "project_history.page", null)));
+
+    configPages.add(new ConfigPage(
+      componentTypeHasProperty(component, PROPERTY_UPDATABLE_KEY),
+      String.format("/project/key?id=%s", componentKey),
+      i18n.message(locale, "update_key.page", null)));
+
+    configPages.add(new ConfigPage(
+      componentTypeHasProperty(component, PROPERTY_DELETABLE),
+      String.format("/project/deletion?id=%s", componentKey),
+      i18n.message(locale, "deletion.page", null)));
+
+    return configPages;
+  }
+
+  static String encodeComponentKey(ComponentDto component) {
+    String componentKey = component.getKey();
+    try {
+      componentKey = URLEncoder.encode(componentKey, Charsets.UTF_8.name());
+    } catch (UnsupportedEncodingException unknownEncoding) {
+      throw new IllegalStateException(unknownEncoding);
+    }
+    return componentKey;
+  }
+
+  boolean componentTypeHasProperty(ComponentDto component, String resourceTypeProperty) {
+    ResourceType resourceType = resourceTypes.get(component.qualifier());
+    if (resourceType != null) {
+      return resourceType.getBooleanProperty(resourceTypeProperty);
+    }
+    return false;
+  }
+
+  static class ConfigPage {
+    private final boolean visible;
+    private final String url;
+    private final String name;
+
+    ConfigPage(boolean visible, String url, String name) {
+      this.visible = visible;
+      this.url = url;
+      this.name = name;
+    }
+
+    void write(JsonWriter json) {
+      if (visible) {
+        json.beginObject()
+          .prop("url", url)
+          .prop("name", name)
+          .endObject();
+      }
+    }
+  }
+
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/ui/ws/ComponentNavigationAction.java b/server/sonar-server/src/main/java/org/sonar/server/ui/ws/ComponentNavigationAction.java
new file mode 100644 (file)
index 0000000..8da3efa
--- /dev/null
@@ -0,0 +1,227 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 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.ui.ws;
+
+import com.google.common.collect.Lists;
+import org.sonar.api.i18n.I18n;
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.server.ws.WebService.NewAction;
+import org.sonar.api.server.ws.WebService.NewController;
+import org.sonar.api.utils.DateUtils;
+import org.sonar.api.utils.text.JsonWriter;
+import org.sonar.api.web.NavigationSection;
+import org.sonar.api.web.Page;
+import org.sonar.api.web.UserRole;
+import org.sonar.core.component.ComponentDto;
+import org.sonar.core.component.SnapshotDto;
+import org.sonar.core.dashboard.ActiveDashboardDao;
+import org.sonar.core.dashboard.DashboardDto;
+import org.sonar.core.permission.GlobalPermissions;
+import org.sonar.core.persistence.DbSession;
+import org.sonar.core.properties.PropertyDto;
+import org.sonar.core.properties.PropertyQuery;
+import org.sonar.server.db.DbClient;
+import org.sonar.server.ui.ViewProxy;
+import org.sonar.server.ui.Views;
+import org.sonar.server.ui.ws.ComponentConfigurationPages.ConfigPage;
+import org.sonar.server.user.UserSession;
+
+import javax.annotation.Nullable;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+public class ComponentNavigationAction implements NavigationAction {
+
+  private static final String PARAM_COMPONENT_KEY = "componentKey";
+
+  private final DbClient dbClient;
+  private final ActiveDashboardDao activeDashboardDao;
+  private final Views views;
+  private final I18n i18n;
+  private final ComponentConfigurationPages projectConfiguration;
+
+  public ComponentNavigationAction(DbClient dbClient, ActiveDashboardDao activeDashboardDao, Views views, I18n i18n,
+    ComponentConfigurationPages projectConfiguration) {
+    this.dbClient = dbClient;
+    this.activeDashboardDao = activeDashboardDao;
+    this.views = views;
+    this.i18n = i18n;
+    this.projectConfiguration = projectConfiguration;
+  }
+
+  @Override
+  public void define(NewController context) {
+    NewAction projectNavigation = context.createAction("component")
+      .setDescription("Get information concerning component navigation for the current user. " +
+        "Requires the 'Browse' permission on the component's project.")
+      .setHandler(this)
+      .setInternal(true)
+      .setResponseExample(getClass().getResource("example-component.json"))
+      .setSince("5.2");
+
+    projectNavigation.createParam(PARAM_COMPONENT_KEY)
+      .setDescription("A component key.")
+      .setExampleValue("org.codehaus.sonar:sonar")
+      .setRequired(true);
+  }
+
+  @Override
+  public void handle(Request request, Response response) throws Exception {
+    String componentKey = request.mandatoryParam(PARAM_COMPONENT_KEY);
+
+    UserSession userSession = UserSession.get();
+    DbSession session = dbClient.openSession(false);
+
+    try {
+      ComponentDto component = dbClient.componentDao().getByKey(session, componentKey);
+
+      userSession.checkProjectUuidPermission(UserRole.USER, component.projectUuid());
+
+      SnapshotDto snapshot = dbClient.snapshotDao().getLastSnapshot(session, new SnapshotDto().setResourceId(component.getId()));
+
+      JsonWriter json = response.newJsonWriter();
+      json.beginObject();
+      writeComponent(json, session, component, snapshot, userSession);
+
+      if (userSession.hasProjectPermissionByUuid(UserRole.ADMIN, component.projectUuid()) || userSession.hasGlobalPermission(GlobalPermissions.QUALITY_PROFILE_ADMIN)) {
+        writeConfiguration(json, component, userSession);
+      }
+
+      writeBreadCrumbs(json, session, component, snapshot);
+      json.endObject().close();
+
+    } finally {
+      session.close();
+    }
+  }
+
+  private void writeComponent(JsonWriter json, DbSession session, ComponentDto component, @Nullable SnapshotDto snapshot, UserSession userSession) {
+
+    json.prop("key", component.key())
+      .prop("uuid", component.uuid())
+      .prop("name", component.name())
+      .prop("isComparable", projectConfiguration.componentTypeHasProperty(component, ComponentConfigurationPages.PROPERTY_COMPARABLE))
+      .prop("canBeFavorite", userSession.isLoggedIn())
+      .prop("isFavorite", isFavourite(session, component, userSession));
+
+    List<DashboardDto> dashboards = activeDashboardDao.selectProjectDashboardsForUserLogin(session, userSession.login());
+    writeDashboards(json, component, dashboards, userSession.locale());
+
+    if (snapshot != null) {
+      json.prop("version", snapshot.getVersion())
+        .prop("date", DateUtils.formatDateTime(new Date(snapshot.getCreatedAt())));
+      String[] availableMeasures = dbClient.measureDao().selectMetricKeysForSnapshot(session, snapshot.getId()).toArray(new String[0]);
+      List<ViewProxy<Page>> pages = views.getPages(NavigationSection.RESOURCE, component.scope(), component.qualifier(), component.language(), availableMeasures);
+      writeExtensions(json, component, pages, userSession.locale());
+    }
+  }
+
+  private boolean isFavourite(DbSession session, ComponentDto component, UserSession userSession) {
+    PropertyQuery propertyQuery = PropertyQuery.builder()
+      .setUserId(userSession.userId())
+      .setKey("favourite")
+      .setComponentId(component.getId())
+      .build();
+    List<PropertyDto> componentFavourites = dbClient.propertiesDao().selectByQuery(propertyQuery, session);
+    return componentFavourites.size() == 1;
+  }
+
+  private void writeExtensions(JsonWriter json, ComponentDto component, List<ViewProxy<Page>> pages, Locale locale) {
+    json.name("extensions").beginArray();
+    for (ViewProxy<Page> page: pages) {
+      writePage(json, getPageUrl(page, component), i18n.message(locale, page.getId() + ".page", page.getTitle()));
+    }
+    json.endArray();
+  }
+
+  private String getPageUrl(ViewProxy<Page> page, ComponentDto component) {
+    String result = null;
+    String componentKey = ComponentConfigurationPages.encodeComponentKey(component);
+    if (page.isController()) {
+      result = String.format("%s?id=%s", page.getId(), componentKey);
+    } else {
+      result = String.format("/plugins/resource/%s?page=%s", componentKey, page.getId());
+    }
+    return result;
+  }
+
+  private void writeDashboards(JsonWriter json, ComponentDto component, List<DashboardDto> dashboards, Locale locale) {
+    json.name("dashboards").beginArray();
+    for (DashboardDto dashboard : dashboards) {
+      json.beginObject()
+        .prop("key", dashboard.getId())
+        .prop("name", i18n.message(locale, String.format("dashboard.%s.name", dashboard.getName()), dashboard.getName()))
+        .endObject();
+    }
+    json.endArray();
+  }
+
+  private void writeConfiguration(JsonWriter json, ComponentDto component, UserSession userSession) {
+    boolean isAdmin = userSession.hasProjectPermissionByUuid(UserRole.ADMIN, component.projectUuid());
+    Locale locale = userSession.locale();
+
+    json.name("configuration").beginArray();
+    for (ConfigPage page : projectConfiguration.getConfigPages(component, userSession)) {
+      page.write(json);
+    }
+
+    if (isAdmin) {
+      List<ViewProxy<Page>> configPages = views.getPages(NavigationSection.RESOURCE_CONFIGURATION, component.scope(), component.qualifier(), component.language(), null);
+      for (ViewProxy<Page> page : configPages) {
+        writePage(json, getPageUrl(page, component), i18n.message(locale, page.getId() + ".page", page.getTitle()));
+      }
+    }
+    json.endArray();
+  }
+
+  private void writePage(JsonWriter json, String url, String name) {
+    json.beginObject()
+      .prop("url", url)
+      .prop("name", name)
+      .endObject();
+  }
+
+  private void writeBreadCrumbs(JsonWriter json, DbSession session, ComponentDto component, @Nullable SnapshotDto snapshot) {
+    json.name("breadcrumbs").beginArray();
+
+    List<ComponentDto> componentPath = Lists.newArrayList(component);
+
+    if (snapshot != null) {
+      SnapshotDto currentSnapshot = snapshot;
+      while (currentSnapshot.getParentId() != null) {
+        currentSnapshot = dbClient.snapshotDao().getByKey(session, currentSnapshot.getParentId());
+        componentPath.add(0, dbClient.componentDao().getById(currentSnapshot.getResourceId(), session));
+      }
+    }
+
+    for (ComponentDto crumbComponent : componentPath) {
+      json.beginObject()
+        .prop("key", crumbComponent.key())
+        .prop("name", crumbComponent.name())
+        .prop("qualifier", crumbComponent.qualifier())
+        .endObject();
+    }
+
+    json.endArray();
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/ui/ws/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/ui/ws/package-info.java
new file mode 100644 (file)
index 0000000..4f12742
--- /dev/null
@@ -0,0 +1,24 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.ui.ws;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/server/sonar-server/src/main/resources/org/sonar/server/ui/ws/example-component.json b/server/sonar-server/src/main/resources/org/sonar/server/ui/ws/example-component.json
new file mode 100644 (file)
index 0000000..b03cef5
--- /dev/null
@@ -0,0 +1,39 @@
+{
+  "id": 2865,
+  "key": "org.codehaus.sonar:sonar",
+  "uuid": "69e57151-be0d-4157-adff-c06741d88879",
+  "isComparable": true,
+  "canBeFavorite": true,
+  "isFavorite": true,
+  "version": "5.2-SNAPSHOT",
+  "snapshotDate": "2015-04-16T14:40:32+02:00",
+  "dashboards": [
+    {
+      "key": 1,
+      "name": "Main Dashboard"
+    }
+  ],
+  "extensions": [
+    {
+      "name": "My Resource Plugin",
+      "url": "/plugins/resource/2865?page=my-resource-plugin"
+    }
+  ],
+  "configuration": [
+    {
+      "name": "General Settings",
+      "url": "/project/settings/2865"
+    },
+    {
+      "name": "Action Plans",
+      "url": "/action_plans/index/2865"
+    }
+  ],
+  "breadcrumbs": [
+    {
+      "name": "SonarQube",
+      "qualifier": "TRK",
+      "key": "org.codehaus.sonar:sonar"
+    }
+  ]
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/ui/ws/ComponentConfigurationPagesTest.java b/server/sonar-server/src/test/java/org/sonar/server/ui/ws/ComponentConfigurationPagesTest.java
new file mode 100644 (file)
index 0000000..1cfbe7d
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 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.ui.ws;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+import org.sonar.api.i18n.I18n;
+import org.sonar.api.resources.ResourceType;
+import org.sonar.api.resources.ResourceTypes;
+import org.sonar.api.web.UserRole;
+import org.sonar.core.component.ComponentDto;
+import org.sonar.server.component.ComponentTesting;
+import org.sonar.server.ui.ws.ComponentConfigurationPages.ConfigPage;
+import org.sonar.server.user.MockUserSession;
+import org.sonar.server.user.UserSession;
+
+import java.util.List;
+import java.util.Locale;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ComponentConfigurationPagesTest {
+
+  @Mock
+  private I18n i18n;
+
+  @Mock
+  private ResourceTypes resourceTypes;
+
+  @Before
+  public void before() {
+    when(i18n.message(Matchers.any(Locale.class), Matchers.anyString(), Matchers.anyString())).thenAnswer(new Answer<String>() {
+      @Override
+      public String answer(InvocationOnMock invocation) throws Throwable {
+        return invocation.getArgumentAt(1, String.class);
+      }
+    });
+  }
+
+  @Test
+  public void pages_for_project() throws Exception {
+    String uuid = "abcd";
+    ComponentDto component = ComponentTesting.newProjectDto(uuid).setKey("org.codehaus.sonar:sonar");
+    UserSession userSession = MockUserSession.set().setLogin("obiwan").addProjectUuidPermissions(UserRole.ADMIN, uuid);
+
+    List<ConfigPage> pages = new ComponentConfigurationPages(i18n, resourceTypes).getConfigPages(component, userSession);
+    assertThat(pages).extracting("visible").containsExactly(
+      false, true, true, true, true, true, false, false, false, false);
+    assertThat(pages).extracting("url").containsExactly(
+        "/project/settings?id=org.codehaus.sonar%3Asonar",
+        "/project/profile?id=org.codehaus.sonar%3Asonar",
+        "/project/qualitygate?id=org.codehaus.sonar%3Asonar",
+        "/manual_measures/index?id=org.codehaus.sonar%3Asonar",
+        "/action_plans/index?id=org.codehaus.sonar%3Asonar",
+        "/project/links?id=org.codehaus.sonar%3Asonar",
+        "/project_roles/index?id=org.codehaus.sonar%3Asonar",
+        "/project/history?id=org.codehaus.sonar%3Asonar",
+        "/project/key?id=org.codehaus.sonar%3Asonar",
+        "/project/deletion?id=org.codehaus.sonar%3Asonar"
+      );
+  }
+
+  @Test
+  public void pages_for_project_with_resource_type_property() throws Exception {
+    String uuid = "abcd";
+    ComponentDto component = ComponentTesting.newProjectDto(uuid);
+    UserSession userSession = MockUserSession.set().setLogin("obiwan").addProjectUuidPermissions(UserRole.ADMIN, uuid);
+    when(resourceTypes.get(component.qualifier())).thenReturn(
+      ResourceType.builder(component.qualifier()).setProperty("configurable", true).build());
+
+    List<ConfigPage> pages = new ComponentConfigurationPages(i18n, resourceTypes).getConfigPages(component, userSession);
+    assertThat(pages).extracting("visible").containsExactly(
+      true, true, true, true, true, true, false, false, false, false);
+  }
+
+  @Test
+  public void pages_for_module() throws Exception {
+    String uuid = "abcd";
+    ComponentDto project = ComponentTesting.newProjectDto(uuid);
+    ComponentDto module = ComponentTesting.newModuleDto(project);
+    UserSession userSession = MockUserSession.set().setLogin("obiwan").addProjectUuidPermissions(UserRole.ADMIN, uuid);
+
+    List<ConfigPage> pages = new ComponentConfigurationPages(i18n, resourceTypes).getConfigPages(module, userSession);
+    assertThat(pages).extracting("visible").containsExactly(
+      false, false, false, true, false, false, false, false, false, false);
+  }
+
+  @Test
+  public void pages_for_non_admin() throws Exception {
+    String uuid = "abcd";
+    ComponentDto project = ComponentTesting.newProjectDto(uuid);
+    UserSession userSession = MockUserSession.set().setLogin("obiwan").addProjectUuidPermissions(UserRole.USER, uuid);
+
+    List<ConfigPage> pages = new ComponentConfigurationPages(i18n, resourceTypes).getConfigPages(project, userSession);
+    assertThat(pages).extracting("visible").containsExactly(
+      false, true, true, false, false, false, false, false, false, false);
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/ui/ws/ComponentNavigationActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/ui/ws/ComponentNavigationActionTest.java
new file mode 100644 (file)
index 0000000..1479c6e
--- /dev/null
@@ -0,0 +1,428 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 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.ui.ws;
+
+import com.google.common.collect.Maps;
+import org.apache.commons.lang.BooleanUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.sonar.api.i18n.I18n;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.resources.Scopes;
+import org.sonar.api.utils.DateUtils;
+import org.sonar.api.utils.System2;
+import org.sonar.api.web.NavigationSection;
+import org.sonar.api.web.Page;
+import org.sonar.api.web.ResourceLanguage;
+import org.sonar.api.web.ResourceQualifier;
+import org.sonar.api.web.ResourceScope;
+import org.sonar.api.web.UserRole;
+import org.sonar.core.component.ComponentDto;
+import org.sonar.core.component.SnapshotDto;
+import org.sonar.core.dashboard.ActiveDashboardDao;
+import org.sonar.core.dashboard.ActiveDashboardDto;
+import org.sonar.core.dashboard.DashboardDao;
+import org.sonar.core.dashboard.DashboardDto;
+import org.sonar.core.permission.GlobalPermissions;
+import org.sonar.core.persistence.DbSession;
+import org.sonar.core.persistence.DbTester;
+import org.sonar.core.properties.PropertiesDao;
+import org.sonar.core.properties.PropertyDto;
+import org.sonar.server.component.ComponentTesting;
+import org.sonar.server.component.db.ComponentDao;
+import org.sonar.server.component.db.SnapshotDao;
+import org.sonar.server.db.DbClient;
+import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.measure.persistence.MeasureDao;
+import org.sonar.server.ui.Views;
+import org.sonar.server.user.MockUserSession;
+import org.sonar.server.user.UserSession;
+import org.sonar.server.user.db.UserDao;
+import org.sonar.server.ws.WsTester;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ComponentNavigationActionTest {
+
+  @ClassRule
+  public static final DbTester dbTester = new DbTester();
+
+  private DbSession session;
+
+  private WsTester wsTester;
+
+  private UserDao userDao;
+
+  private DashboardDao dashboardDao;
+
+  private ActiveDashboardDao activeDashboardDao;
+
+  private DbClient dbClient;
+
+  private I18n i18n;
+
+  private ProjectConfigurationPagesStub projectConfigurationPages;
+
+  @Before
+  public void before() throws Exception {
+    dbTester.truncateTables();
+
+    System2 system = mock(System2.class);
+    userDao = new UserDao(dbTester.myBatis(), system);
+    dashboardDao = new DashboardDao(dbTester.myBatis());
+    activeDashboardDao = new ActiveDashboardDao(dbTester.myBatis());
+    dbClient = new DbClient(
+      dbTester.database(), dbTester.myBatis(), userDao, dashboardDao, activeDashboardDao,
+      new ComponentDao(system), new SnapshotDao(system), new PropertiesDao(dbTester.myBatis()),
+      new MeasureDao());
+
+    i18n = mock(I18n.class);
+    when(i18n.message(any(Locale.class), any(String.class), any(String.class)))
+      .thenAnswer(new Answer<String>() {
+        @Override
+        public String answer(InvocationOnMock invocation) throws Throwable {
+          return invocation.getArgumentAt(2, String.class);
+        }
+      });
+
+    projectConfigurationPages = new ProjectConfigurationPagesStub();
+
+    session = dbClient.openSession(false);
+  }
+
+  @After
+  public void after() throws Exception {
+    session.close();
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void fail_on_missing_parameters() throws Exception {
+    wsTester = new WsTester(new NavigationWs(new ComponentNavigationAction(null, null, null, null, null)));
+
+    wsTester.newGetRequest("api/navigation", "component").execute();
+  }
+
+  @Test(expected = NotFoundException.class)
+  public void fail_on_unexistent_key() throws Exception {
+    wsTester = new WsTester(new NavigationWs(new ComponentNavigationAction(dbClient, null, null, null, null)));
+
+    wsTester.newGetRequest("api/navigation", "component").setParam("componentKey", "polop").execute();
+  }
+
+  @Test(expected = ForbiddenException.class)
+  public void fail_on_missing_permission() throws Exception {
+    dbClient.componentDao().insert(session, ComponentTesting.newProjectDto("abcd").setKey("polop"));
+    session.commit();
+
+    MockUserSession.set();
+    wsTester = new WsTester(new NavigationWs(new ComponentNavigationAction(dbClient, null, null, null, null)));
+
+    wsTester.newGetRequest("api/navigation", "component").setParam("componentKey", "polop").execute();
+  }
+
+  @Test
+  public void no_snapshot_anonymous() throws Exception {
+    dbClient.componentDao().insert(session, ComponentTesting.newProjectDto("abcd")
+      .setKey("polop").setName("Polop"));
+    session.commit();
+
+    MockUserSession.set().addProjectUuidPermissions(UserRole.USER, "abcd");
+    wsTester = new WsTester(new NavigationWs(new ComponentNavigationAction(dbClient, activeDashboardDao,
+      new Views(), i18n, projectConfigurationPages)));
+
+    wsTester.newGetRequest("api/navigation", "component").setParam("componentKey", "polop").execute().assertJson(getClass(), "no_snapshot.json");
+  }
+
+  @Test
+  public void no_snapshot_connected_user_and_favorite() throws Exception {
+    int userId = 42;
+    ComponentDto project = dbClient.componentDao().insert(session, ComponentTesting.newProjectDto("abcd")
+      .setKey("polop").setName("Polop"));
+    dbClient.propertiesDao().setProperty(new PropertyDto().setKey("favourite").setResourceId(project.getId()).setUserId((long) userId), session);
+    session.commit();
+
+    MockUserSession.set().setLogin("obiwan").setUserId(userId).addProjectUuidPermissions(UserRole.USER, "abcd");
+
+    wsTester = new WsTester(new NavigationWs(new ComponentNavigationAction(dbClient, activeDashboardDao,
+      new Views(), i18n, projectConfigurationPages)));
+
+    wsTester.newGetRequest("api/navigation", "component").setParam("componentKey", "polop").execute().assertJson(getClass(), "no_snapshot_user_favourite.json");
+  }
+
+  @Test
+  public void with_snapshot_and_connected_user() throws Exception {
+    Date snapshotDate = DateUtils.parseDateTime("2015-04-22T11:44:00+0200");
+
+    int userId = 42;
+    ComponentDto project = dbClient.componentDao().insert(session, ComponentTesting.newProjectDto("abcd")
+      .setKey("polop").setName("Polop"));
+    dbClient.snapshotDao().insert(session, new SnapshotDto().setCreatedAt(snapshotDate.getTime()).setVersion("3.14")
+      .setLast(true).setQualifier(project.qualifier()).setResourceId(project.getId()).setRootProjectId(project.getId()).setScope(project.scope()));
+    session.commit();
+
+    MockUserSession.set().setLogin("obiwan").setUserId(userId).addProjectUuidPermissions(UserRole.USER, "abcd");
+
+    wsTester = new WsTester(new NavigationWs(new ComponentNavigationAction(dbClient, activeDashboardDao,
+      new Views(), i18n, projectConfigurationPages)));
+
+    wsTester.newGetRequest("api/navigation", "component").setParam("componentKey", "polop").execute().assertJson(getClass(), "with_snapshot_and_connected_user.json");
+  }
+
+  @Test
+  public void with_dashboards() throws Exception {
+    dbClient.componentDao().insert(session, ComponentTesting.newProjectDto("abcd")
+      .setKey("polop").setName("Polop"));
+    DashboardDto dashboard = new DashboardDto().setGlobal(false).setName("Anon Dashboard").setShared(true).setColumnLayout("100%");
+    dashboardDao.insert(dashboard);
+    activeDashboardDao.insert(new ActiveDashboardDto().setDashboardId(dashboard.getId()));
+    session.commit();
+
+    MockUserSession.set().addProjectUuidPermissions(UserRole.USER, "abcd");
+
+    wsTester = new WsTester(new NavigationWs(new ComponentNavigationAction(dbClient, activeDashboardDao,
+      new Views(), i18n, projectConfigurationPages)));
+
+    wsTester.newGetRequest("api/navigation", "component").setParam("componentKey", "polop").execute().assertJson(getClass(), "with_dashboards.json");
+  }
+
+  @Test
+  public void with_extensions() throws Exception {
+    final String language = "xoo";
+    ComponentDto project = dbClient.componentDao().insert(session, ComponentTesting.newProjectDto("abcd")
+      .setKey("polop").setName("Polop").setLanguage(language));
+    dbClient.snapshotDao().insert(session, new SnapshotDto()
+      .setLast(true).setQualifier(project.qualifier()).setResourceId(project.getId()).setRootProjectId(project.getId()).setScope(project.scope()));
+    session.commit();
+
+    MockUserSession.set().addProjectUuidPermissions(UserRole.USER, "abcd");
+
+    @NavigationSection(NavigationSection.RESOURCE)
+    @ResourceScope(Scopes.PROJECT)
+    @ResourceQualifier(Qualifiers.PROJECT)
+    @ResourceLanguage(language)
+    class FirstPage implements Page {
+      @Override
+      public String getTitle() {
+        return "First Page";
+      }
+
+      @Override
+      public String getId() {
+        return "first_page";
+      }
+    }
+    Page page1 = new FirstPage();
+
+    @NavigationSection(NavigationSection.RESOURCE)
+    @ResourceScope(Scopes.PROJECT)
+    @ResourceQualifier(Qualifiers.PROJECT)
+    @ResourceLanguage(language)
+    class SecondPage implements Page {
+      @Override
+      public String getTitle() {
+        return "Second Page";
+      }
+
+      @Override
+      public String getId() {
+        return "/second/page";
+      }
+    }
+    Page page2 = new SecondPage();
+
+    wsTester = new WsTester(new NavigationWs(new ComponentNavigationAction(dbClient, activeDashboardDao,
+      new Views(new Page[] {page1, page2}), i18n, projectConfigurationPages)));
+
+    wsTester.newGetRequest("api/navigation", "component").setParam("componentKey", "polop").execute().assertJson(getClass(), "with_extensions.json");
+  }
+
+  @Test
+  public void with_admin_rights() throws Exception {
+    final String language = "xoo";
+    int userId = 42;
+    dbClient.componentDao().insert(session, ComponentTesting.newProjectDto("abcd")
+      .setKey("polop").setName("Polop").setLanguage(language));
+    session.commit();
+
+    MockUserSession.set().setLogin("obiwan").setUserId(userId)
+      .addProjectUuidPermissions(UserRole.USER, "abcd")
+      .addProjectUuidPermissions(UserRole.ADMIN, "abcd");
+
+    @NavigationSection(NavigationSection.RESOURCE_CONFIGURATION)
+    @ResourceScope(Scopes.PROJECT)
+    @ResourceQualifier(Qualifiers.PROJECT)
+    @ResourceLanguage(language)
+    class FirstPage implements Page {
+      @Override
+      public String getTitle() {
+        return "First Page";
+      }
+
+      @Override
+      public String getId() {
+        return "first_page";
+      }
+    }
+    Page page1 = new FirstPage();
+
+    @NavigationSection(NavigationSection.RESOURCE_CONFIGURATION)
+    @ResourceScope(Scopes.PROJECT)
+    @ResourceQualifier(Qualifiers.PROJECT)
+    @ResourceLanguage(language)
+    class SecondPage implements Page {
+      @Override
+      public String getTitle() {
+        return "Second Page";
+      }
+
+      @Override
+      public String getId() {
+        return "/second/page";
+      }
+    }
+    Page page2 = new SecondPage();
+
+    wsTester = new WsTester(new NavigationWs(new ComponentNavigationAction(dbClient, activeDashboardDao,
+      new Views(new Page[] {page1, page2}), i18n, projectConfigurationPages)));
+
+    wsTester.newGetRequest("api/navigation", "component").setParam("componentKey", "polop").execute().assertJson(getClass(), "with_admin_rights.json");
+  }
+
+  @Test
+  public void with_quality_profile_admin_rights() throws Exception {
+    final String language = "xoo";
+    int userId = 42;
+    dbClient.componentDao().insert(session, ComponentTesting.newProjectDto("abcd")
+      .setKey("polop").setName("Polop").setLanguage(language));
+    session.commit();
+
+    MockUserSession.set().setLogin("obiwan").setUserId(userId)
+      .addProjectUuidPermissions(UserRole.USER, "abcd")
+      .setGlobalPermissions(GlobalPermissions.QUALITY_PROFILE_ADMIN);
+
+    @NavigationSection(NavigationSection.RESOURCE_CONFIGURATION)
+    class FirstPage implements Page {
+      @Override
+      public String getTitle() {
+        return "First Page";
+      }
+
+      @Override
+      public String getId() {
+        return "first_page";
+      }
+    }
+    Page page = new FirstPage();
+
+    wsTester = new WsTester(new NavigationWs(new ComponentNavigationAction(dbClient, activeDashboardDao,
+      new Views(new Page[] {page}), i18n, projectConfigurationPages)));
+
+    wsTester.newGetRequest("api/navigation", "component").setParam("componentKey", "polop").execute().assertJson(getClass(), "quality_profile_admin.json");
+  }
+
+  @Test
+  public void bread_crumbs_on_several_levels() throws Exception {
+    ComponentDto project = ComponentTesting.newProjectDto("abcd")
+      .setKey("polop").setName("Polop");
+    ComponentDto module = ComponentTesting.newModuleDto("bcde", project)
+      .setKey("palap").setName("Palap");
+    ComponentDto directory = ComponentTesting.newDirectory(module, "src/main/xoo");
+    ComponentDto file = ComponentTesting.newFileDto(module, "cdef").setName("Source.xoo")
+      .setKey("palap:src/main/xoo/Source.xoo")
+      .setPath(directory.path());
+    dbClient.componentDao().insert(session, project, module, directory, file);
+
+    SnapshotDto projectSnapshot = dbClient.snapshotDao().insert(session, new SnapshotDto()
+      .setLast(true)
+      .setQualifier(project.qualifier())
+      .setResourceId(project.getId())
+      .setRootProjectId(project.getId())
+      .setScope(project.scope()));
+    SnapshotDto moduleSnapshot = dbClient.snapshotDao().insert(session, new SnapshotDto()
+      .setLast(true)
+      .setQualifier(module.qualifier())
+      .setResourceId(module.getId())
+      .setRootProjectId(project.getId())
+      .setScope(module.scope())
+      .setParentId(projectSnapshot.getId()));
+    SnapshotDto directorySnapshot = dbClient.snapshotDao().insert(session, new SnapshotDto()
+      .setLast(true)
+      .setQualifier(directory.qualifier())
+      .setResourceId(directory.getId())
+      .setRootProjectId(project.getId())
+      .setScope(directory.scope())
+      .setParentId(moduleSnapshot.getId()));
+    dbClient.snapshotDao().insert(session, new SnapshotDto()
+      .setLast(true)
+      .setQualifier(file.qualifier())
+      .setResourceId(file.getId())
+      .setRootProjectId(project.getId())
+      .setScope(file.scope())
+      .setParentId(directorySnapshot.getId()));
+
+    session.commit();
+
+    MockUserSession.set().addProjectUuidPermissions(UserRole.USER, "abcd");
+
+    wsTester = new WsTester(new NavigationWs(new ComponentNavigationAction(dbClient, activeDashboardDao,
+      new Views(), i18n, projectConfigurationPages)));
+
+    wsTester.newGetRequest("api/navigation", "component").setParam("componentKey", "palap:src/main/xoo/Source.xoo").execute().assertJson(getClass(), "breadcrumbs.json");
+  }
+
+  class ProjectConfigurationPagesStub extends ComponentConfigurationPages {
+
+    private Map<String, Boolean> resourceTypeHasProperty;
+
+    public ProjectConfigurationPagesStub() {
+      super(i18n, null);
+      resourceTypeHasProperty = Maps.newHashMap();
+    }
+
+    void setComponentTypeProperty(String resourceTypeProperty, boolean value) {
+      resourceTypeHasProperty.put(resourceTypeProperty, value);
+    }
+
+    @Override
+    boolean componentTypeHasProperty(ComponentDto component, String resourceTypeProperty) {
+      return BooleanUtils.isTrue(resourceTypeHasProperty.get(resourceTypeProperty));
+    }
+
+    @Override
+    List<ConfigPage> getConfigPages(ComponentDto component, UserSession userSession) {
+      return Arrays.asList(
+        new ConfigPage(true, "/visible/page", "Visible Config Page"),
+        new ConfigPage(false, "/invisible/page", "Invisible Config Page"));
+    }
+  }
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/ui/ws/ComponentNavigationActionTest/breadcrumbs.json b/server/sonar-server/src/test/resources/org/sonar/server/ui/ws/ComponentNavigationActionTest/breadcrumbs.json
new file mode 100644 (file)
index 0000000..61593d8
--- /dev/null
@@ -0,0 +1,31 @@
+{
+  "key": "palap:src/main/xoo/Source.xoo",
+  "uuid": "cdef",
+  "name": "Source.xoo",
+  "isComparable": false,
+  "canBeFavorite": false,
+  "isFavorite": false,
+  "dashboards": [],
+  "breadcrumbs": [
+    {
+      "key": "polop",
+      "name": "Polop",
+      "qualifier": "TRK"
+    },
+    {
+      "key": "palap",
+      "name": "Palap",
+      "qualifier": "BRC"
+    },
+    {
+      "key": "palap:src/main/xoo",
+      "name": "src/main/xoo",
+      "qualifier": "DIR"
+    },
+    {
+      "key": "palap:src/main/xoo/Source.xoo",
+      "name": "Source.xoo",
+      "qualifier": "FIL"
+    }
+  ]
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/ui/ws/ComponentNavigationActionTest/no_snapshot.json b/server/sonar-server/src/test/resources/org/sonar/server/ui/ws/ComponentNavigationActionTest/no_snapshot.json
new file mode 100644 (file)
index 0000000..0aa0d58
--- /dev/null
@@ -0,0 +1,16 @@
+{
+  "key": "polop",
+  "uuid": "abcd",
+  "name": "Polop",
+  "isComparable": false,
+  "canBeFavorite": false,
+  "isFavorite": false,
+  "dashboards": [],
+  "breadcrumbs": [
+    {
+      "key": "polop",
+      "name": "Polop",
+      "qualifier": "TRK"
+    }
+  ]
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/ui/ws/ComponentNavigationActionTest/no_snapshot_user_favourite.json b/server/sonar-server/src/test/resources/org/sonar/server/ui/ws/ComponentNavigationActionTest/no_snapshot_user_favourite.json
new file mode 100644 (file)
index 0000000..1f3d471
--- /dev/null
@@ -0,0 +1,16 @@
+{
+  "key": "polop",
+  "uuid": "abcd",
+  "name": "Polop",
+  "isComparable": false,
+  "canBeFavorite": true,
+  "isFavorite": true,
+  "dashboards": [],
+  "breadcrumbs": [
+    {
+      "key": "polop",
+      "name": "Polop",
+      "qualifier": "TRK"
+    }
+  ]
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/ui/ws/ComponentNavigationActionTest/quality_profile_admin.json b/server/sonar-server/src/test/resources/org/sonar/server/ui/ws/ComponentNavigationActionTest/quality_profile_admin.json
new file mode 100644 (file)
index 0000000..c377a7b
--- /dev/null
@@ -0,0 +1,22 @@
+{
+  "key": "polop",
+  "uuid": "abcd",
+  "name": "Polop",
+  "isComparable": false,
+  "canBeFavorite": true,
+  "isFavorite": false,
+  "dashboards": [],
+  "configuration": [
+    {
+      "name": "Visible Config Page",
+      "url": "/visible/page"
+    }
+  ],
+  "breadcrumbs": [
+    {
+      "key": "polop",
+      "name": "Polop",
+      "qualifier": "TRK"
+    }
+  ]
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/ui/ws/ComponentNavigationActionTest/with_admin_rights.json b/server/sonar-server/src/test/resources/org/sonar/server/ui/ws/ComponentNavigationActionTest/with_admin_rights.json
new file mode 100644 (file)
index 0000000..09726b6
--- /dev/null
@@ -0,0 +1,30 @@
+{
+  "key": "polop",
+  "uuid": "abcd",
+  "name": "Polop",
+  "isComparable": false,
+  "canBeFavorite": true,
+  "isFavorite": false,
+  "dashboards": [],
+  "configuration": [
+    {
+      "name": "Visible Config Page",
+      "url": "/visible/page"
+    },
+    {
+      "name": "First Page",
+      "url": "/plugins/resource/polop?page=first_page"
+    },
+    {
+      "name": "Second Page",
+      "url": "/second/page?id=polop"
+    }
+  ],
+  "breadcrumbs": [
+    {
+      "key": "polop",
+      "name": "Polop",
+      "qualifier": "TRK"
+    }
+  ]
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/ui/ws/ComponentNavigationActionTest/with_dashboards.json b/server/sonar-server/src/test/resources/org/sonar/server/ui/ws/ComponentNavigationActionTest/with_dashboards.json
new file mode 100644 (file)
index 0000000..2d54fcd
--- /dev/null
@@ -0,0 +1,20 @@
+{
+  "key": "polop",
+  "uuid": "abcd",
+  "name": "Polop",
+  "isComparable": false,
+  "canBeFavorite": false,
+  "isFavorite": false,
+  "dashboards": [
+    {
+      "name": "Anon Dashboard"
+    }
+  ],
+  "breadcrumbs": [
+    {
+      "key": "polop",
+      "name": "Polop",
+      "qualifier": "TRK"
+    }
+  ]
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/ui/ws/ComponentNavigationActionTest/with_extensions.json b/server/sonar-server/src/test/resources/org/sonar/server/ui/ws/ComponentNavigationActionTest/with_extensions.json
new file mode 100644 (file)
index 0000000..9f30963
--- /dev/null
@@ -0,0 +1,26 @@
+{
+  "key": "polop",
+  "uuid": "abcd",
+  "name": "Polop",
+  "isComparable": false,
+  "canBeFavorite": false,
+  "isFavorite": false,
+  "dashboards": [],
+  "extensions": [
+    {
+      "name": "First Page",
+      "url": "/plugins/resource/polop?page=first_page"
+    },
+    {
+      "name": "Second Page",
+      "url": "/second/page?id=polop"
+    }
+  ],
+  "breadcrumbs": [
+    {
+      "key": "polop",
+      "name": "Polop",
+      "qualifier": "TRK"
+    }
+  ]
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/ui/ws/ComponentNavigationActionTest/with_snapshot_and_connected_user.json b/server/sonar-server/src/test/resources/org/sonar/server/ui/ws/ComponentNavigationActionTest/with_snapshot_and_connected_user.json
new file mode 100644 (file)
index 0000000..1c55fce
--- /dev/null
@@ -0,0 +1,18 @@
+{
+  "key": "polop",
+  "uuid": "abcd",
+  "name": "Polop",
+  "isComparable": false,
+  "canBeFavorite": true,
+  "isFavorite": false,
+  "date": "2015-04-22T11:44:00+0200",
+  "version": "3.14",
+  "dashboards": [],
+  "breadcrumbs": [
+    {
+      "key": "polop",
+      "name": "Polop",
+      "qualifier": "TRK"
+    }
+  ]
+}
index cc3de408fc6297832efec07f71c7f49642dd9382..de705d74eae6e125b052692a15c0fa5afaf5f93d 100644 (file)
@@ -67,6 +67,19 @@ public class ActiveDashboardDao implements BatchComponent, ServerComponent, DaoC
     }
   }
 
+  public List<DashboardDto> selectProjectDashboardsForUserLogin(@Nullable String login) {
+    SqlSession session = mybatis.openSession(false);
+    try {
+      return selectProjectDashboardsForUserLogin(session, login);
+    } finally {
+      session.close();
+    }
+  }
+
+  public List<DashboardDto> selectProjectDashboardsForUserLogin(SqlSession session, @Nullable String login) {
+    return getMapper(session).selectProjectDashboardsForUserLogin(login);
+  }
+
   private ActiveDashboardMapper getMapper(SqlSession session) {
     return session.getMapper(ActiveDashboardMapper.class);
   }
index 42dbc93c8aa18057c5e3f42570e48ce51af2708a..b7f951a9a94d2a7ec4a6907b9b4e5e9ff4322733 100644 (file)
@@ -35,4 +35,6 @@ public interface ActiveDashboardMapper {
   Integer selectMaxOrderIndexForNullUser();
 
   List<DashboardDto> selectGlobalDashboardsForUserLogin(@Nullable @Param("login") String login);
+
+  List<DashboardDto> selectProjectDashboardsForUserLogin(@Nullable @Param("login") String login);
 }
index de8df080ea69aa1b20c6c8f2568aa066dd3d86b6..39a32c758aed0a523352634b0cce3f5b4e7531cf 100644 (file)
@@ -38,4 +38,6 @@ public interface MeasureMapper {
   long countByComponentAndMetric(@Param("componentKey") String componentKey, @Param("metricKey") String metricKey);
 
   void insert(MeasureDto measureDto);
+
+  List<String> selectMetricKeysForSnapshot(@Param("snapshotId") long snapshotId);
 }
index 68b740e858cd3dec3d232dc3d859ba54a0ae7b4a..a192aab847fd8d12faf899206a478c02693e90f1 100644 (file)
     ORDER BY order_index ASC
   </select>
 
+  <select id="selectProjectDashboardsForUserLogin" parameterType="String" resultType="Dashboard">
+    SELECT <include refid="dashboardColumns" />
+    FROM dashboards d
+    INNER JOIN active_dashboards ad on d.id=ad.dashboard_id
+    LEFT OUTER JOIN users u on u.id=ad.user_id
+    WHERE d.is_global=${_false}
+    <choose>
+      <when test="login == null">
+      AND u.login IS NULL
+      </when>
+      <otherwise>
+      AND u.login=#{login}
+      </otherwise>
+    </choose>
+    ORDER BY order_index ASC
+  </select>
+
 </mapper>
index 4a65b908e35e83566607731e6e3855e1021657bc..110a48de4e87916ce3948db2b875bdb599d94669 100644 (file)
     )
   </insert>
 
+  <select id="selectMetricKeysForSnapshot" parameterType="long" resultType="string">
+  SELECT m.name
+  FROM project_measures pm
+  INNER JOIN metrics m ON m.id=pm.metric_id
+  WHERE pm.snapshot_id=#{snapshotId}
+  </select>
+
 </mapper>
index bfcffe66513331fc82dc1ca1ded0a4c29605bfa5..4f3ec24bae7291bf1c3cb5e56d29b385bd840dff 100644 (file)
@@ -98,4 +98,18 @@ public class ActiveDashboardDaoTest {
 
     assertThat(dao.selectGlobalDashboardsForUserLogin("obiwan")).hasSize(2).extracting("id").containsExactly(2L, 1L);
   }
+
+  @Test
+  public void should_get_project_dashboards_for_anonymous() throws Exception {
+    dbTester.prepareDbUnit(getClass(), "shouldSelectProjectDashboardsForAnonymous.xml");
+
+    assertThat(dao.selectProjectDashboardsForUserLogin(null)).hasSize(2).extracting("id").containsExactly(2L, 1L);
+  }
+
+  @Test
+  public void should_get_project_dashboards_for_user() throws Exception {
+    dbTester.prepareDbUnit(getClass(), "shouldSelectProjectDashboardsForUser.xml");
+
+    assertThat(dao.selectProjectDashboardsForUserLogin("obiwan")).hasSize(2).extracting("id").containsExactly(2L, 1L);
+  }
 }
diff --git a/sonar-core/src/test/resources/org/sonar/core/dashboard/ActiveDashboardDaoTest/shouldSelectProjectDashboardsForAnonymous.xml b/sonar-core/src/test/resources/org/sonar/core/dashboard/ActiveDashboardDaoTest/shouldSelectProjectDashboardsForAnonymous.xml
new file mode 100644 (file)
index 0000000..850d6a5
--- /dev/null
@@ -0,0 +1,68 @@
+<dataset>
+
+  <users id="42" login="obiwan" name="Obiwan" email="obiwan@keno.bi"
+    created_at="1418215735482" updated_at="1418215735482" active="[true]"/>
+
+  <dashboards
+    id="1"
+    user_id="1"
+    name="My Dashboard"
+    description="Dashboard shared by admin"
+    column_layout="100%"
+    shared="[true]"
+    is_global="[false]"
+  />
+  <dashboards
+    id="2"
+    user_id="[null]"
+    name="Default Dashboard"
+    description="Dashboard provided by system"
+    column_layout="100%"
+    shared="[true]"
+    is_global="[false]"
+  />
+  <dashboards
+    id="3"
+    user_id="[null]"
+    name="Project Dashboard"
+    description="Won't appear, global"
+    column_layout="100%"
+    shared="[true]"
+    is_global="[true]"
+  />
+  <dashboards
+    id="4"
+    user_id="[null]"
+    name="User Dashboard"
+    description="Won't appear, not anonymous"
+    column_layout="100%"
+    shared="[true]"
+    is_global="[false]"
+  />
+
+  <!-- Dashboard with ID 1 appears after ID 2 -->
+  <active_dashboards
+    id="1"
+    dashboard_id="1"
+    user_id="[null]"
+    order_index="2"/>
+  <!-- Dashboard with ID 2 appears before ID 1 -->
+  <active_dashboards
+    id="2"
+    dashboard_id="2"
+    user_id="[null]"
+    order_index="1"/>
+  <!-- Dashboard with ID 3 does not appear (global) -->
+  <active_dashboards
+    id="3"
+    dashboard_id="3"
+    user_id="[null]"
+    order_index="1"/>
+  <!-- Dashboard with ID 4 does not appear (not anonymous) -->
+  <active_dashboards
+    id="4"
+    dashboard_id="4"
+    user_id="42"
+    order_index="1"/>
+
+</dataset>
diff --git a/sonar-core/src/test/resources/org/sonar/core/dashboard/ActiveDashboardDaoTest/shouldSelectProjectDashboardsForUser.xml b/sonar-core/src/test/resources/org/sonar/core/dashboard/ActiveDashboardDaoTest/shouldSelectProjectDashboardsForUser.xml
new file mode 100644 (file)
index 0000000..88de79b
--- /dev/null
@@ -0,0 +1,85 @@
+<dataset>
+
+  <users id="24" login="anakin" name="Anakin" email="anakin@skywalk.er"
+    created_at="1418215735482" updated_at="1418215735482" active="[true]"/>
+  <users id="42" login="obiwan" name="Obiwan" email="obiwan@keno.bi"
+    created_at="1418215735482" updated_at="1418215735482" active="[true]"/>
+
+  <dashboards
+    id="1"
+    user_id="1"
+    name="My Dashboard"
+    description="Dashboard shared by admin"
+    column_layout="100%"
+    shared="[true]"
+    is_global="[false]"
+  />
+  <dashboards
+    id="2"
+    user_id="[null]"
+    name="Default Dashboard"
+    description="Dashboard provided by system"
+    column_layout="100%"
+    shared="[true]"
+    is_global="[false]"
+  />
+  <dashboards
+    id="3"
+    user_id="[null]"
+    name="Project Dashboard"
+    description="Won't appear, global"
+    column_layout="100%"
+    shared="[true]"
+    is_global="[true]"
+  />
+  <dashboards
+    id="4"
+    user_id="[null]"
+    name="Anonymous Dashboard"
+    description="Won't appear, anonymous"
+    column_layout="100%"
+    shared="[true]"
+    is_global="[false]"
+  />
+  <dashboards
+    id="5"
+    user_id="[null]"
+    name="Another User Dashboard"
+    description="Won't appear, different user"
+    column_layout="100%"
+    shared="[true]"
+    is_global="[false]"
+  />
+
+  <!-- Dashboard with ID 1 appears after ID 2 -->
+  <active_dashboards
+    id="1"
+    dashboard_id="1"
+    user_id="42"
+    order_index="2"/>
+  <!-- Dashboard with ID 2 appears before ID 1 -->
+  <active_dashboards
+    id="2"
+    dashboard_id="2"
+    user_id="42"
+    order_index="1"/>
+  <!-- Dashboard with ID 3 does not appear (global) -->
+  <active_dashboards
+    id="3"
+    dashboard_id="3"
+    user_id="42"
+    order_index="1"/>
+  <!-- Dashboard with ID 4 does not appear (anonymous) -->
+  <active_dashboards
+    id="4"
+    dashboard_id="4"
+    user_id="[null]"
+    order_index="1"/>
+  <!-- Dashboard with ID 5 does not appear (another user) -->
+  <active_dashboards
+    id="5"
+    dashboard_id="5"
+    user_id="24"
+    order_index="1"/>
+
+</dataset>
index 5fcf4e934d39343edc5534a5fc7dc62a3ed15762..83962edd5f85a505ae44caa1cc314773297a5573 100644 (file)
@@ -57,6 +57,10 @@ public class ResourceTypes implements TaskComponent, ServerComponent {
   private final Map<String, ResourceType> typeByQualifier;
   private final Collection<ResourceType> rootTypes;
 
+  public ResourceTypes() {
+    this(new ResourceTypeTree[0]);
+  }
+
   public ResourceTypes(ResourceTypeTree[] trees) {
     Preconditions.checkNotNull(trees);