]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10833 Return server status and sonarcloud flag in index.html
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Fri, 1 Jun 2018 09:31:31 +0000 (11:31 +0200)
committerSonarTech <sonartech@sonarsource.com>
Mon, 4 Jun 2018 18:20:50 +0000 (20:20 +0200)
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel2.java
server/sonar-server/src/main/java/org/sonar/server/platform/web/WebPagesCache.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/platform/web/WebPagesFilter.java
server/sonar-server/src/test/java/org/sonar/server/platform/web/WebPagesCacheTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/platform/web/WebPagesFilterTest.java

index a67ce655e5147d6276ff0e61a088179830bbb1ab..ca1ef1020e52d169b7f39bb2ecc978cbebeb8eb9 100644 (file)
@@ -35,6 +35,7 @@ import org.sonar.server.platform.db.migration.charset.DatabaseCharsetChecker;
 import org.sonar.server.platform.db.migration.history.MigrationHistoryTable;
 import org.sonar.server.platform.db.migration.history.MigrationHistoryTableImpl;
 import org.sonar.server.platform.db.migration.version.DatabaseVersion;
+import org.sonar.server.platform.web.WebPagesCache;
 import org.sonar.server.plugins.InstalledPluginReferentialFactory;
 import org.sonar.server.plugins.PluginFileSystem;
 import org.sonar.server.plugins.ServerPluginJarExploder;
@@ -58,6 +59,9 @@ public class PlatformLevel2 extends PlatformLevel {
       DefaultServerUpgradeStatus.class,
       Durations.class,
 
+      // index.html cache
+      WebPagesCache.class,
+
       // plugins
       ServerPluginRepository.class,
       ServerPluginJarExploder.class,
diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/web/WebPagesCache.java b/server/sonar-server/src/main/java/org/sonar/server/platform/web/WebPagesCache.java
new file mode 100644 (file)
index 0000000..bbd8c69
--- /dev/null
@@ -0,0 +1,109 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.platform.web;
+
+import com.google.common.collect.ImmutableSet;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import javax.servlet.ServletContext;
+import org.apache.commons.io.IOUtils;
+import org.sonar.api.config.Configuration;
+import org.sonar.server.platform.Platform;
+import org.sonar.server.platform.Platform.Status;
+
+import static com.google.common.base.Preconditions.checkState;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Objects.requireNonNull;
+import static org.sonar.process.ProcessProperties.Property.SONARCLOUD_ENABLED;
+import static org.sonar.server.platform.Platform.Status.UP;
+
+public class WebPagesCache {
+
+  private static final String WEB_CONTEXT_PLACEHOLDER = "%WEB_CONTEXT%";
+  private static final String SERVER_STATUS_PLACEHOLDER = "%SERVER_STATUS%";
+  private static final String INSTANCE_PLACEHOLDER = "%INSTANCE%";
+
+  private static final String SONARCLOUD_INSTANCE_VALUE = "SonarCloud";
+  private static final String SONARQUBE_INSTANCE_VALUE = "SonarQube";
+
+  private static final String INDEX_HTML_PATH = "/index.html";
+
+  private static final Set<String> HTML_PATHS = ImmutableSet.of(INDEX_HTML_PATH, "/integration/vsts/index.html");
+
+  private final Platform platform;
+  private final Configuration configuration;
+
+  private ServletContext servletContext;
+  private Map<String, String> indexHtmlByPath;
+  private Status status;
+
+  public WebPagesCache(Platform platform, Configuration configuration) {
+    this.platform = platform;
+    this.configuration = configuration;
+    this.indexHtmlByPath = new HashMap<>();
+  }
+
+  public void init(ServletContext servletContext) {
+    this.servletContext = servletContext;
+    generate(platform.status());
+  }
+
+  public String getContent(String path) {
+    String htmlPath = HTML_PATHS.contains(path) ? path : INDEX_HTML_PATH;
+    checkState(servletContext != null, "init has not been called");
+    // Optimization to not have to call platform.currentStatus on each call
+    if (Objects.equals(status, UP)) {
+      return indexHtmlByPath.get(htmlPath);
+    }
+    Status currentStatus = platform.status();
+    if (!Objects.equals(status, currentStatus)) {
+      generate(currentStatus);
+    }
+    return indexHtmlByPath.get(htmlPath);
+  }
+
+  private void generate(Status status) {
+    this.status = status;
+    HTML_PATHS.forEach(path -> indexHtmlByPath.put(path, provide(path)));
+  }
+
+  private String provide(String path) {
+    getClass().getResourceAsStream(INDEX_HTML_PATH);
+    boolean isSonarCloud = configuration.getBoolean(SONARCLOUD_ENABLED.getKey()).orElse(false);
+    String instance = isSonarCloud ? SONARCLOUD_INSTANCE_VALUE : SONARQUBE_INSTANCE_VALUE;
+    return loadHtmlFile(path, status.name(), instance);
+  }
+
+  private String loadHtmlFile(String path, String serverStatus, String instance) {
+    try (InputStream input = servletContext.getResourceAsStream(path)) {
+      String template = IOUtils.toString(requireNonNull(input), UTF_8);
+      return template
+        .replaceAll(WEB_CONTEXT_PLACEHOLDER, servletContext.getContextPath())
+        .replaceAll(SERVER_STATUS_PLACEHOLDER, serverStatus)
+        .replaceAll(INSTANCE_PLACEHOLDER, instance);
+    } catch (Exception e) {
+      throw new IllegalStateException("Fail to load file " + path, e);
+    }
+  }
+
+}
index ab4990322f6af37645f36a9a3d9aa594d4bd9db1..508e0fb39c4a9ad6afc495eabe38eb428f04e9ec 100644 (file)
  */
 package org.sonar.server.platform.web;
 
-import com.google.common.collect.ImmutableSet;
+import com.google.common.annotations.VisibleForTesting;
 import java.io.IOException;
-import java.io.InputStream;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
 import javax.servlet.Filter;
 import javax.servlet.FilterChain;
 import javax.servlet.FilterConfig;
-import javax.servlet.ServletContext;
 import javax.servlet.ServletException;
 import javax.servlet.ServletRequest;
 import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import org.apache.commons.io.IOUtils;
 import org.sonar.api.web.ServletFilter;
+import org.sonar.server.platform.Platform;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Locale.ENGLISH;
@@ -52,23 +47,26 @@ public class WebPagesFilter implements Filter {
 
   private static final String CACHE_CONTROL_HEADER = "Cache-Control";
   private static final String CACHE_CONTROL_VALUE = "no-cache, no-store, must-revalidate";
-  private static final String WEB_CONTEXT_PLACEHOLDER = "%WEB_CONTEXT%";
-  private static final String DEFAULT_HTML_PATH = "/index.html";
-  // all the html files to be loaded from disk
-  private static final Set<String> HTML_PATHS = ImmutableSet.of(DEFAULT_HTML_PATH, "/integration/vsts/index.html");
-  private static final Map<String, String> HTML_CONTENTS_BY_PATH = new HashMap<>();
 
   private static final ServletFilter.UrlPattern URL_PATTERN = ServletFilter.UrlPattern
     .builder()
     .excludes(staticResourcePatterns())
     .build();
 
+  private WebPagesCache webPagesCache;
+
+  public WebPagesFilter() {
+    this(Platform.getInstance().getContainer().getComponentByType(WebPagesCache.class));
+  }
+
+  @VisibleForTesting
+  WebPagesFilter(WebPagesCache webPagesCache) {
+    this.webPagesCache = webPagesCache;
+  }
+
   @Override
   public void init(FilterConfig filterConfig) {
-    HTML_PATHS.forEach(path -> {
-      ServletContext servletContext = filterConfig.getServletContext();
-      HTML_CONTENTS_BY_PATH.put(path, loadHtmlFile(servletContext, path));
-    });
+    webPagesCache.init(filterConfig.getServletContext());
   }
 
   @Override
@@ -83,18 +81,9 @@ public class WebPagesFilter implements Filter {
     httpServletResponse.setContentType(HTML);
     httpServletResponse.setCharacterEncoding(UTF_8.name().toLowerCase(ENGLISH));
     httpServletResponse.setHeader(CACHE_CONTROL_HEADER, CACHE_CONTROL_VALUE);
-    String htmlPath = HTML_PATHS.contains(path) ? path : DEFAULT_HTML_PATH;
-    String htmlContent = requireNonNull(HTML_CONTENTS_BY_PATH.get(htmlPath));
-    write(htmlContent, httpServletResponse.getOutputStream(), UTF_8);
-  }
 
-  private static String loadHtmlFile(ServletContext context, String path) {
-    try (InputStream input = context.getResourceAsStream(path)) {
-      String template = IOUtils.toString(requireNonNull(input), UTF_8);
-      return template.replaceAll(WEB_CONTEXT_PLACEHOLDER, context.getContextPath());
-    } catch (Exception e) {
-      throw new IllegalStateException("Fail to load file " + path, e);
-    }
+    String htmlContent = requireNonNull(webPagesCache.getContent(path));
+    write(htmlContent, httpServletResponse.getOutputStream(), UTF_8);
   }
 
   @Override
diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/web/WebPagesCacheTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/web/WebPagesCacheTest.java
new file mode 100644 (file)
index 0000000..e77c823
--- /dev/null
@@ -0,0 +1,166 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.platform.web;
+
+import java.io.InputStream;
+import javax.servlet.ServletContext;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.mockito.stubbing.Answer;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.server.platform.Platform;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.commons.io.IOUtils.toInputStream;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.server.platform.Platform.Status.BOOTING;
+import static org.sonar.server.platform.Platform.Status.STARTING;
+import static org.sonar.server.platform.Platform.Status.UP;
+
+public class WebPagesCacheTest {
+
+  private static final String TEST_CONTEXT = "/sonarqube";
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  private ServletContext servletContext = mock(ServletContext.class);
+
+  private Platform platform = mock(Platform.class);
+  private MapSettings mapSettings = new MapSettings();
+
+  private WebPagesCache underTest = new WebPagesCache(platform, mapSettings.asConfig());
+
+  @Before
+  public void setUp() throws Exception {
+    when(servletContext.getContextPath()).thenReturn(TEST_CONTEXT);
+    when(servletContext.getResourceAsStream("/index.html")).thenAnswer(
+      (Answer<InputStream>) invocationOnMock -> toInputStream("Content of default index.html with context [%WEB_CONTEXT%], status [%SERVER_STATUS%], instance [%INSTANCE%]",
+        UTF_8));
+    when(servletContext.getResourceAsStream("/integration/vsts/index.html"))
+      .thenAnswer((Answer<InputStream>) invocationOnMock -> toInputStream("Content of vsts index.html with context [%WEB_CONTEXT%]", UTF_8));
+  }
+
+  @Test
+  public void check_paths() {
+    doInit();
+    when(platform.status()).thenReturn(UP);
+
+    assertThat(underTest.getContent("/foo")).contains(TEST_CONTEXT).contains("default");
+    assertThat(underTest.getContent("/foo.html")).contains(TEST_CONTEXT).contains("default");
+    assertThat(underTest.getContent("/index")).contains(TEST_CONTEXT).contains("default");
+    assertThat(underTest.getContent("/index.html")).contains(TEST_CONTEXT).contains("default");
+    assertThat(underTest.getContent("/integration/vsts/index.html")).contains(TEST_CONTEXT).contains("vsts");
+  }
+
+  @Test
+  public void contains_web_context() {
+    doInit();
+
+    assertThat(underTest.getContent("/foo"))
+      .contains(TEST_CONTEXT);
+  }
+
+  @Test
+  public void status_is_starting() {
+    doInit();
+    when(platform.status()).thenReturn(STARTING);
+
+    assertThat(underTest.getContent("/foo"))
+      .contains(STARTING.name());
+  }
+
+  @Test
+  public void status_is_up() {
+    doInit();
+    when(platform.status()).thenReturn(UP);
+
+    assertThat(underTest.getContent("/foo"))
+      .contains(UP.name());
+  }
+
+  @Test
+  public void no_sonarcloud_setting() {
+    doInit();
+
+    assertThat(underTest.getContent("/foo"))
+      .contains("SonarQube");
+  }
+
+  @Test
+  public void sonarcloud_setting_is_false() {
+    mapSettings.setProperty("sonar.sonarcloud.enabled", false);
+    doInit();
+
+    assertThat(underTest.getContent("/foo"))
+      .contains("SonarQube");
+  }
+
+  @Test
+  public void sonarcloud_setting_is_true() {
+    mapSettings.setProperty("sonar.sonarcloud.enabled", true);
+    doInit();
+
+    assertThat(underTest.getContent("/foo"))
+      .contains("SonarCloud");
+  }
+
+  @Test
+  public void content_is_updated_when_status_has_changed() {
+    doInit();
+    when(platform.status()).thenReturn(STARTING);
+    assertThat(underTest.getContent("/foo"))
+      .contains(STARTING.name());
+
+    when(platform.status()).thenReturn(UP);
+    assertThat(underTest.getContent("/foo"))
+      .contains(UP.name());
+  }
+
+  @Test
+  public void content_is_not_updated_when_status_is_up() {
+    doInit();
+    when(platform.status()).thenReturn(UP);
+    assertThat(underTest.getContent("/foo"))
+      .contains(UP.name());
+
+    when(platform.status()).thenReturn(STARTING);
+    assertThat(underTest.getContent("/foo"))
+      .contains(UP.name());
+  }
+
+  @Test
+  public void fail_to_get_content_when_init_has_not_been_called() {
+    expectedException.expect(IllegalStateException.class);
+    expectedException.expectMessage("init has not been called");
+
+    underTest.getContent("/foo");
+  }
+
+  private void doInit() {
+    when(platform.status()).thenReturn(BOOTING);
+    underTest.init(servletContext);
+  }
+
+}
index 75b11a726ad92d1c5ee1fca487c9e41a83ff155a..d30ce6c66e486dd04185bb69647876b0633c51eb 100644 (file)
  */
 package org.sonar.server.platform.web;
 
-import java.io.InputStream;
 import javax.servlet.FilterChain;
-import javax.servlet.FilterConfig;
 import javax.servlet.ServletContext;
 import javax.servlet.ServletOutputStream;
 import javax.servlet.WriteListener;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import org.apache.commons.io.IOUtils;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
-import org.mockito.stubbing.Answer;
 
-import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.RETURNS_MOCKS;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
 public class WebPagesFilterTest {
@@ -49,51 +44,45 @@ public class WebPagesFilterTest {
   public ExpectedException expectedException = ExpectedException.none();
 
   private ServletContext servletContext = mock(ServletContext.class, RETURNS_MOCKS);
-  private FilterConfig filterConfig = mock(FilterConfig.class);
-  private WebPagesFilter underTest = new WebPagesFilter();
+  private WebPagesCache webPagesCache = mock(WebPagesCache.class);
+
+  private HttpServletRequest request = mock(HttpServletRequest.class);
+  private HttpServletResponse response = mock(HttpServletResponse.class);
+  private FilterChain chain = mock(FilterChain.class);
+
+  private WebPagesFilter underTest = new WebPagesFilter(webPagesCache);
 
   @Before
   public void setUp() throws Exception {
     when(servletContext.getContextPath()).thenReturn(TEST_CONTEXT);
-    when(servletContext.getResourceAsStream(anyString())).thenAnswer((Answer<InputStream>) invocationOnMock -> {
-      String path = invocationOnMock.getArgument(0);
-      return IOUtils.toInputStream("Content of " + path + " with context [%WEB_CONTEXT%]", UTF_8);
-    });
-    when(filterConfig.getServletContext()).thenReturn(servletContext);
-    underTest.init(filterConfig);
-  }
-
-  @Test
-  public void return_base_index_dot_html_if_not_static_resource() throws Exception {
-    verifyDefaultHtml("/index.html");
-    verifyDefaultHtml("/foo");
-    verifyDefaultHtml("/foo.html");
   }
 
   @Test
-  public void return_integration_html() throws Exception {
-    verifyHtml("/integration/vsts/index.html", "Content of /integration/vsts/index.html with context [" + TEST_CONTEXT + "]");
-  }
-
-  private void verifyDefaultHtml(String path) throws Exception {
-    verifyHtml(path, "Content of /index.html with context [" + TEST_CONTEXT + "]");
-  }
-
-  private void verifyHtml(String path, String expectedContent) throws Exception {
-    HttpServletRequest request = mock(HttpServletRequest.class);
+  public void return_web_page_content() throws Exception {
+    String path = "/index.html";
+    when(webPagesCache.getContent(path)).thenReturn("test");
     when(request.getRequestURI()).thenReturn(path);
     when(request.getContextPath()).thenReturn(TEST_CONTEXT);
-    HttpServletResponse response = mock(HttpServletResponse.class);
     StringOutputStream outputStream = new StringOutputStream();
     when(response.getOutputStream()).thenReturn(outputStream);
-    FilterChain chain = mock(FilterChain.class);
 
     underTest.doFilter(request, response, chain);
 
     verify(response).setContentType("text/html");
     verify(response).setCharacterEncoding("utf-8");
     verify(response).setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
-    assertThat(outputStream.toString()).isEqualTo(expectedContent);
+    assertThat(outputStream.toString()).isEqualTo("test");
+  }
+
+  @Test
+  public void does_nothing_when_static_resource() throws Exception{
+    when(request.getRequestURI()).thenReturn("/static");
+    when(request.getContextPath()).thenReturn(TEST_CONTEXT);
+
+    underTest.doFilter(request, response, chain);
+
+    verify(chain).doFilter(request, response);
+    verifyZeroInteractions(webPagesCache);
   }
 
   class StringOutputStream extends ServletOutputStream {
@@ -121,7 +110,7 @@ public class WebPagesFilterTest {
     }
 
     public void write(int b) {
-      byte[] bytes = new byte[]{(byte) b};
+      byte[] bytes = new byte[] {(byte) b};
       this.buf.append(new String(bytes));
     }