.addMapping("_default_", "{\"dynamic\": \"strict\"}")
.get();
- if (this.isBlocking) {
- while (node != null && !node.isClosed()) {
+ if (isBlocking) {
+ while (!node.isClosed()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
*/
package org.sonar.server.app;
-import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.startup.Tomcat;
import org.apache.commons.io.FileUtils;
return ready && tomcat != null;
}
- int port() {
- Connector[] connectors = tomcat.getService().findConnectors();
- if (connectors.length > 0) {
- return connectors[0].getLocalPort();
- }
- return -1;
- }
-
@Override
public void terminate() {
if (tomcat != null) {
--- /dev/null
+
+/*
+ * 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.app;
+
+import com.google.common.collect.ImmutableMap;
+import org.apache.catalina.connector.Connector;
+import org.apache.catalina.startup.Tomcat;
+import org.junit.Test;
+import org.mockito.ArgumentMatcher;
+import org.mockito.Mockito;
+import org.sonar.process.Props;
+
+import java.net.InetAddress;
+import java.util.Map;
+import java.util.Properties;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.fest.assertions.Fail.fail;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+public class ConnectorsTest {
+
+ Tomcat tomcat = mock(Tomcat.class, Mockito.RETURNS_DEEP_STUBS);
+
+ // ---- connectors
+
+ @Test
+ public void configure_thread_pool() throws Exception {
+ Properties p = new Properties();
+ p.setProperty("sonar.web.http.minThreads", "2");
+ p.setProperty("sonar.web.http.maxThreads", "30");
+ p.setProperty("sonar.web.http.acceptCount", "20");
+ Props props = new Props(p);
+
+ Connectors.configure(tomcat, props);
+
+ verify(tomcat).setConnector(argThat(new PropertiesMatcher(
+ ImmutableMap.<String, Object>of("minSpareThreads", 2, "maxThreads", 30, "acceptCount", 20)
+ )));
+ }
+
+ @Test
+ public void configure_default_thread_pool() throws Exception {
+ Props props = new Props(new Properties());
+
+ Connectors.configure(tomcat, props);
+
+ verify(tomcat).setConnector(argThat(new PropertiesMatcher(
+ ImmutableMap.<String, Object>of("minSpareThreads", 5, "maxThreads", 50, "acceptCount", 25)
+ )));
+ }
+
+ @Test
+ public void different_thread_pools_for_connectors() throws Exception {
+ Properties p = new Properties();
+ p.setProperty("sonar.web.port", "9000");
+ p.setProperty("sonar.web.http.minThreads", "2");
+ p.setProperty("sonar.web.https.port", "9443");
+ p.setProperty("sonar.web.https.minThreads", "5");
+ Props props = new Props(p);
+
+ Connectors.configure(tomcat, props);
+
+ verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
+ @Override
+ public boolean matches(Object o) {
+ Connector c = (Connector) o;
+ return c.getPort() == 9000 && c.getProperty("minSpareThreads").equals(2);
+ }
+ }));
+ verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
+ @Override
+ public boolean matches(Object o) {
+ Connector c = (Connector) o;
+ return c.getPort() == 9443 && c.getProperty("minSpareThreads").equals(5);
+ }
+ }));
+ }
+
+ @Test
+ public void fail_if_http_connectors_are_disabled() {
+ Properties p = new Properties();
+ p.setProperty("sonar.web.port", "-1");
+ p.setProperty("sonar.web.https.port", "-1");
+ Props props = new Props(p);
+
+ try {
+ Connectors.configure(tomcat, props);
+ fail();
+ } catch (IllegalStateException e) {
+ assertThat(e.getMessage()).isEqualTo("HTTP connectors are disabled");
+ }
+ }
+
+ @Test
+ public void only_https_is_enabled() {
+ Properties p = new Properties();
+ p.setProperty("sonar.web.port", "-1");
+ p.setProperty("sonar.web.https.port", "9443");
+ Props props = new Props(p);
+
+ Connectors.configure(tomcat, props);
+
+ verify(tomcat).setConnector(argThat(new ArgumentMatcher<Connector>() {
+ @Override
+ public boolean matches(Object o) {
+ Connector c = (Connector) o;
+ return c.getScheme().equals("https") && c.getPort() == 9443
+ && c.getProperty("clientAuth").equals("false");
+ }
+ }));
+ }
+
+ @Test
+ public void all_connectors_are_enabled() {
+ Properties p = new Properties();
+ p.setProperty("sonar.web.port", "9000");
+ p.setProperty("sonar.ajp.port", "9009");
+ p.setProperty("sonar.web.https.port", "9443");
+ Props props = new Props(p);
+
+ Connectors.configure(tomcat, props);
+
+ verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
+ @Override
+ public boolean matches(Object o) {
+ Connector c = (Connector) o;
+ return c.getScheme().equals("http") && c.getPort() == 9000 && c.getProtocol().equals(Connectors.HTTP_PROTOCOL);
+ }
+ }));
+ verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
+ @Override
+ public boolean matches(Object o) {
+ Connector c = (Connector) o;
+ return c.getScheme().equals("http") && c.getPort() == 9009 && c.getProtocol().equals(Connectors.AJP_PROTOCOL);
+ }
+ }));
+ verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
+ @Override
+ public boolean matches(Object o) {
+ Connector c = (Connector) o;
+ return c.getScheme().equals("https") && c.getPort() == 9443 && c.getProtocol().equals(Connectors.HTTP_PROTOCOL);
+ }
+ }));
+ }
+
+ @Test
+ public void http_and_ajp_and_https_ports_should_be_different() throws Exception {
+ Properties p = new Properties();
+ p.setProperty("sonar.web.port", "9000");
+ p.setProperty("sonar.ajp.port", "9000");
+ p.setProperty("sonar.web.https.port", "9000");
+
+ try {
+ Connectors.configure(tomcat, new Props(p));
+ fail();
+ } catch (IllegalStateException e) {
+ assertThat(e).hasMessage("HTTP, AJP and HTTPS must not use the same port 9000");
+ }
+ }
+
+ @Test
+ public void bind_to_all_addresses_by_default() throws Exception {
+ Properties p = new Properties();
+ p.setProperty("sonar.web.port", "9000");
+ p.setProperty("sonar.ajp.port", "9009");
+ p.setProperty("sonar.web.https.port", "9443");
+
+ Connectors.configure(tomcat, new Props(p));
+
+ verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
+ @Override
+ public boolean matches(Object o) {
+ Connector c = (Connector) o;
+ return c.getScheme().equals("http") && c.getPort() == 9000 && ((InetAddress) c.getProperty("address")).getHostAddress().equals("0.0.0.0");
+ }
+ }));
+ verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
+ @Override
+ public boolean matches(Object o) {
+ Connector c = (Connector) o;
+ return c.getScheme().equals("http") && c.getPort() == 9009 && ((InetAddress) c.getProperty("address")).getHostAddress().equals("0.0.0.0");
+ }
+ }));
+ verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
+ @Override
+ public boolean matches(Object o) {
+ Connector c = (Connector) o;
+ return c.getScheme().equals("https") && c.getPort() == 9443 && ((InetAddress) c.getProperty("address")).getHostAddress().equals("0.0.0.0");
+ }
+ }));
+ }
+
+ @Test
+ public void bind_to_specific_address() throws Exception {
+ Properties p = new Properties();
+ p.setProperty("sonar.web.port", "9000");
+ p.setProperty("sonar.web.https.port", "9443");
+ p.setProperty("sonar.web.host", "1.2.3.4");
+
+ Connectors.configure(tomcat, new Props(p));
+
+ verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
+ @Override
+ public boolean matches(Object o) {
+ Connector c = (Connector) o;
+ return c.getScheme().equals("http") && c.getPort() == 9000 && ((InetAddress) c.getProperty("address")).getHostAddress().equals("1.2.3.4");
+ }
+ }));
+ verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
+ @Override
+ public boolean matches(Object o) {
+ Connector c = (Connector) o;
+ return c.getScheme().equals("https") && c.getPort() == 9443 && ((InetAddress) c.getProperty("address")).getHostAddress().equals("1.2.3.4");
+ }
+ }));
+ }
+
+ @Test
+ public void enable_client_auth() throws Exception {
+
+ Properties p = new Properties();
+
+ p.setProperty("sonar.web.port", "-1");
+ p.setProperty("sonar.web.https.port", "9443");
+ p.setProperty("sonar.web.https.clientAuth", "want");
+
+ Props props = new Props(p);
+
+ Connectors.configure(tomcat, props);
+
+ verify(tomcat).setConnector(argThat(new ArgumentMatcher<Connector>() {
+ @Override
+ public boolean matches(Object o) {
+ Connector c = (Connector) o;
+ return c.getScheme().equals("https") && c.getProperty("clientAuth").equals("want");
+ }
+ }));
+ }
+
+ @Test
+ public void require_client_auth() throws Exception {
+
+ Properties p = new Properties();
+
+ p.setProperty("sonar.web.port", "-1");
+ p.setProperty("sonar.web.https.port", "9443");
+ p.setProperty("sonar.web.https.clientAuth", "true");
+
+ Props props = new Props(p);
+
+ Connectors.configure(tomcat, props);
+
+ verify(tomcat).setConnector(argThat(new ArgumentMatcher<Connector>() {
+ @Override
+ public boolean matches(Object o) {
+ Connector c = (Connector) o;
+ return c.getScheme().equals("https") && c.getProperty("clientAuth").equals("true");
+ }
+ }));
+ }
+
+ private static class PropertiesMatcher extends ArgumentMatcher<Connector> {
+ private final Map<String, Object> expected;
+
+ PropertiesMatcher(Map<String, Object> expected) {
+ this.expected = expected;
+ }
+
+ public boolean matches(Object o) {
+ Connector c = (Connector) o;
+ for (Map.Entry<String, Object> entry : expected.entrySet()) {
+ if (!entry.getValue().equals(c.getProperty(entry.getKey()))) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+}
--- /dev/null
+/*
+ * 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.app;
+
+import ch.qos.logback.access.tomcat.LogbackValve;
+import org.apache.catalina.Lifecycle;
+import org.apache.catalina.LifecycleEvent;
+import org.apache.catalina.Valve;
+import org.apache.catalina.startup.Tomcat;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.ArgumentMatcher;
+import org.mockito.Mockito;
+import org.slf4j.Logger;
+import org.sonar.process.Props;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Properties;
+
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+public class LoggingTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Before
+ public void setHome() throws IOException {
+ File homeDir = temp.newFolder("home");
+ System.setProperty("SONAR_HOME", homeDir.getAbsolutePath());
+ }
+
+ @Test
+ public void enable_access_logs_by_Default() throws Exception {
+ Tomcat tomcat = mock(Tomcat.class, Mockito.RETURNS_DEEP_STUBS);
+ Props props = new Props(new Properties());
+ props.set("sonar.path.web", temp.newFolder().getAbsolutePath());
+ Logging.configure(tomcat, props);
+
+ verify(tomcat.getHost().getPipeline()).addValve(argThat(new ArgumentMatcher<Valve>() {
+ @Override
+ public boolean matches(Object o) {
+ LogbackValve v = (LogbackValve) o;
+ String confFile = v.getFilename();
+ return confFile.endsWith("logback-access.xml");
+ }
+ }));
+ }
+
+ @Test
+ public void log_when_started_and_stopped() {
+ Logger logger = mock(Logger.class);
+ Logging.LifecycleLogger listener = new Logging.LifecycleLogger(logger);
+
+ LifecycleEvent event = new LifecycleEvent(mock(Lifecycle.class), "before_init", null);
+ listener.lifecycleEvent(event);
+ verifyZeroInteractions(logger);
+
+ event = new LifecycleEvent(mock(Lifecycle.class), "after_start", null);
+ listener.lifecycleEvent(event);
+ verify(logger).info("Web server is started");
+
+ event = new LifecycleEvent(mock(Lifecycle.class), "after_destroy", null);
+ listener.lifecycleEvent(event);
+ verify(logger).info("Web server is stopped");
+ }
+}
--- /dev/null
+/*
+ * 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.
+ */
+
+/*
+ * 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.app;
+
+import org.apache.tomcat.JarScannerCallback;
+import org.junit.Test;
+
+import javax.servlet.ServletContext;
+
+import java.util.HashSet;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+public class NullJarScannerTest {
+ @Test
+ public void does_nothing() {
+ ServletContext context = mock(ServletContext.class);
+ ClassLoader classloader = mock(ClassLoader.class);
+ JarScannerCallback callback = mock(JarScannerCallback.class);
+ new NullJarScanner().scan(context, classloader, callback, new HashSet<String>());
+ verifyZeroInteractions(context, classloader, callback);
+ }
+}
--- /dev/null
+/*
+ * 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.app;
+
+import org.apache.catalina.Context;
+import org.apache.catalina.core.StandardContext;
+import org.apache.catalina.startup.Tomcat;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.process.Props;
+
+import java.io.File;
+import java.util.Properties;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.fest.assertions.Fail.fail;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class WebappTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ Props props = new Props(new Properties());
+
+ @Test
+ public void fail_on_error() throws Exception {
+ File webDir = temp.newFolder("web");
+
+ Tomcat tomcat = mock(Tomcat.class, RETURNS_DEEP_STUBS);
+ when(tomcat.addContext("", webDir.getAbsolutePath())).thenThrow(new NullPointerException());
+
+ try {
+ Webapp.configure(tomcat, props);
+ fail();
+ } catch (IllegalStateException e) {
+ assertThat(e).hasMessage("Fail to configure webapp");
+ }
+ }
+
+ @Test
+ public void configure_context() throws Exception {
+ props.set("foo", "bar");
+ StandardContext context = mock(StandardContext.class);
+ Tomcat tomcat = mock(Tomcat.class);
+ when(tomcat.addWebapp(anyString(), anyString())).thenReturn(context);
+
+ Webapp.configure(tomcat, props);
+
+ // configure webapp with properties
+ verify(context).addParameter("foo", "bar");
+ }
+
+ @Test
+ public void configure_rails_dev_mode() throws Exception {
+ props.set("sonar.web.dev", "true");
+ Context context = mock(Context.class);
+
+ Webapp.configureRails(props, context);
+
+ verify(context).addParameter("jruby.max.runtimes", "3");
+ verify(context).addParameter("rails.env", "development");
+ }
+
+ @Test
+ public void configure_production_mode() throws Exception {
+ props.set("sonar.web.dev", "false");
+ Context context = mock(Context.class);
+
+ Webapp.configureRails(props, context);
+
+ verify(context).addParameter("jruby.max.runtimes", "1");
+ verify(context).addParameter("rails.env", "production");
+ }
+
+ @Test
+ public void context_path_must_start_with_slash() throws Exception {
+ Properties p = new Properties();
+ p.setProperty("sonar.web.context", "foo");
+
+ try {
+ Webapp.getContextPath(new Props(p));
+ fail();
+ } catch (IllegalStateException e) {
+ assertThat(e.getMessage()).isEqualTo("Value of 'sonar.web.context' must start with a forward slash: 'foo'");
+ }
+ }
+
+ @Test
+ public void root_context_path_must_be_blank() throws Exception {
+ Properties p = new Properties();
+ p.setProperty("sonar.web.context", "/");
+
+ assertThat(Webapp.getContextPath(new Props(p))).isEqualTo("");
+ }
+
+ @Test
+ public void default_context_path_is_root() throws Exception {
+ String context = Webapp.getContextPath(new Props(new Properties()));
+ assertThat(context).isEqualTo("");
+ }
+}
}
UsedQProfiles used = new UsedQProfiles();
for (Measure childProfilesMeasure : context.getChildrenMeasures(CoreMetrics.QUALITY_PROFILES)) {
- UsedQProfiles childProfiles = UsedQProfiles.fromJson(childProfilesMeasure.getData());
- used.add(childProfiles);
+ String data = childProfilesMeasure.getData();
+ if (data != null) {
+ UsedQProfiles childProfiles = UsedQProfiles.fromJson(data);
+ used.add(childProfiles);
+ }
}
Measure detailsMeasure = new Measure(CoreMetrics.QUALITY_PROFILES, used.toJson());
doCompleteProperties(properties);
dialect = DialectUtils.find(properties.getProperty(SONAR_JDBC_DIALECT), properties.getProperty(SONAR_JDBC_URL));
- if (dialect == null) {
- throw new IllegalStateException(String.format("Can not guess the JDBC dialect. Please check the property %s.", SONAR_JDBC_URL));
- }
properties.setProperty(DatabaseProperties.PROP_DRIVER, dialect.getDefaultDriverClassName());
}
import com.google.common.base.Predicate;
import com.google.common.collect.Iterators;
import org.apache.commons.lang.StringUtils;
-import org.sonar.api.utils.SonarException;
+import org.sonar.api.utils.MessageException;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
+
import java.util.NoSuchElementException;
public final class DialectUtils {
public static Dialect find(final String dialectId, final String jdbcConnectionUrl) {
Dialect match = StringUtils.isNotBlank(dialectId) ? findById(dialectId) : findByJdbcUrl(jdbcConnectionUrl);
if (match == null) {
- throw new SonarException("Unable to determine database dialect to use within sonar with dialect " + dialectId + " jdbc url " + jdbcConnectionUrl);
+ throw MessageException.of("Unable to determine database dialect to use within sonar with dialect " + dialectId + " jdbc url " + jdbcConnectionUrl);
}
return match;
}
package org.sonar.core.persistence.dialect;
import org.junit.Test;
-import org.sonar.api.utils.SonarException;
+import org.sonar.api.utils.MessageException;
import static org.fest.assertions.Assertions.assertThat;
assertThat(d).isInstanceOf(MySql.class);
}
- @Test(expected = SonarException.class)
+ @Test(expected = MessageException.class)
public void testFindNoMatch() {
DialectUtils.find("foo", "bar");
}