From: Julien Lancelot Date: Fri, 1 Jun 2018 09:31:31 +0000 (+0200) Subject: SONAR-10833 Return server status and sonarcloud flag in index.html X-Git-Tag: 7.5~1099 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=349b6fdb12230bf5b882367477bdac238c93a4ec;p=sonarqube.git SONAR-10833 Return server status and sonarcloud flag in index.html --- diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel2.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel2.java index a67ce655e51..ca1ef1020e5 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel2.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel2.java @@ -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 index 00000000000..bbd8c6904d9 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/web/WebPagesCache.java @@ -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 HTML_PATHS = ImmutableSet.of(INDEX_HTML_PATH, "/integration/vsts/index.html"); + + private final Platform platform; + private final Configuration configuration; + + private ServletContext servletContext; + private Map 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); + } + } + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/web/WebPagesFilter.java b/server/sonar-server/src/main/java/org/sonar/server/platform/web/WebPagesFilter.java index ab4990322f6..508e0fb39c4 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/web/WebPagesFilter.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/web/WebPagesFilter.java @@ -19,23 +19,18 @@ */ 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 HTML_PATHS = ImmutableSet.of(DEFAULT_HTML_PATH, "/integration/vsts/index.html"); - private static final Map 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 index 00000000000..e77c823a9ee --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/web/WebPagesCacheTest.java @@ -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) 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) 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); + } + +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/web/WebPagesFilterTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/web/WebPagesFilterTest.java index 75b11a726ad..d30ce6c66e4 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/platform/web/WebPagesFilterTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/web/WebPagesFilterTest.java @@ -19,27 +19,22 @@ */ 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) 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)); }