Spring framework provides mechanism for binding controller method parameters to custom model. Here is a very good article on how to implement custom data binding https://www.baeldung.com/spring-mvc-custom-data-binder During data binding there might be a need to access HTTP request body.
Solution
HTTP request body is hidden in the ModelAndViewContainer object that spring provides to the argument resolver. Here is a code example:
All details about setting up method argument resolver are omitted and the focus is on the toContext function that demonstrates how to get request body from ModelAndViewContainer.
Interesting part of this response is REJECTED_THREAD_EXECUTION. None of the services were reporting this message. After looking deeper, the Zuul log showed this error
Caused by: java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@3b17817e rejected from java.util.concurrent.ThreadPoolExecutor@67f89c35[Running, pool size = 10, active threads = 10, queued tasks = 0, completed tasks = 2535368]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063) ~[?:1.8.0_212]
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830) ~[?:1.8.0_212]
Solution
After realizing that exception is in the zuul log it was a matter of finding the right properties to configure services thread pools. By default, zuul was using 10 threads with the max thread count set to 10. To change this default across all services set these properties:
Suppose you are working on multiple Spring Boot/Spring Cloud projects that are using the spring cloud config server. You setup property encryption for the spring-cloud config. The encryption key is sored in your local profile as an environment variable ENCRYPT_KEY. Then, you switch to work on another spring-boot/spring-cloud project that does not use spring config server and have spring-cloud-context as a dependency. When you run this project you get the following exception stack trace:
Caused by: java.lang.NullPointerException: null
at org.springframework.cloud.context.encrypt.EncryptorFactory.create(EncryptorFactory.java:54)
at org.springframework.cloud.bootstrap.encrypt.EncryptionBootstrapConfiguration$RsaEncryptionConfiguration.textEncryptor(EncryptionBootstrapConfiguration.java:83)
at org.springframework.cloud.bootstrap.encrypt.EncryptionBootstrapConfiguration$RsaEncryptionConfiguration$$EnhancerBySpringCGLIB$$c1a972bf.CGLIB$textEncryptor$0(<generated>)
at org.springframework.cloud.bootstrap.encrypt.EncryptionBootstrapConfiguration$RsaEncryptionConfiguration$$EnhancerBySpringCGLIB$$c1a972bf$$FastClassBySpringCGLIB$$4d33eb7b.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:244)
at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:363)
at org.springframework.cloud.bootstrap.encrypt.EncryptionBootstrapConfiguration$RsaEncryptionConfiguration$$EnhancerBySpringCGLIB$$c1a972bf.textEncryptor(<generated>)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:154)
... 31 common frames omitted
Diagnostic
This is actually a spring bug! Another way to provide the encryption key to the config server is to use -Dencrypt.key=123 system property. If you do it this way everything is working fine. The problem only arises when the environment variable ENCRYPT_KEY is used. The root cause is in:
This condition bean determines whether spring will initialize TextEncryptor class or not. When spring detects ENCRYPT_KEY environment variable the TextEncryptor class gets created. It uses KeyProperties to initialize encryptor. If -Dencrypt.key=123 is provided then KeyProperties is initialized with the key provided as a system property. If ENCRYPT_KEY environment variable is found KeyProperties are not initialized and the key member is null.
Solution
Remove ENCRYPT_KEY property from the environment prior to running spring boot application. The problem is fixed.
Provide a library that can serve swagger UI in Jersey application. Inspiration for this library was the SpringFox Swagger UI project that works with spring boot application.
Use Case
Suppose you have a runnable Jar Jersey REST API application with Open API annotations and would like to provide a swagger UI for API consumers to learn and play with your APIs. Swagger UI comes as a set of static Javascript files that need to be included in your application. In addition, you need to configure a web context that will serve swagger UI pages. By including itzap-jerseyswagger.jarin your project you will get an endpoint that will load functional Swagger UI with your API definitions.
Implementation Details
Here is the code that starts embedded Tomcat in the project that I described in my previous post itzap-message. I modified it to use Open API annotations and include swagger UI.
Immutable POJOs are key to bug-free programs. It is not straight forward to write immutable POJOs in Java. Builder pattern comes handy to solve this problem by separating getters and setters. The problem with the Builder pattern in java is the need to duplicate POJO properties. The purpose of this solution is to minimize the downside of the Builder pattern in java by separating the state into a separate class.
Use Case
Suppose we have a User POJO like this:
public class User {
private final String lastName;
private final String firstName;
private User(Builder builder) {
this.lastName = builder.lastName;
this.firstName = builder.firstName;
}
public String getLastName() {
return this.lastName;
}
public String getFirstName() {
return this.firstName;
}
public static class Builder {
private String lastName;
private String firstName;
public Builder setLastName(String lastName) {
this.lastName = lastName;
return this;
}
public Builder setFirstName(String firstName) {
this.firstName = firstName;
return this;
}
public User build() {
return new User(this);
}
}
public static Builder builder() {
return new Builder();
}
}
The problem with the above solution is that lastName and firstName attributes need to be repeated for both builder and POJO itself.
Solution
To address this issue we can try to separate attributes into a separate State class.
public class User {
private static class State {
private String lastName;
private String firstName;
}
private final State state;
private User(Builder builder) {
this.state = builder.state;
}
public String getLastName() {
return this.state.lastName;
}
public String getFirstName() {
return this.state.firstName;
}
public static class Builder {
private final State state = new State();
public Builder setLastName(String lastName) {
this.state.lastName = lastName;
return this;
}
public Builder setFirstName(String firstName) {
this.state.firstName = firstName;
return this;
}
public User build() {
return new User(this);
}
public static Builder from(User user) {
return User.builder()
.setLastName(user.getLastName())
.setFirstName(user.getFirstName());
}
}
public static Builder builder() {
return new Builder();
}
}
Bonus for IntelliJ Users
IntelliJ’s generate Getter/Setter feature cannot be used effectively when attributes of the POJO are factored out into a separate State. Luckily, IntelliJ provides the ability to customize generators. Here is an example of a possible custom Getter/Setter generator scripts.
Navigate to the State class and generate Getters/Setters. Then copy getters to the POJO and setters to the builder. A small extra step but save a lot of boilerplate code.
NoSQL databases are dominating enterprise systems. One reason is that they do not require precise data schema and can deal with new data without triggering massive changes. The idea is to achieve schema flexibility with the typical SQL database like PostgreSQL or MySql. In general, NoSQL databases require some key/index/primary fields to enable fast lookups. If we preserve the concept of necessary fields and store the rest of the document in the JSON text field we can accomplish similar flexibility with the plain old SQL database.
Use Case
Imagine some users table where fields like username and email are required to identify a user, but user attributes like phone, lastName, firstName can be just stored as part of the user record.
Implementation Details
Here is the database schema for the above use case using H2 Java in-memory database:
drop table if exists users;
create table users
(
id bigint NOT NULL auto_increment,
username varchar(50) not null,
password varchar(255) not null,
enabled boolean not null,
email varchar(255) not null,
object varchar(4048) NOT NULL DEFAULT '{}',
CONSTRAINT pk_users PRIMARY KEY (id)
);
In this example, application key fields defined as typical SQL schema and the rest of the document is stored in the object field. Now, all we need to do is merge key fields into a final object. I used ebean ORM library to implement database connectivity and statement execution and command/reactive design pattern explained in my RxJava post to implement DAO layer.
Model
itzap-ebeans project implements this idea and contains an example UserDao implementation. Simple user model looks like this:
@JsonDeserialize(builder = User.Builder.class)
public class User extends Auditable {
private final String lastName;
private final String firstName;
private final String email;
private final String phone;
@JsonProperty("enabled")
private final Boolean enabled;
private final String username;
@JsonIgnore
private final String password;
public User(Builder builder) {
super(builder);
this.lastName = builder.lastName;
this.firstName = builder.firstName;
this.email = builder.email;
this.phone = builder.phone;
this.enabled = builder.enabled;
this.username = builder.username;
this.password = builder.password;
}
public String getLastName() {
return lastName;
}
public String getFirstName() {
return firstName;
}
public String getEmail() {
return email;
}
public String getPhone() {
return phone;
}
public Boolean getEnabled() {
return enabled;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this.getClass())
.add("lastName", lastName)
.add("email", email)
.add("username", username)
.toString();
}
@JsonPOJOBuilder(withPrefix = "set")
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Builder extends Auditable.Builder<User, User.Builder> {
private String lastName;
private String firstName;
private String email;
private String phone;
private Boolean enabled;
private String username;
private String password;
public Builder() {
}
@Override
protected Builder getThis() {
return this;
}
public Builder setLastName(String lastName) {
this.lastName = lastName;
return this;
}
public Builder setFirstName(String firstName) {
this.firstName = firstName;
return this;
}
public Builder setEmail(String email) {
this.email = email;
return this;
}
public Builder setPhone(String phone) {
this.phone = phone;
return this;
}
public Builder setEnabled(Boolean enabled) {
this.enabled = enabled;
return this;
}
public Builder setUsername(String username) {
this.username = username;
return this;
}
public Builder setPassword(String password) {
this.password = password;
return this;
}
@Override
public Builder merge(User org) {
Builder bld = super.merge(org)
.setEmail(StringUtils.defaultIfBlank(this.email, org.getEmail()))
.setEnabled(ObjectUtils.defaultIfNull(this.enabled, org.getEnabled()))
.setLastName(StringUtils.defaultIfBlank(this.lastName, org.getLastName()))
.setUsername(StringUtils.defaultIfBlank(this.username, org.getUsername()))
.setFirstName(StringUtils.defaultIfBlank(this.firstName, org.getFirstName()))
.setPassword(StringUtils.defaultIfBlank(this.password, org.getPassword()))
.setPhone(StringUtils.defaultIfBlank(this.phone, org.getPhone()));
return bld;
}
@Override
public User build() {
return new User(this);
}
}
public static Builder from(User user) {
Builder builder = new Builder();
BeanUtils.copyProperties(user, builder);
return builder;
}
public static Builder builder() {
return new Builder();
}
}
Note all the Json annotations that enable serialization/deserialization of the model and merge method that provides rules for merging database and Json objects.
Dao Implementation
Dao layer implemented using two base commands: ListBaseEbeanCommand and BaseBulkEbeanCommand. ListBaseEbeanCommand is the base command for all select operations and BaseBulkEbeanCommand is the base command for all Insert/Update/Delete operations. Base commands operate using EbeanHandler and report exceptions using EbeanErrorHandler. Complete DAO implementation looks like this:
public class UserDao extends AbstractServiceDao {
private static final String SELECT_ALL_USERS = "select * from users";
private static final String WHERE_USER = " where username=:username";
private static final String WHERE_USER_ID = " where id=:id";
private static final String ADD_USER = "INSERT INTO public.users(" +
" username, password, enabled, email, object)" +
" VALUES (:username, :password, :enabled, :email, :object)";
private static final String UPDATE_USER = "UPDATE public.users " +
" SET username=:username, password=:password, enabled=:enabled, email=:email, object=:object" +
" WHERE id=:id";
private static final String DELETE_USER = "DELETE FROM users WHERE id=:id";
public UserDao(Servers.Server server) {
super(server);
}
public Observable<User> getUsers() {
return new SelectUsersCommand()
.toObservable()
.flatMap(Observable::fromIterable);
}
public Observable<User> getUserByName(String name) {
return new SelectUsersCommand()
.setName(name)
.toObservable()
.map(users -> users.get(0));
}
public Observable<User> getUser(Long userId) {
return new SelectUsersCommand()
.setUserId(userId)
.toObservable()
.map(users -> users.get(0));
}
public Observable<Integer> addUser(User user) {
return new BaseBulkEbeanCommand<BaseBulkEbeanCommand>("cmd-addUser-cmd",
new UserHandler(user)) {
@Override
protected BaseBulkEbeanCommand getThis() {
return this;
}
@Override
protected SqlUpdate createSql() {
return server.getServer().createSqlUpdate(ADD_USER);
}
}.toObservable();
}
public Observable<Integer> updateUser(Long userId, User user) {
return new SelectUsersCommand()
.setName(user.getUsername())
.toObservable()
.flatMap(orgUser -> new BaseBulkEbeanCommand<BaseBulkEbeanCommand>("cmd-updateUser-cmd",
new UserHandler(userId, user, orgUser.get(0))) {
@Override
protected BaseBulkEbeanCommand getThis() {
return this;
}
@Override
protected SqlUpdate createSql() {
return server.getServer().createSqlUpdate(UPDATE_USER);
}
}.toObservable());
}
public Observable<Integer> deleteUser(Long id) {
return new BaseBulkEbeanCommand<BaseBulkEbeanCommand>("cmd-deleteUser-cmd",
new DeleteEbeanHandler<>(QueryParameter.id(id))) {
@Override
protected BaseBulkEbeanCommand getThis() {
return this;
}
@Override
protected SqlUpdate createSql() {
return server.getServer().createSqlUpdate(DELETE_USER);
}
}.toObservable();
}
class UserHandler extends AbstractAuditableHandler<User, User.Builder> {
UserHandler(User user) {
this(null, user, User.builder().build());
}
UserHandler(Long userId, User user, User dbUser) {
super(userId, user, dbUser);
}
@Override
protected User.Builder rowProperties(SqlUpdate updateQuery, User user) {
updateQuery.setParameter("username",
StringUtils.defaultIfBlank(user.getUsername(), orgObject.getUsername()));
String pass = getPassword(user);
if (StringUtils.isBlank(pass)) {
updateQuery.setParameter("password", orgObject.getPassword());
} else {
updateQuery.setParameter("password", pass);
}
updateQuery.setParameter("enabled", ObjectUtils.defaultIfNull(user.getEnabled(),
orgObject.getEnabled()));
updateQuery.setParameter("email",
StringUtils.defaultIfBlank(user.getEmail(), orgObject.getEmail()));
return audibalBuilder(user, User.from(user));
}
}
private static String getPassword(User user) {
return user.getPassword();
}
private static User.Builder fromRow(SqlRow row, User user) {
return User.from(user)
.setUsername(row.getString("username"))
.setEmail(row.getString("email"))
.setEnabled(row.getBoolean("enabled"))
.setPassword(row.getString("password"));
}
class SelectUsersCommand extends ListBaseEbeanCommand<User, SelectUsersCommand> {
private String name;
private Long userId;
public SelectUsersCommand setName(String name) {
this.name = name;
return getThis();
}
public SelectUsersCommand setUserId(Long userId) {
this.userId = userId;
return this;
}
SelectUsersCommand() {
super("cmd-get-users",
new AbstractSelectListEbeanHandler<User, User.Builder>(User.class) {
@Override
protected User.Builder from(SqlRow row, User user) {
return fromRow(row, user);
}
});
}
@Override
protected SelectUsersCommand getThis() {
return this;
}
@Override
protected SqlQuery createSql() {
String sql = SELECT_ALL_USERS;
if (StringUtils.isNotBlank(this.name)) {
sql = sql + WHERE_USER;
} else if (this.userId != null) {
sql = sql + WHERE_USER_ID;
}
SqlQuery qry = server.getServer().createSqlQuery(sql);
if (StringUtils.isNotBlank(this.name)) {
qry.setParameter("username", this.name);
} else if (this.userId != null) {
qry.setParameter("id", this.userId);
}
return qry;
}
}
}
Readme
itzap-ebeans
itzap-ebeans provides an easy to use library for building reactive DAO with flexible schema models.
Visit my ITZap blog to read more about this project.
Let’s say you built spring-boot executable jar and uploaded it into nexus repository. To deploy your spring-boot application you create a Dockerfile that downloads your jar using nexus rest search and download API. For example:
FROM openjdk:8-jre-alpine
MAINTAINER itzap <mailer@itzap.com>
WORKDIR /
RUN wget -O app-service.jar \
'https://nexus.companyUrl.com/nexus/service/rest/v1/search/assets/download?group=com.mycompany.service&name=app-service&repository=company-snapshots&sort=version&direction=des'
EXPOSE 8080
ENTRYPOINT [ "java", "-jar","app-service.jar"]
When you run docker container you see the following error message:
no main manifest attribute, in app-service.jar
Diagnostic
When you build and run jar locally everything is working fine. app-service.jar inside docker container appears to be corrupted. To diagnose the issue run the wget command from the terminal:
$ wget -O app-service.jar 'https://nexus.companyUrl.com/nexus/service/rest/v1/search/assets/download?group=com.mycompany.service&name=app-service&repository=company-snapshots&sort=version&direction=desc'
--2019-10-21 21:48:27-- https://nexus.companyUrl.com/nexus/service/rest/v1/search/assets/download?group=com.mycompany.service&name=app-service&repository=company-snapshots&sort=version&direction=desc
Распознаётся nexus.compnyUrl.com (nexus.companyUrl.com)… 00.00.00.00
Подключение к nexus.compnyUrl.com (nexus.compnyUrl.com)|00.00.00.00|:443... соединение установлено.
HTTP-запрос отправлен. Ожидание ответа… 302 Found
Адрес: https://nexus.compnyUrl.com/nexus/repository/company-snapshots/com/mycompany/service/app-service/0.0.1-SNAPSHOT/app-service-0.0.1-20191021.220127-1-javadoc.jar [переход]
--2019-10-21 21:48:28-- https://nexus.compnyUrl.com/nexus/repository/app-snapshots/com/mycompany/service/app-service/0.0.1-SNAPSHOT/app-service-0.0.1-20191021.220127-1-javadoc.jar
Повторное использование соединения с nexus.compnyUrl.com:443.
HTTP-запрос отправлен. Ожидание ответа… 200 OK
Длина: 247463 (242K) [application/java-archive]
Сохранение в: «app-service.jar»
Do not get all these messages in Russian fool you. This is not a Russian hacker attack. Pay attention to what is actually being downloaded. app-service-0.0.1-20191021.220127-1-javadoc.jarjavadoc!!!
Solution
Solution to this issue can be found in sonatype Search API. Looking through documentation I came across this query parameter maven.classifier This issue happens when your repository contains javadoc jar alnog side your artifact. &sort=version&direction=desc parameters are also critical to insure you are downloading the latest version, but not enough to uniquely identify artifact. Final URL that works looks like this:
Let’s say you are building a spring-boot library and would like to tap into spring-boot auto-configuration feature. You create class MyAutoConfiguration and annotate it with the spring @Configuration annotation. Then, in you application, you use this library as a dependency, run the application and get the following exception
Caused by: java.lang.IllegalStateException: Unable to read meta-data for class com.mylib.autoconfigure.MyAutoConfiguration
at org.springframework.boot.autoconfigure.AutoConfigurationSorter$AutoConfigurationClass.getAnnotationMetadata (AutoConfigurationSorter.java:233)
at org.springframework.boot.autoconfigure.AutoConfigurationSorter$AutoConfigurationClass.getOrder (AutoConfigurationSorter.java:204)
at org.springframework.boot.autoconfigure.AutoConfigurationSorter$AutoConfigurationClass.access$000 (AutoConfigurationSorter.java:150)
at org.springframework.boot.autoconfigure.AutoConfigurationSorter.lambda$getInPriorityOrder$0 (AutoConfigurationSorter.java:63)
Steps to Reproduce
Create spring-boot project and add the following class
@Configuration
public class MyAutoConfiguration {
private static final Logger LOGGER = LoggerFactory.getLogger(MyAutoConfiguration.class);
@PostConstruct
public void printConfigurationMessage() {
LOGGER.info("Configuration for My Lib is complete...");
}
}
Do not forget to add spring.factories file to resources -> META-INF -> spring.factories
Finally, you add your library as a dependency to your application, run it and get the runtime error mentioned above.
Diagnostic
Online search suggests that either spring.factories file is missing, or the name of the class is not correct, … Everything is checked out and fine. To make sure MyAutoConfiguration class is in fact included I start looking into my library jar:
jar tvf target/mylib.jar | grep MyAutoConfiguration
Here, I can see that the class is under BOOT-INF/classes root.
Solution
Apparently, auto configuration classes cannot be found under BOOT-INF/classes root. The main issue with my setup was the fact that I used spring-boot-maven-plugin to build the library module. After removing the plugin and rebuilding the library jar tvf target/mylib.jar | grep MyAutoConfiguration is showing this:
com/mylib/autoconfigure/MyAutoConfiguration.class
Now, spring-boot application builds fine and MyAutoConfiguration class is found and ready to be used to configure library.
In a spring-boot application that uses spring-boot-dependencies instead of spring-boot-starter-parent and a third party library that depends on elasticsearch version that is not comparable with version 6.x, elasticsearch dependency version get mixed up creating class loading issue.
Steps to reproduce
Create a spring-boot project and include spring-boot-dependencies in the dependency management section of the maven pom.xml
Turns out mvn dependency:tree shows the issue, but it does not say where org.elasticsearch:elasticsearch:jar:6.4.3:compile is comming from. The best command for the job is:
mvn -Dverbose=true help:effective-pom
This command outputs effective POM that can be redirected to a file mvn -Dverbose=true help:effective-pom > ~/pom.xml and analysed. The effective pom will look something like this
...
<dependency>
<groupId>org.elasticsearch</groupId> <!-- org.springframework.boot:spring-boot-dependencies:2.1.6.RELEASE, line 1965 -->
<artifactId>elasticsearch</artifactId> <!-- org.springframework.boot:spring-boot-dependencies:2.1.6.RELEASE, line 1966 -->
<version>6.4.3</version> <!-- org.springframework.boot:spring-boot-dependencies:2.1.6.RELEASE, line 1967 -->
</dependency>
...
Magic! now we can see exactly where the problem is:
<!-- org.springframework.boot:spring-boot-dependencies:2.1.6.RELEASE, line 1967 -->
spring-boot-dependencies is imported, so there is no easy way to exclude elasticsearch dependency that it pulls into our project. A simple maven <exclude> will not work here. The only way to solve the issue is to bring elasticsearch dependencies into your project dependency management block like this:
Build 7.3.x elasticsearch embedded server into itzap–elasticsearch library to enable easy prototyping and unit/integration testing
How It Works
Elasticsearch embedded server starts a single elasticsearchNode. Elasticsearch server node will be listening for incoming requests on port 9200 Elasticsearch client can be configured to access embedded server using the following url: http://localhost:9200 Elasticsearch embedded server will create data and home temp folders that will be deleted opon application close.
Code
Here is the main class that implements elasticsearch embedded server node
public class EmbeddedElasticSearchServer implements IOData {
private static final Logger LOGGER = LoggerFactory.getLogger(EmbeddedElasticSearchServer.class);
private Node instance;
private int port;
private final AnyConfig config;
public EmbeddedElasticSearchServer(AnyConfig config) {
this.config = config;
}
private static class PluginConfigurableNode extends Node {
PluginConfigurableNode(Settings input,
Map<String, String> properties,
Path configPath,
Supplier<String> defaultNodeName,
Collection<Class<? extends Plugin>> classpathPlugins) {
super(InternalSettingsPreparer.prepareEnvironment(input, properties, configPath, defaultNodeName), classpathPlugins, false);
}
}
@Override
public synchronized Completable start() {
return new RunnableCommand<Void>("cmd-start") {
@Override
protected Void run() {
Settings settings = getSettings();
instance = new PluginConfigurableNode(settings, ImmutableMap.of(),
null, () -> config.getString(EsConfig.CLUSTER_NAME),
singletonList(Netty4Plugin.class));
try {
instance.start();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
if (instance != null) {
instance.close();
}
} catch (IOException e) {
LOGGER.error("Error closing ElasticSearch");
}
}));
LOGGER.info("ElasticSearch cluster {} started in local mode on port {}", instance.settings().get("cluster.name"),
port);
return null;
} catch (NodeValidationException e) {
throw new IZapException("Failed to start embedded elastic search server", e);
}
}
}.toCompletable();
}
@Override
public synchronized Completable stop() {
return new RunnableCommand<Void>("cmd-stop") {
@Override
protected Void run() {
if (instance != null && !instance.isClosed()) {
LOGGER.info("Stopping Elastic Search");
try {
instance.close();
instance = null;
LOGGER.info("Elastic Search on port {} stopped", port);
} catch (IOException e) {
throw new IZapException("Failed to close elastic search embedded server", e);
}
}
return null;
}
}.toCompletable();
}
private Settings getSettings() {
String clusterName = config.getString(EsConfig.CLUSTER_NAME);
String host = config.getString(EsConfig.HOST);
port = config.getInt(EsConfig.PORT);
try {
File dataDir = Files.createTempDirectory(clusterName + "_" + System.currentTimeMillis() + "data").toFile();
FileUtils.forceDeleteOnExit(dataDir);
cleanDataDir(dataDir.getAbsolutePath());
File homeDir = Files.createTempDirectory(clusterName + "_" + System.currentTimeMillis() + "-home").toFile();
cleanDataDir(homeDir.getAbsolutePath());
FileUtils.forceDeleteOnExit(homeDir);
Settings.Builder settingsBuilder = Settings.builder()
.put("cluster.name", clusterName)
.put("http.host", host)
.put("http.port", port)
.put("transport.tcp.port", port + 100)
.put(EsConfig.DATA_PATH.getName(), dataDir.getAbsolutePath())
.put(EsConfig.HOME_PATH.getName(), homeDir.getAbsolutePath())
.put("http.cors.enabled", true)
.put("node.data", true)
.put("http.type", "netty4")
.put("transport.type", "netty4");
return settingsBuilder.build();
} catch (IOException e) {
throw new IZapException("Failed to create temp data/home dir.", e);
}
}
}
Elasticsearch embedded server implementation is using RunnableCommand command pattern to implement start andstopmethods. Please note that RunnableCommand returns Completable and not Observable. Sice Observable<Void> is no longer permitted in RxJava2 and start/stop methods have no return values. Here is the unit test for the elasticsearch embedded server
public class EmbeddedElasticSearchServerTest {
private EmbeddedElasticSearchServer server;
@Before
public void setup() {
AnyConfig config = ConfigBuilder.builder(ConfigType.TYPE_SAFE)
.setFileName(this.getClass()
.getResource("/es-config.properties").getFile())
.build();
server = new EmbeddedElasticSearchServer(config);
}
@Test
public void startTest() {
server.start().blockingGet();
server.stop().blockingGet();
}
}