]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-8581 Create new PagesDefinition API to create a new page in a plugin
authorTeryk Bellahsene <teryk.bellahsene@sonarsource.com>
Fri, 6 Jan 2017 16:06:01 +0000 (17:06 +0100)
committerTeryk Bellahsene <teryk.bellahsene@sonarsource.com>
Fri, 13 Jan 2017 16:58:12 +0000 (17:58 +0100)
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
server/sonar-server/src/main/java/org/sonar/server/ui/PageRepository.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/ui/PageRepositoryTest.java [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/web/page/Context.java [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/web/page/Page.java [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/web/page/PageDefinition.java [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/web/page/package-info.java [new file with mode: 0644]
sonar-plugin-api/src/test/java/org/sonar/api/web/page/ContextTest.java [new file with mode: 0644]
sonar-plugin-api/src/test/java/org/sonar/api/web/page/PageDefinitionTest.java [new file with mode: 0644]
sonar-plugin-api/src/test/java/org/sonar/api/web/page/PageTest.java [new file with mode: 0644]

index a03956b1af46d51dbc4f4192276dd09a88034498..cd7f02201ca980b55ac339a5539eb662b99bbf34 100644 (file)
@@ -190,6 +190,7 @@ import org.sonar.server.test.ws.TestsWs;
 import org.sonar.server.text.MacroInterpreter;
 import org.sonar.server.text.RubyTextService;
 import org.sonar.server.ui.PageDecorations;
+import org.sonar.server.ui.PageRepository;
 import org.sonar.server.ui.Views;
 import org.sonar.server.ui.ws.NavigationWsModule;
 import org.sonar.server.updatecenter.UpdateCenterModule;
@@ -231,6 +232,7 @@ public class PlatformLevel4 extends PlatformLevel {
     add(
       PluginDownloader.class,
       Views.class,
+      PageRepository.class,
       ResourceTypes.class,
       DefaultResourceTypes.get(),
       SettingsChangeNotifier.class,
diff --git a/server/sonar-server/src/main/java/org/sonar/server/ui/PageRepository.java b/server/sonar-server/src/main/java/org/sonar/server/ui/PageRepository.java
new file mode 100644 (file)
index 0000000..f0f81b2
--- /dev/null
@@ -0,0 +1,120 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
+import javax.annotation.Nullable;
+import org.sonar.api.Startable;
+import org.sonar.api.server.ServerSide;
+import org.sonar.api.web.page.Context;
+import org.sonar.api.web.page.Page;
+import org.sonar.api.web.page.Page.Qualifier;
+import org.sonar.api.web.page.Page.Scope;
+import org.sonar.api.web.page.PageDefinition;
+import org.sonar.core.platform.PluginRepository;
+import org.sonar.core.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkState;
+import static java.util.Collections.emptyList;
+import static java.util.Comparator.comparing;
+import static java.util.Objects.requireNonNull;
+import static org.sonar.api.web.page.Page.Scope.COMPONENT;
+import static org.sonar.api.web.page.Page.Scope.GLOBAL;
+
+@ServerSide
+public class PageRepository implements Startable {
+  private static final Splitter PAGE_KEY_SPLITTER = Splitter.on("/").omitEmptyStrings();
+
+  private final PluginRepository pluginRepository;
+  private final List<PageDefinition> definitions;
+  private List<Page> pages;
+
+  public PageRepository(PluginRepository pluginRepository) {
+    this.pluginRepository = pluginRepository;
+    // in case there's no page definition
+    this.definitions = Collections.emptyList();
+  }
+
+  public PageRepository(PluginRepository pluginRepository, PageDefinition[] pageDefinitions) {
+    this.pluginRepository = pluginRepository;
+    definitions = ImmutableList.copyOf(pageDefinitions);
+  }
+
+  private static Consumer<Page> checkWellFormed() {
+    return page -> {
+      boolean isWellFormed = PAGE_KEY_SPLITTER.splitToList(page.getKey()).size() == 2;
+      checkState(isWellFormed, "Page '%s' with key '%s' does not respect the format plugin_key/extension_point_key (ex: governance/project_dump)",
+        page.getName(), page.getKey());
+    };
+  }
+
+  @Override
+  public void start() {
+    Context context = new Context();
+    definitions.forEach(definition -> definition.define(context));
+    pages = context.getPages().stream()
+      .peek(checkWellFormed())
+      .peek(checkPluginExists())
+      .sorted(comparing(Page::getKey))
+      .collect(Collectors.toList());
+  }
+
+  @Override
+  public void stop() {
+    // nothing to do
+  }
+
+  public List<Page> getGlobalPages(boolean isAdmin) {
+    return getPages(GLOBAL, isAdmin, null);
+  }
+
+  public List<Page> getComponentPages(boolean isAdmin, String qualifierKey) {
+    Qualifier qualifier = Qualifier.fromKey(qualifierKey);
+    return qualifier == null ? emptyList() : getPages(COMPONENT, isAdmin, qualifier);
+  }
+
+  private List<Page> getPages(Scope scope, boolean isAdmin, @Nullable Qualifier qualifier) {
+    return getAllPages().stream()
+      .filter(p -> p.getScope().equals(scope))
+      .filter(p -> p.isAdmin() == isAdmin)
+      .filter(p -> GLOBAL.equals(p.getScope()) || p.getComponentQualifiers().contains(qualifier))
+      .collect(Collectors.toList());
+  }
+
+  @VisibleForTesting
+  List<Page> getAllPages() {
+    return requireNonNull(pages, "Pages haven't been initialized yet");
+  }
+
+  private Consumer<Page> checkPluginExists() {
+    return page -> {
+      String plugin = PAGE_KEY_SPLITTER.splitToList(page.getKey()).get(0);
+      boolean pluginExists = pluginRepository.hasPlugin(plugin);
+      checkState(pluginExists, "Page '%s' references plugin '%s' that does not exist", page.getName(), plugin);
+    };
+  }
+
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/ui/PageRepositoryTest.java b/server/sonar-server/src/test/java/org/sonar/server/ui/PageRepositoryTest.java
new file mode 100644 (file)
index 0000000..8ced52e
--- /dev/null
@@ -0,0 +1,151 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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;
+
+import java.util.List;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.web.page.Page;
+import org.sonar.api.web.page.Page.Qualifier;
+import org.sonar.api.web.page.PageDefinition;
+import org.sonar.core.platform.PluginRepository;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.api.web.page.Page.Scope.COMPONENT;
+import static org.sonar.api.web.page.Page.Scope.GLOBAL;
+
+public class PageRepositoryTest {
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+  @Rule
+  public LogTester LOGGER = new LogTester();
+
+  private PluginRepository pluginRepository = mock(PluginRepository.class);
+
+  private PageRepository underTest = new PageRepository(pluginRepository);
+
+  @Before
+  public void setUp() {
+    when(pluginRepository.hasPlugin(anyString())).thenReturn(true);
+  }
+
+  @Test
+  public void pages_from_different_page_definitions_ordered_by_key() {
+    PageDefinition firstPlugin = context -> context
+      .addPage(Page.builder("my_plugin/K1").setName("N1").build())
+      .addPage(Page.builder("my_plugin/K3").setName("N3").build());
+    PageDefinition secondPlugin = context -> context.addPage(Page.builder("my_plugin/K2").setName("N2").build());
+    underTest = new PageRepository(pluginRepository, new PageDefinition[] {firstPlugin, secondPlugin});
+    underTest.start();
+
+    List<Page> result = underTest.getAllPages();
+
+    assertThat(result).extracting(Page::getKey, Page::getName)
+      .containsExactly(
+        tuple("my_plugin/K1", "N1"),
+        tuple("my_plugin/K2", "N2"),
+        tuple("my_plugin/K3", "N3"));
+  }
+
+  @Test
+  public void filter_by_navigation_and_qualifier() {
+    PageDefinition plugin = context -> context
+      // Default with GLOBAL navigation and no qualifiers
+      .addPage(Page.builder("my_plugin/K1").setName("K1").build())
+      .addPage(Page.builder("my_plugin/K2").setName("K2").setScope(COMPONENT).setComponentQualifiers(Qualifier.PROJECT).build())
+      .addPage(Page.builder("my_plugin/K3").setName("K3").setScope(COMPONENT).setComponentQualifiers(Qualifier.MODULE).build())
+      .addPage(Page.builder("my_plugin/K4").setName("K4").setScope(GLOBAL).build())
+      .addPage(Page.builder("my_plugin/K5").setName("K5").setScope(COMPONENT).setComponentQualifiers(Qualifier.VIEW).build());
+    underTest = new PageRepository(pluginRepository, new PageDefinition[] {plugin});
+    underTest.start();
+
+    List<Page> result = underTest.getComponentPages(false, Qualifiers.PROJECT);
+
+    assertThat(result).extracting(Page::getKey).containsExactly("my_plugin/K2");
+  }
+
+  @Test
+  public void empty_pages_if_no_page_definition() {
+    underTest.start();
+
+    List<Page> result = underTest.getAllPages();
+
+    assertThat(result).isEmpty();
+  }
+
+  @Test
+  public void filter_pages_without_qualifier() {
+    PageDefinition plugin = context -> context
+      .addPage(Page.builder("my_plugin/K1").setName("N1").build())
+      .addPage(Page.builder("my_plugin/K2").setName("N2").build())
+      .addPage(Page.builder("my_plugin/K3").setName("N3").build());
+    underTest = new PageRepository(pluginRepository, new PageDefinition[] {plugin});
+    underTest.start();
+
+    List<Page> result = underTest.getGlobalPages(false);
+
+    assertThat(result).extracting(Page::getKey).containsExactly("my_plugin/K1", "my_plugin/K2", "my_plugin/K3");
+  }
+
+  @Test
+  public void fail_if_pages_called_before_server_startup() {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("Pages haven't been initialized yet");
+
+    underTest.getAllPages();
+  }
+
+  @Test
+  public void fail_if_page_with_wrong_format() {
+    PageDefinition plugin = context -> context
+      .addPage(Page.builder("my_key").setName("N1").build())
+      .addPage(Page.builder("my_plugin/my_key").setName("N2").build());
+    underTest = new PageRepository(pluginRepository, new PageDefinition[] {plugin});
+
+    expectedException.expect(IllegalStateException.class);
+    expectedException.expectMessage("Page 'N1' with key 'my_key' does not respect the format plugin_key/extension_point_key (ex: governance/project_dump)");
+
+    underTest.start();
+  }
+
+  @Test
+  public void fail_if_page_with_unknown_plugin() {
+    PageDefinition governance = context -> context.addPage(Page.builder("governance/my_key").setName("N1").build());
+    PageDefinition plugin42 = context -> context.addPage(Page.builder("plugin_42/my_key").setName("N2").build());
+    pluginRepository = mock(PluginRepository.class);
+    when(pluginRepository.hasPlugin("governance")).thenReturn(true);
+    underTest = new PageRepository(pluginRepository, new PageDefinition[] {governance, plugin42});
+
+    expectedException.expect(IllegalStateException.class);
+    expectedException.expectMessage("Page 'N2' references plugin 'plugin_42' that does not exist");
+
+    underTest.start();
+  }
+}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/web/page/Context.java b/sonar-plugin-api/src/main/java/org/sonar/api/web/page/Context.java
new file mode 100644 (file)
index 0000000..daf2d10
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.api.web.page;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import static java.lang.String.format;
+import static java.util.Collections.unmodifiableCollection;
+
+/**
+ * @see PageDefinition
+ * @since 6.3
+ */
+public final class Context {
+  private final Map<String, Page> pagesByPath = new HashMap<>();
+
+  public Context addPage(Page page) {
+    Page existingPageWithSameKey = pagesByPath.putIfAbsent(page.getKey(), page);
+    if (existingPageWithSameKey != null) {
+      throw new IllegalArgumentException(format("Page '%s' cannot be loaded. Another page with key '%s' already exists.", page.getName(), page.getKey()));
+    }
+
+    return this;
+  }
+
+  public Collection<Page> getPages() {
+    return unmodifiableCollection(pagesByPath.values());
+  }
+}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/web/page/Page.java b/sonar-plugin-api/src/main/java/org/sonar/api/web/page/Page.java
new file mode 100644 (file)
index 0000000..b68ee6b
--- /dev/null
@@ -0,0 +1,172 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.api.web.page;
+
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.annotation.CheckForNull;
+
+import static java.lang.String.format;
+import static java.util.Collections.unmodifiableSet;
+import static java.util.Objects.requireNonNull;
+import static org.sonar.api.web.page.Page.Scope.COMPONENT;
+import static org.sonar.api.web.page.Page.Scope.GLOBAL;
+
+/**
+ * @see PageDefinition
+ * @since 6.3
+ */
+public final class Page {
+  private final String key;
+  private final String name;
+  private final boolean isAdmin;
+  private final Scope scope;
+  private final Set<Qualifier> qualifiers;
+
+  private Page(Builder builder) {
+    this.key = builder.key;
+    this.name = builder.name;
+    this.qualifiers = unmodifiableSet(Stream.of(builder.qualifiers).sorted().collect(Collectors.toSet()));
+    this.isAdmin = builder.isAdmin;
+    this.scope = builder.scope;
+  }
+
+  public static Builder builder(String key) {
+    return new Builder(key);
+  }
+
+  public String getKey() {
+    return key;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public Set<Qualifier> getComponentQualifiers() {
+    return qualifiers;
+  }
+
+  public boolean isAdmin() {
+    return isAdmin;
+  }
+
+  public Scope getScope() {
+    return scope;
+  }
+
+  public enum Scope {
+    GLOBAL, COMPONENT
+  }
+
+  public enum Qualifier {
+    PROJECT(org.sonar.api.resources.Qualifiers.PROJECT),
+    MODULE(org.sonar.api.resources.Qualifiers.MODULE),
+    VIEW(org.sonar.api.resources.Qualifiers.VIEW),
+    SUB_VIEW(org.sonar.api.resources.Qualifiers.SUBVIEW);
+
+    private final String key;
+
+    Qualifier(String key) {
+      this.key = key;
+    }
+
+    @CheckForNull
+    public static Qualifier fromKey(String key) {
+      return Arrays.stream(values())
+        .filter(qualifier -> qualifier.key.equals(key))
+        .findAny()
+        .orElse(null);
+    }
+
+    public String getKey() {
+      return key;
+    }
+  }
+
+  public static class Builder {
+    private final String key;
+    private String name;
+    private boolean isAdmin = false;
+    private Scope scope = GLOBAL;
+    private Qualifier[] qualifiers = new Qualifier[] {};
+
+    /**
+     * @param key It must respect the format plugin_key/page_identifier. Example: <code>my_plugin/my_page</code>
+     */
+    private Builder(String key) {
+      this.key = requireNonNull(key, "Key must not be null");
+    }
+
+    /**
+     * Page name displayed in the UI. Mandatory.
+     */
+    public Builder setName(String name) {
+      this.name = name;
+      return this;
+    }
+
+    /**
+     * if set to true, display the page in the administration section, depending on the scope
+     */
+    public Builder setAdmin(boolean admin) {
+      this.isAdmin = admin;
+      return this;
+    }
+
+    /**
+     * Define where the page is displayed, either in the global menu or in a component page
+     * @param scope - default is GLOBAL
+     */
+    public Builder setScope(Scope scope) {
+      this.scope = requireNonNull(scope, "Scope must not be null");
+      return this;
+    }
+
+    /**
+     * Define the components where the page is displayed. If set, {@link #setScope(Scope)} must be set to COMPONENT
+     * @see Qualifier
+     */
+    public Builder setComponentQualifiers(Qualifier... qualifiers) {
+      this.qualifiers = requireNonNull(qualifiers);
+      return this;
+    }
+
+    public Page build() {
+      if (key == null || key.isEmpty()) {
+        throw new IllegalArgumentException("Key must not be empty");
+      }
+      if (name == null || name.isEmpty()) {
+        throw new IllegalArgumentException("Name must be defined and not empty");
+      }
+      if (qualifiers.length > 0 && GLOBAL.equals(scope)) {
+        throw new IllegalArgumentException(format("The scope must be '%s' when component qualifiers are provided", COMPONENT));
+      }
+      if (qualifiers.length == 0 && COMPONENT.equals(scope)) {
+        qualifiers = Qualifier.values();
+      }
+
+      return new Page(this);
+    }
+  }
+}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/web/page/PageDefinition.java b/sonar-plugin-api/src/main/java/org/sonar/api/web/page/PageDefinition.java
new file mode 100644 (file)
index 0000000..3b2d351
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.api.web.page;
+
+import org.sonar.api.ExtensionPoint;
+import org.sonar.api.server.ServerSide;
+
+/**
+ * Defines the Javascript pages added to SonarQube.
+ * <br>
+ * This interface replaces the deprecated class {@link org.sonar.api.web.Page}. Moreover, the technology changed from Ruby to Javascript
+ * <br>
+ * <h3>How to define pages</h3>
+ * <pre>
+ * import org.sonar.api.web.page.Page.Qualifier;
+ *
+ * public class MyPluginPagesDefinition implements PagesDefinition {
+ *  {@literal @Override}
+ *  public void define(Context context) {
+ *    context
+ *      // Global page by default
+ *      .addPage(Page.builder("my_plugin/global_page").setName("Global Page").build())
+ *      // Global admin page
+ *      .addPage(Page.builder("my_plugin/global_admin_page").setName("Admin Global Page").setAdmin(true).build())
+ *      // Project page
+ *      .addPage(Page.builder("my_plugin/project_page").setName("Project Page").setScope(Scope.COMPONENT).setQualifiers(Qualifier.PROJECT).build())
+ *      // Admin project or module page
+ *      .addPage(Page.builder("my_plugin/project_admin_page").setName("Admin Page for Project or Module").setAdmin(true)
+ *        .setScope(Scope.COMPONENT).setQualifiers(Qualifier.PROJECT, Qualifier.MODULE).build())
+ *      // Page on all components (see Qualifier class) supported
+ *      .addPage(Page.builder("my_plugin/component_page").setName("Component Page").setScope(Scope.COMPONENT).build());
+ *  }
+ * }
+ * </pre>
+ * <h3>How to test a page definition</h3>
+ * <pre>
+ *   public class PageDefinitionTest {
+ *     {@literal @Test}
+ *     public void test_page_definition() {
+ *       PageDefinition underTest = context -> context.addPage(Page.builder("my_plugin/my_page").setName("My Page").build());
+ *       Context context = new Context();
+ *
+ *       underTest.define(context);
+ *
+ *       assertThat(context.getPages()).extracting(Page::getKey).contains("my_plugin/my_page");
+ *     }
+ * </pre>
+ *
+ * @since 6.3
+ */
+
+@ServerSide
+@ExtensionPoint
+public interface PageDefinition {
+  /**
+   * This method is executed when server is started
+   */
+  void define(Context context);
+}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/web/page/package-info.java b/sonar-plugin-api/src/main/java/org/sonar/api/web/page/package-info.java
new file mode 100644 (file)
index 0000000..b1257e7
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.api.web.page;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/web/page/ContextTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/web/page/ContextTest.java
new file mode 100644 (file)
index 0000000..b3c2474
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.api.web.page;
+
+import java.util.Collection;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+
+public class ContextTest {
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  private Context underTest = new Context();
+
+  private Page page = Page.builder("governance/project_export").setName("Project Export").build();
+
+  @Test
+  public void no_pages_with_the_same_path() {
+    underTest.addPage(page);
+
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Page 'Project Export' cannot be loaded. Another page with key 'governance/project_export' already exists.");
+
+    underTest.addPage(page);
+  }
+
+  @Test
+  public void ordered_by_name() {
+    underTest
+      .addPage(Page.builder("K1").setName("N2").build())
+      .addPage(Page.builder("K2").setName("N3").build())
+      .addPage(Page.builder("K3").setName("N1").build());
+
+    Collection<Page> result = underTest.getPages();
+
+    assertThat(result).extracting(Page::getKey, Page::getName)
+      .containsOnly(
+        tuple("K3", "N1"),
+        tuple("K1", "N2"),
+        tuple("K2", "N3"));
+  }
+
+  @Test
+  public void empty_pages_by_default() {
+    Collection<Page> result = underTest.getPages();
+
+    assertThat(result).isEmpty();
+  }
+
+}
diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/web/page/PageDefinitionTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/web/page/PageDefinitionTest.java
new file mode 100644 (file)
index 0000000..f5c3033
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.api.web.page;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Used for the documentation
+ */
+public class PageDefinitionTest {
+  @Test
+  public void test_page_definition() {
+    PageDefinition underTest = context -> context.addPage(Page.builder("my_plugin/my_page").setName("My Page").build());
+    Context context = new Context();
+
+    underTest.define(context);
+
+    assertThat(context.getPages()).extracting(Page::getKey).contains("my_plugin/my_page");
+  }
+
+}
diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/web/page/PageTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/web/page/PageTest.java
new file mode 100644 (file)
index 0000000..61a2e70
--- /dev/null
@@ -0,0 +1,136 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.api.web.page;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.web.page.Page.Qualifier;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.api.web.page.Page.Qualifier.MODULE;
+import static org.sonar.api.web.page.Page.Qualifier.PROJECT;
+import static org.sonar.api.web.page.Page.Qualifier.SUB_VIEW;
+import static org.sonar.api.web.page.Page.Qualifier.VIEW;
+import static org.sonar.api.web.page.Page.Scope.COMPONENT;
+import static org.sonar.api.web.page.Page.Scope.GLOBAL;
+
+public class PageTest {
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  private Page.Builder underTest = Page.builder("governance/project_dump").setName("Project Dump");
+
+  @Test
+  public void full_test() {
+    Page result = underTest
+      .setComponentQualifiers(PROJECT, MODULE)
+      .setScope(COMPONENT)
+      .setAdmin(true)
+      .build();
+
+    assertThat(result.getKey()).isEqualTo("governance/project_dump");
+    assertThat(result.getName()).isEqualTo("Project Dump");
+    assertThat(result.getComponentQualifiers()).containsOnly(PROJECT, MODULE);
+    assertThat(result.getScope()).isEqualTo(COMPONENT);
+    assertThat(result.isAdmin()).isTrue();
+  }
+
+  @Test
+  public void qualifiers_map_to_key() {
+    assertThat(Qualifier.PROJECT.getKey()).isEqualTo(org.sonar.api.resources.Qualifiers.PROJECT);
+    assertThat(Qualifier.MODULE.getKey()).isEqualTo(org.sonar.api.resources.Qualifiers.MODULE);
+    assertThat(Qualifier.VIEW.getKey()).isEqualTo(org.sonar.api.resources.Qualifiers.VIEW);
+    assertThat(Qualifier.SUB_VIEW.getKey()).isEqualTo(org.sonar.api.resources.Qualifiers.SUBVIEW);
+  }
+
+  @Test
+  public void authorized_qualifiers() {
+    Qualifier[] qualifiers = Qualifier.values();
+
+    assertThat(qualifiers).hasSize(4).containsOnly(PROJECT, MODULE, VIEW, SUB_VIEW);
+  }
+
+  @Test
+  public void default_values() {
+    Page result = underTest.build();
+
+    assertThat(result.getComponentQualifiers()).isEmpty();
+    assertThat(result.getScope()).isEqualTo(GLOBAL);
+    assertThat(result.isAdmin()).isFalse();
+  }
+
+  @Test
+  public void all_qualifiers_when_component_page() {
+    Page result = underTest.setScope(COMPONENT).build();
+
+    assertThat(result.getComponentQualifiers()).containsOnly(Qualifier.values());
+  }
+
+  @Test
+  public void qualifiers_from_key() {
+    assertThat(Qualifier.fromKey(Qualifiers.PROJECT)).isEqualTo(Qualifier.PROJECT);
+    assertThat(Qualifier.fromKey("42")).isNull();
+  }
+
+  @Test
+  public void fail_if_no_qualifier() {
+    expectedException.expect(NullPointerException.class);
+
+    underTest.setComponentQualifiers(null).build();
+  }
+
+  @Test
+  public void fail_if_a_page_has_a_null_key() {
+    expectedException.expect(NullPointerException.class);
+
+    Page.builder(null).setName("Say my name").build();
+  }
+
+  @Test
+  public void fail_if_a_page_has_an_empty_key() {
+    expectedException.expect(IllegalArgumentException.class);
+
+    Page.builder("").setName("Say my name").build();
+  }
+
+  @Test
+  public void fail_if_a_page_has_a_null_name() {
+    expectedException.expect(IllegalArgumentException.class);
+
+    Page.builder("governance/project_dump").build();
+  }
+
+  @Test
+  public void fail_if_a_page_has_an_empty_name() {
+    expectedException.expect(IllegalArgumentException.class);
+
+    Page.builder("governance/project_dump").setName("").build();
+  }
+
+  @Test
+  public void fail_if_qualifiers_without_scope() {
+    expectedException.expect(IllegalArgumentException.class);
+
+    underTest.setComponentQualifiers(PROJECT).build();
+  }
+}