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;
DefaultServerUpgradeStatus.class,
Durations.class,
+ // index.html cache
+ WebPagesCache.class,
+
// plugins
ServerPluginRepository.class,
ServerPluginJarExploder.class,
--- /dev/null
+/*
+ * 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);
+ }
+ }
+
+}
*/
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;
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
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
--- /dev/null
+/*
+ * 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);
+ }
+
+}
*/
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 {
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 {
}
public void write(int b) {
- byte[] bytes = new byte[]{(byte) b};
+ byte[] bytes = new byte[] {(byte) b};
this.buf.append(new String(bytes));
}