Commit 513c6a1d authored by Phillip Webb's avatar Phillip Webb

Polish

parent 85fb1cba
...@@ -19,7 +19,7 @@ Javadocs. Some rules of thumb: ...@@ -19,7 +19,7 @@ Javadocs. Some rules of thumb:
* Look for classes called `*AutoConfiguration` and read their sources, * Look for classes called `*AutoConfiguration` and read their sources,
in particular the `@Conditional*` annotations to find out what in particular the `@Conditional*` annotations to find out what
features they enable and when. In those clases... features they enable and when. In those clases...
* Look for classes that are `@ConfigurationProperties` * Look for classes that are `@ConfigurationProperties`
(e.g. [`ServerProperties`](https://github.com/spring-projects/spring-boot/blob/master/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java?source=c)) (e.g. [`ServerProperties`](https://github.com/spring-projects/spring-boot/blob/master/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java?source=c))
and read from there the available external configuration and read from there the available external configuration
...@@ -27,16 +27,16 @@ Javadocs. Some rules of thumb: ...@@ -27,16 +27,16 @@ Javadocs. Some rules of thumb:
acts as a prefix to external properties, thus `ServerProperties` has acts as a prefix to external properties, thus `ServerProperties` has
`name="server"` and its configuration properties are `server.port`, `name="server"` and its configuration properties are `server.port`,
`server.address` etc. `server.address` etc.
* Look for use of `RelaxedEnvironment` to pull configuration values * Look for use of `RelaxedEnvironment` to pull configuration values
explicitly out of the `Environment`. It often is used with a prefix. explicitly out of the `Environment`. It often is used with a prefix.
* Look for `@Value` annotations that bind directly to the * Look for `@Value` annotations that bind directly to the
`Environment`. This is less flexible than the `RelaxedEnvironment` `Environment`. This is less flexible than the `RelaxedEnvironment`
approach, but does allow some relaxed binding, specifically for OS approach, but does allow some relaxed binding, specifically for OS
environment variables (so `CAPITALS_AND_UNDERSCORES` are synonyms environment variables (so `CAPITALS_AND_UNDERSCORES` are synonyms
for `period.separated`). for `period.separated`).
* Look for `@ConditionalOnExpression` annotations that switch features * Look for `@ConditionalOnExpression` annotations that switch features
on and off in response to SpEL expressions, normally evaluated with on and off in response to SpEL expressions, normally evaluated with
placeholders resolved from the `Environment`. placeholders resolved from the `Environment`.
...@@ -84,7 +84,7 @@ In addition, if your context contains any beans of type `ObjectMapper` ...@@ -84,7 +84,7 @@ In addition, if your context contains any beans of type `ObjectMapper`
then all of the `Module` beans will be registered with all of the then all of the `Module` beans will be registered with all of the
mappers. So there is a global mechanism for contributing custom mappers. So there is a global mechanism for contributing custom
modules when you add new features to your application. modules when you add new features to your application.
Finally, if you provide any `@Beans` of type Finally, if you provide any `@Beans` of type
`MappingJackson2HttpMessageConverter` then they will replace the `MappingJackson2HttpMessageConverter` then they will replace the
default value in the MVC configuration. Also, a convenience bean is default value in the MVC configuration. Also, a convenience bean is
...@@ -173,7 +173,7 @@ public EmbeddedServletContainerCustomizer containerCustomizer(){ ...@@ -173,7 +173,7 @@ public EmbeddedServletContainerCustomizer containerCustomizer(){
if(factory instanceof TomcatEmbeddedServletContainerFactory){ if(factory instanceof TomcatEmbeddedServletContainerFactory){
TomcatEmbeddedServletContainerFactory containerFactory = (TomcatEmbeddedServletContainerFactory) factory; TomcatEmbeddedServletContainerFactory containerFactory = (TomcatEmbeddedServletContainerFactory) factory;
containerFactory.addConnectorCustomizers(new TomcatConnectorCustomizer() { containerFactory.addConnectorCustomizers(new TomcatConnectorCustomizer() {
@Override @Override
public void customize(Connector connector) { public void customize(Connector connector) {
...@@ -190,7 +190,7 @@ public EmbeddedServletContainerCustomizer containerCustomizer(){ ...@@ -190,7 +190,7 @@ public EmbeddedServletContainerCustomizer containerCustomizer(){
connector.setAttribute("clientAuth", "false"); connector.setAttribute("clientAuth", "false");
connector.setAttribute("sslProtocol", "TLS"); connector.setAttribute("sslProtocol", "TLS");
connector.setAttribute("SSLEnabled", true); connector.setAttribute("SSLEnabled", true);
}); });
} }
} }
...@@ -256,7 +256,7 @@ Create a deployable WAR by extending `SpringBootServletInitializer` ...@@ -256,7 +256,7 @@ Create a deployable WAR by extending `SpringBootServletInitializer`
@EnableAutoConfiguration @EnableAutoConfiguration
@ComponentScan @ComponentScan
public class Application extends SpringBootServletInitializer { public class Application extends SpringBootServletInitializer {
@Override @Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class); return application.sources(Application.class);
...@@ -307,7 +307,7 @@ Applications can fall into more than one category: ...@@ -307,7 +307,7 @@ Applications can fall into more than one category:
* Servlet 3.0 applications with no `web.xml` * Servlet 3.0 applications with no `web.xml`
* Applications with a `web.xml` * Applications with a `web.xml`
* Applications with a context hierarchy and * Applications with a context hierarchy and
* Those without a context hierarchy * Those without a context hierarchy
All of these should be amenable to translation, but each might require All of these should be amenable to translation, but each might require
...@@ -465,7 +465,7 @@ want to set levels, e.g. ...@@ -465,7 +465,7 @@ want to set levels, e.g.
```xml ```xml
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<configuration> <configuration>
<include resource="org/springframework/boot/logging/logback/base.xml"/> <include resource="org/springframework/boot/logging/logback/base.xml"/>
<logger name="org.springframework.web" level="DEBUG"/> <logger name="org.springframework.web" level="DEBUG"/>
</configuration> </configuration>
``` ```
...@@ -478,7 +478,7 @@ will see that it uses some useful System properties which the ...@@ -478,7 +478,7 @@ will see that it uses some useful System properties which the
* `${LOG_FILE}` if `logging.file` was set in Boot's external configuration * `${LOG_FILE}` if `logging.file` was set in Boot's external configuration
* `${LOG_PATH` if `logging.path` was set (representing a directory for * `${LOG_PATH` if `logging.path` was set (representing a directory for
log files to live in) log files to live in)
Spring Boot also provides some nice ANSI colour terminal output on a Spring Boot also provides some nice ANSI colour terminal output on a
console (but not in a log file) using a custom Logback converter. See console (but not in a log file) using a custom Logback converter. See
the default `base.xml` configuration for details. the default `base.xml` configuration for details.
...@@ -626,7 +626,7 @@ algorithm for choosing a specific implementation. ...@@ -626,7 +626,7 @@ algorithm for choosing a specific implementation.
* If neither of those is available but an embedded database is then we * If neither of those is available but an embedded database is then we
create one of those for you (preference order is h2, then Apache create one of those for you (preference order is h2, then Apache
Derby, then hsqldb). Derby, then hsqldb).
The pooling `DataSource` option is controlled by external The pooling `DataSource` option is controlled by external
configuration properties in `spring.datasource.*` for example: configuration properties in `spring.datasource.*` for example:
...@@ -742,7 +742,7 @@ Properties from different sources are added to the Spring ...@@ -742,7 +742,7 @@ Properties from different sources are added to the Spring
`Environment` in a defined order, and the precedence for resolution is `Environment` in a defined order, and the precedence for resolution is
1) commandline, 2) filesystem (current working directory) 1) commandline, 2) filesystem (current working directory)
`application.properties`, 3) classpath `application.properties`. To `application.properties`, 3) classpath `application.properties`. To
modify this you can provide System properties (or environment variables) modify this you can provide System properties (or environment variables)
* `config.name` (`CONFIG_NAME`), defaults to `application` as the root * `config.name` (`CONFIG_NAME`), defaults to `application` as the root
of the file name of the file name
......
...@@ -57,7 +57,7 @@ public class ConfigurationPropertiesReportEndpoint extends ...@@ -57,7 +57,7 @@ public class ConfigurationPropertiesReportEndpoint extends
return this.keysToSanitize; return this.keysToSanitize;
} }
public void setKeysToSanitize(String[] keysToSanitize) { public void setKeysToSanitize(String... keysToSanitize) {
Assert.notNull(keysToSanitize, "KeysToSanitize must not be null"); Assert.notNull(keysToSanitize, "KeysToSanitize must not be null");
this.keysToSanitize = keysToSanitize; this.keysToSanitize = keysToSanitize;
} }
...@@ -69,10 +69,10 @@ public class ConfigurationPropertiesReportEndpoint extends ...@@ -69,10 +69,10 @@ public class ConfigurationPropertiesReportEndpoint extends
.getBeansWithAnnotation(ConfigurationProperties.class); .getBeansWithAnnotation(ConfigurationProperties.class);
// Serialize beans into map structure and sanitize values // Serialize beans into map structure and sanitize values
ObjectMapper mapper = new ObjectMapper();
for (Map.Entry<String, Object> entry : beans.entrySet()) { for (Map.Entry<String, Object> entry : beans.entrySet()) {
ObjectMapper m = new ObjectMapper(); Map<String, Object> value = mapper.convertValue(entry.getValue(), Map.class);
beans.put(entry.getKey(), beans.put(entry.getKey(), sanitize(value));
sanitize(m.convertValue(entry.getValue(), Map.class)));
} }
return beans; return beans;
...@@ -94,7 +94,7 @@ public class ConfigurationPropertiesReportEndpoint extends ...@@ -94,7 +94,7 @@ public class ConfigurationPropertiesReportEndpoint extends
private Object sanitize(String name, Object object) { private Object sanitize(String name, Object object) {
for (String keyToSanitize : this.keysToSanitize) { for (String keyToSanitize : this.keysToSanitize) {
if (name.toLowerCase().endsWith(keyToSanitize)) { if (name.toLowerCase().endsWith(keyToSanitize)) {
return object == null ? null : "******"; return (object == null ? null : "******");
} }
} }
return object; return object;
......
...@@ -26,6 +26,7 @@ import javax.annotation.PostConstruct; ...@@ -26,6 +26,7 @@ import javax.annotation.PostConstruct;
import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
...@@ -39,6 +40,8 @@ import com.fasterxml.jackson.databind.Module; ...@@ -39,6 +40,8 @@ import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
/** /**
* {@link EnableAutoConfiguration Auto-configuration} for {@link HttpMessageConverter}s.
*
* @author Dave Syer * @author Dave Syer
*/ */
@Configuration @Configuration
......
...@@ -39,6 +39,8 @@ import static org.junit.Assert.assertTrue; ...@@ -39,6 +39,8 @@ import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
/** /**
* Tests for {@link HttpMessageConverters}.
*
* @author Dave Syer * @author Dave Syer
* @author Phillip Webb * @author Phillip Webb
*/ */
......
...@@ -35,8 +35,6 @@ import org.eclipse.aether.repository.RemoteRepository; ...@@ -35,8 +35,6 @@ import org.eclipse.aether.repository.RemoteRepository;
/** /**
* (Copied from aether source code - not available yet in Maven repo.) * (Copied from aether source code - not available yet in Maven repo.)
*
* @author dsyer
*/ */
public final class JreProxySelector implements ProxySelector { public final class JreProxySelector implements ProxySelector {
......
# Spring Boot Actuator Sample # Spring Boot Actuator Sample
You can build this sample using Maven (>3) or Gradle (1.6).
You can build this sample using Maven (>3) or Gradle (1.6).
With Maven: With Maven:
...@@ -9,7 +8,8 @@ $ mvn package ...@@ -9,7 +8,8 @@ $ mvn package
$ java -jar target/*.jar $ java -jar target/*.jar
``` ```
Then access the app via a browser (or curl) on http://localhost:8080 (the user name is "user" and look at the INFO log output for the password to login). Then access the app via a browser (or curl) on http://localhost:8080 (the user name is
"user" and look at the INFO log output for the password to login).
With gradle: With gradle:
...@@ -18,4 +18,6 @@ $ gradle build ...@@ -18,4 +18,6 @@ $ gradle build
$ java -jar build/libs/*.jar $ java -jar build/libs/*.jar
``` ```
The gradle build contains an intentionally odd configuration to exclude the security dependencies from the executable JAR. So the app run like this behaves differently than the one run from the Maven-built JAR file. See comments in the `build.gradle` for details. The gradle build contains an intentionally odd configuration to exclude the security
\ No newline at end of file dependencies from the executable JAR. So the app run like this behaves differently than
the one run from the Maven-built JAR file. See comments in the `build.gradle` for details.
...@@ -16,8 +16,6 @@ ...@@ -16,8 +16,6 @@
package org.springframework.boot.sample.actuator.log4j; package org.springframework.boot.sample.actuator.log4j;
import static org.junit.Assert.assertEquals;
import java.io.IOException; import java.io.IOException;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
...@@ -36,6 +34,8 @@ import org.springframework.http.client.ClientHttpResponse; ...@@ -36,6 +34,8 @@ import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.DefaultResponseErrorHandler; import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import static org.junit.Assert.assertEquals;
/** /**
* Basic integration tests for service demo application. * Basic integration tests for service demo application.
* *
...@@ -77,7 +77,6 @@ public class SampleActuatorApplicationTests { ...@@ -77,7 +77,6 @@ public class SampleActuatorApplicationTests {
assertEquals("Hello Phil", body.get("message")); assertEquals("Hello Phil", body.get("message"));
} }
private RestTemplate getRestTemplate() { private RestTemplate getRestTemplate() {
RestTemplate restTemplate = new RestTemplate(); RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new DefaultResponseErrorHandler() { restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
......
...@@ -27,7 +27,7 @@ interface ReviewRepository extends Repository<Review, Long> { ...@@ -27,7 +27,7 @@ interface ReviewRepository extends Repository<Review, Long> {
Page<Review> findByHotel(Hotel hotel, Pageable pageable); Page<Review> findByHotel(Hotel hotel, Pageable pageable);
Review findByHotelAndIndex(Hotel hotel, int index); Review findByHotelAndIndex(Hotel hotel, int index);
Review save(Review review); Review save(Review review);
} }
...@@ -70,7 +70,7 @@ is intended to be run as it is you need to have all dependencies in it, however ...@@ -70,7 +70,7 @@ is intended to be run as it is you need to have all dependencies in it, however
if a plan is to explode a jar file and run main class manually you may already if a plan is to explode a jar file and run main class manually you may already
have some of the libraries available via `CLASSPATH`. This is a situation where have some of the libraries available via `CLASSPATH`. This is a situation where
you can repackage boot jar with a different set of dependencies. Using a custom you can repackage boot jar with a different set of dependencies. Using a custom
configuration will automatically disable dependency resolving from configuration will automatically disable dependency resolving from
`compile`, `runtime` and `provided` scopes. Custom configuration can be either `compile`, `runtime` and `provided` scopes. Custom configuration can be either
defined globally inside `springBoot` or per task. defined globally inside `springBoot` or per task.
...@@ -131,7 +131,7 @@ sources jars are automatically skipped). ...@@ -131,7 +131,7 @@ sources jars are automatically skipped).
Because on default every repackage task execution will find all Because on default every repackage task execution will find all
created jar artifacts, the order of Gradle task execution is created jar artifacts, the order of Gradle task execution is
important. This is not going to be an issue if you have a normal important. This is not going to be an issue if you have a normal
project setup where only one jar file is created. However if you are project setup where only one jar file is created. However if you are
planning to create more complex project setup with custom Jar and planning to create more complex project setup with custom Jar and
BootRepackage tasks, there are few tweaks to consider. BootRepackage tasks, there are few tweaks to consider.
...@@ -149,7 +149,7 @@ out from your project. You could also just disable default ...@@ -149,7 +149,7 @@ out from your project. You could also just disable default
bootRepackage.withJarTask = jar bootRepackage.withJarTask = jar
``` ```
Above example simply instructs default `bootRepackage` task to only Above example simply instructs default `bootRepackage` task to only
work with a default `jar` task. work with a default `jar` task.
```groovy ```groovy
...@@ -165,6 +165,6 @@ create dependency to your build so that `bootJars` task would ...@@ -165,6 +165,6 @@ create dependency to your build so that `bootJars` task would
be run after the default `bootRepackage` task is executed. be run after the default `bootRepackage` task is executed.
All the above tweaks are usually used to avoid situation where All the above tweaks are usually used to avoid situation where
already created boot jar is repackaged again. Repackaging already created boot jar is repackaged again. Repackaging
an existing boot jar will not break anything but you may an existing boot jar will not break anything but you may
get unnecessary dependencies in it. get unnecessary dependencies in it.
...@@ -68,14 +68,13 @@ public class PropertiesConfigurationFactoryTests { ...@@ -68,14 +68,13 @@ public class PropertiesConfigurationFactoryTests {
assertEquals("blah", foo.name); assertEquals("blah", foo.name);
} }
@Test @Test
public void testUnderscore() throws Exception { public void testUnderscore() throws Exception {
Foo foo = createFoo("spring_foo_baz: blah\nname: blah"); Foo foo = createFoo("spring_foo_baz: blah\nname: blah");
assertEquals("blah", foo.spring_foo_baz); assertEquals("blah", foo.spring_foo_baz);
assertEquals("blah", foo.name); assertEquals("blah", foo.name);
} }
@Test @Test
public void testUnknownPropertyOkByDefault() throws Exception { public void testUnknownPropertyOkByDefault() throws Exception {
Foo foo = createFoo("hi: hello\nname: foo\nbar: blah"); Foo foo = createFoo("hi: hello\nname: foo\nbar: blah");
...@@ -137,11 +136,11 @@ public class PropertiesConfigurationFactoryTests { ...@@ -137,11 +136,11 @@ public class PropertiesConfigurationFactoryTests {
private String name; private String name;
private String bar; private String bar;
private String spring_foo_baz; private String spring_foo_baz;
public String getSpringFooBaz() { public String getSpringFooBaz() {
return spring_foo_baz; return this.spring_foo_baz;
} }
public void setSpringFooBaz(String spring_foo_baz) { public void setSpringFooBaz(String spring_foo_baz) {
......
...@@ -28,9 +28,14 @@ import org.springframework.context.ConfigurableApplicationContext; ...@@ -28,9 +28,14 @@ import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfiguration;
/** /**
* Class-level annotation that is used to determine how to load and configure an
* ApplicationContext for integration tests. Similar to the standard
* {@link ContextConfiguration} but uses Spring Boot's
* {@link SpringApplicationContextLoader}.
*
* @author Dave Syer * @author Dave Syer
* @see SpringApplicationContextLoader
*/ */
@ContextConfiguration(loader = SpringApplicationContextLoader.class) @ContextConfiguration(loader = SpringApplicationContextLoader.class)
@Documented @Documented
@Inherited @Inherited
......
...@@ -58,27 +58,15 @@ public class SpringApplicationContextLoader extends AbstractContextLoader { ...@@ -58,27 +58,15 @@ public class SpringApplicationContextLoader extends AbstractContextLoader {
public ApplicationContext loadContext(MergedContextConfiguration mergedConfig) public ApplicationContext loadContext(MergedContextConfiguration mergedConfig)
throws Exception { throws Exception {
Set<Object> sources = new LinkedHashSet<Object>();
sources.addAll(Arrays.asList(mergedConfig.getClasses()));
sources.addAll(Arrays.asList(mergedConfig.getLocations()));
SpringApplication application = new SpringApplication(); SpringApplication application = new SpringApplication();
application.setSources(sources); application.setSources(getSources(mergedConfig));
Map<String, Object> args = new LinkedHashMap<String, Object>();
if (!ObjectUtils.isEmpty(mergedConfig.getActiveProfiles())) { if (!ObjectUtils.isEmpty(mergedConfig.getActiveProfiles())) {
application.setAdditionalProfiles(Arrays.asList(mergedConfig application.setAdditionalProfiles(Arrays.asList(mergedConfig
.getActiveProfiles())); .getActiveProfiles()));
} }
// Not running an embedded server, just setting up web context application.setDefaultProperties(getArgs(mergedConfig));
args.put("server.port", "0"); List<ApplicationContextInitializer<?>> initializers = getInitializers(
args.put("management.port", "0"); mergedConfig, application);
application.setDefaultProperties(args);
List<ApplicationContextInitializer<?>> initializers = new ArrayList<ApplicationContextInitializer<?>>(
application.getInitializers());
for (Class<? extends ApplicationContextInitializer<?>> type : mergedConfig
.getContextInitializerClasses()) {
initializers.add(BeanUtils.instantiate(type));
}
if (mergedConfig instanceof WebMergedContextConfiguration) { if (mergedConfig instanceof WebMergedContextConfiguration) {
new WebConfigurer().setup(mergedConfig, application, initializers); new WebConfigurer().setup(mergedConfig, application, initializers);
} }
...@@ -86,9 +74,36 @@ public class SpringApplicationContextLoader extends AbstractContextLoader { ...@@ -86,9 +74,36 @@ public class SpringApplicationContextLoader extends AbstractContextLoader {
application.setWebEnvironment(false); application.setWebEnvironment(false);
} }
application.setInitializers(initializers); application.setInitializers(initializers);
return application.run(); return application.run();
} }
private Set<Object> getSources(MergedContextConfiguration mergedConfig) {
Set<Object> sources = new LinkedHashSet<Object>();
sources.addAll(Arrays.asList(mergedConfig.getClasses()));
sources.addAll(Arrays.asList(mergedConfig.getLocations()));
return sources;
}
private Map<String, Object> getArgs(MergedContextConfiguration mergedConfig) {
Map<String, Object> args = new LinkedHashMap<String, Object>();
// Not running an embedded server, just setting up web context
args.put("server.port", "0");
args.put("management.port", "0");
return args;
}
private List<ApplicationContextInitializer<?>> getInitializers(
MergedContextConfiguration mergedConfig, SpringApplication application) {
List<ApplicationContextInitializer<?>> initializers = new ArrayList<ApplicationContextInitializer<?>>();
initializers.addAll(application.getInitializers());
for (Class<? extends ApplicationContextInitializer<?>> initializerClass : mergedConfig
.getContextInitializerClasses()) {
initializers.add(BeanUtils.instantiate(initializerClass));
}
return initializers;
}
@Override @Override
public ApplicationContext loadContext(String... locations) throws Exception { public ApplicationContext loadContext(String... locations) throws Exception {
throw new UnsupportedOperationException( throw new UnsupportedOperationException(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment