Skip to content

devon4j deployment

devonfw-core edited this page Dec 16, 2021 · 20 revisions

Deployment of devon4j Applications

As mentioned already in the devon4j project section, apart from the core and api project, apps created with devon4j also provide a server project that configures the packaging of the app.

In our JumpTheQueue app we can verify that this server project is available:

JumpTheQueue Server Structure

So — using Maven — we are going to be able to easily package our app in a .war file to be deployed in an application server like Tomcat (the default server provided in devonfw).

The server Project

The server project provided in devon4j applications is an almost empty Maven project. It only has a pom.xml file that is used to configure the packaging of the core project. Taking a closer look at this pom.xml file, we realize that it only contains a single dependency to the core project:

...

  <dependencies>
    <dependency>
      <groupId>${project.groupId}</groupId>
      <artifactId>jtqj-core</artifactId>
      <version>${project.version}</version>
    </dependency>
  </dependencies>

...

It also includes the Spring Boot Maven Plugin, that allows us to package the project in .jar or .war archives and run the application "in-place":

...

    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        ...

      </plugin>
    </plugins>

...

Disabling Security Tests

Since this is a basic tutorial and there is no security or permission handling, we have to modify the files SecurityRestServiceImplTest and PermissionCheckTest in jtqj-core, to disable the tests for these features. This is done by adding the @Disabled annotation to the affected test methods.

Your Eclipse should automatically add the required dependencies for the annotation once you save the files, so they should contain the same import statements as shown below. More specifically org.junit.jupiter will be replacing the older org.junit test framework, which was used before.

First, in src/test/java/…​/general/service/impl/rest/SecurityRestServiceImplTest.java disable the testLogin() method and make sure that the class is annotated with @ExtendWith(SpringExtension.class), instead of the older @RunWith(SpringRunner.class) annotation:

ℹ️

If this class doesn’t exist, go to the next one

package com.devonfw.application.jtqj.general.service.impl.rest;

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.web.client.RestTemplate;

import com.devonfw.application.jtqj.general.common.api.to.UserProfileTo;
import com.devonfw.application.jtqj.general.service.api.rest.SecurityRestService;
import com.devonfw.application.jtqj.general.service.base.test.RestServiceTest;
import com.devonfw.module.service.common.api.client.config.ServiceClientConfigBuilder;

/**
 * This class tests the login functionality of {@link SecurityRestServiceImpl}.
 */
@ExtendWith(SpringExtension.class)
public class SecurityRestServiceImplTest extends RestServiceTest {

  /** Logger instance. */
  private static final Logger LOG = LoggerFactory.getLogger(SecurityRestServiceImplTest.class);

  /**
   * Test the login functionality as it will be used from a JavaScript client.
   */
  @Test
  @Disabled // Security via Login is currently not implemented, so ignore this test
  public void testLogin() {

    String login = "waiter";
    String password = "waiter";

    ResponseEntity<String> postResponse = login(login, password);
    LOG.debug("Body: " + postResponse.getBody());
    assertThat(postResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
    assertThat(postResponse.getHeaders().containsKey(HttpHeaders.SET_COOKIE)).isTrue();
  }

  /**
   * Test of {@code SecurityRestService.getCsrfToken()}.
   */
  @Test
  public void testGetCsrfToken() {

    String login = "waiter";
    String password = "waiter";
    SecurityRestService securityService = getServiceClientFactory().create(SecurityRestService.class,
        new ServiceClientConfigBuilder().host("localhost").authBasic().userLogin(login).userPassword(password)
            .buildMap());
    CsrfToken csrfToken = securityService.getCsrfToken(null, null);
    assertThat(csrfToken.getHeaderName()).isEqualTo("X-CSRF-TOKEN");
    assertThat(csrfToken.getParameterName()).isEqualTo("_csrf");
    assertThat(csrfToken.getToken()).isNotNull();
    LOG.debug("Csrf Token: {}", csrfToken.getToken());
  }

  /**
   * Test of {@link SecurityRestService#getCurrentUser()}.
   */
  @Test
  public void testGetCurrentUser() {

    String login = "waiter";
    String password = "waiter";
    SecurityRestService securityService = getServiceClientFactory().create(SecurityRestService.class,
        new ServiceClientConfigBuilder().host("localhost").authBasic().userLogin(login).userPassword(password)
            .buildMap());
    UserProfileTo userProfile = securityService.getCurrentUser();
    assertThat(userProfile.getLogin()).isEqualTo(login);
  }

  /**
   * Performs the login as required by a JavaScript client.
   *
   * @param userName the username of the user
   * @param tmpPassword the password of the user
   * @return @ {@link ResponseEntity} containing containing a cookie in its header.
   */
  private ResponseEntity<String> login(String userName, String tmpPassword) {

    String tmpUrl = "http://localhost:" + String.valueOf(this.port) + "/services/rest/login";

    HttpEntity<String> postRequest = new HttpEntity<>(
        "{\"j_username\": \"" + userName + "\", \"j_password\": \"" + tmpPassword + "\"}", new HttpHeaders());

    ResponseEntity<String> postResponse = new RestTemplate().exchange(tmpUrl, HttpMethod.POST, postRequest,
        String.class);
    return postResponse;
  }
}

And in src/test/java/…​/general/common/base/PermissionCheckTest.java just disable the permissionCheckAnnotationPresent() method:

package com.devonfw.application.jtqj.general.common.base;

import java.lang.reflect.Method;
import java.util.Set;

import javax.annotation.security.DenyAll;
import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;

import net.sf.mmm.util.filter.api.Filter;
import net.sf.mmm.util.reflect.api.ReflectionUtil;
import net.sf.mmm.util.reflect.base.ReflectionUtilImpl;

import org.assertj.core.api.SoftAssertions;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

import com.devonfw.module.test.common.base.ModuleTest;

/**
 * Tests the permission check in logic layer.
 */
public class PermissionCheckTest extends ModuleTest {

  /**
   * Check if all relevant methods in use case implementations have permission checks i.e. {@link RolesAllowed},
   * {@link DenyAll} or {@link PermitAll} annotation is applied. This is only checked for methods that are declared in
   * the corresponding interface and thus have the {@link Override} annotations applied.
   */
  @Test
  @Disabled // Permission Checks are currently not implemented, so ignore this test
  public void permissionCheckAnnotationPresent() {

    String packageName = "com.devonfw.application.jtqj";
    Filter<String> filter = new Filter<String>() {

      @Override
      public boolean accept(String value) {

        return value.contains(".logic.impl.usecase.Uc") && value.endsWith("Impl");
      }

    };
    ReflectionUtil ru = ReflectionUtilImpl.getInstance();
    Set<String> classNames = ru.findClassNames(packageName, true, filter);
    Set<Class<?>> classes = ru.loadClasses(classNames);
    SoftAssertions assertions = new SoftAssertions();
    for (Class<?> clazz : classes) {
      Method[] methods = clazz.getDeclaredMethods();
      for (Method method : methods) {
        Method parentMethod = ru.getParentMethod(method);
        if (parentMethod != null) {
          Class<?> declaringClass = parentMethod.getDeclaringClass();
          if (declaringClass.isInterface() && declaringClass.getSimpleName().startsWith("Uc")) {
            boolean hasAnnotation = false;
            if (method.getAnnotation(RolesAllowed.class) != null || method.getAnnotation(DenyAll.class) != null
                || method.getAnnotation(PermitAll.class) != null) {
              hasAnnotation = true;
            }
            assertions.assertThat(hasAnnotation)
                .as("Method " + method.getName() + " in Class " + clazz.getSimpleName() + " is missing access control")
                .isTrue();
          }
        }
      }
    }
    assertions.assertAll();
  }
}

This is going to allow our application to pass the tests and be built.

Running the App with Maven

Thanks to Spring Boot and the Spring Boot Maven Plugin, we can run our app using Maven. To do so, just open a command prompt with access to Maven (in our devonfw project folder we can simply do so by right clicking and selecting Open Devon CMD shell here).

Now we need to follow these steps:

1.- As is explained in the devon4j configuration guide, the default application.properties file used for packaging is located in src/main/resources/ (don’t use the one located in src/main/resources/config/). We need to modify some settings in this file in order to gain access to the app:

server.port=8081

spring.application.name=jtqj
server.servlet.context-path=/jumpthequeue

2.- Install the jtqj project in our local Maven repository:

C:\...\workspaces\main\jumpthequeue\java\jtqj> mvn install

3.- Go to the jtqj/server project and boot the application:

C:\...\workspaces\main\jumpthequeue\java\jtqj\server> mvn spring-boot:run

The app should be launched in the Spring Boot embedded Tomcat server. Wait a few seconds until you see a console message like this:

{"timestamp":"20XX-XX-XXTXX:XX:XX.XXX+00:00","message":"Tomcat started on port(s): 8081 (http) with context path '/jumpthequeue'","logger_name":"org.springframework.boot.web.embedded.tomcat.TomcatWebServer","thread_name":"main","level":"INFO","appname":"jtqj"}
{"timestamp":"20XX-XX-XXTXX:XX:XX.XXX+00:00","message":"Started SpringBootApp in XX.XXX seconds (JVM running for XX.XXX)","logger_name":"com.devonfw.application.jtqj.SpringBootApp","thread_name":"main","level":"INFO","appname":"jtqj"}

Now we can try to access the app resource.

JumpTheQueue Simple GET Request

If you get a response similar to the one in the image, you have verified that the app is running fine.

Packaging the App with Maven

In the same way, using Maven we can package our project in a .war file. As in the previous section, open a command prompt with access to Maven (in our devonfw project folder we can simply do so by right clicking and selecting Open Devon CMD shell here). Now execute the following command in the projects root directory:

C:\...\workspaces\main\jumpthequeue\java\jtqj> mvn clean package

The packaging process (which includes compilation, tests and generation of the .war file) will be launched. Once the process is finished you should see a result like this:

[INFO] Packaging webapp
[INFO] Assembling webapp [jtqj-server] in [C:\...\workspaces\main\jump-the-queue\jump-the-queue\java\jtqj\server\target\jtqj-server-v4]
[INFO] Processing war project
[INFO] Webapp assembled in [XXXX msecs]
[INFO] Building war: C:\...\workspaces\main\jump-the-queue\jump-the-queue\java\jtqj\server\target\jtqj-server-v4.war
[INFO]
[INFO] --- spring-boot-maven-plugin:2.1.6.RELEASE:repackage (default) @ jtqj-server ---
[INFO] Attaching repackaged archive C:\...\workspaces\main\jump-the-queue\jump-the-queue\java\jtqj\server\target\jtqj-server-bootified.war with classifier bootified
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary for jtqj v4:
[INFO]
[INFO] jtqj ............................................... SUCCESS [  X.XXX s]
[INFO] jtqj-api ........................................... SUCCESS [ XX.XXX s]
[INFO] jtqj-core .......................................... SUCCESS [XX:XX min]
[INFO] jtqj-server ........................................ SUCCESS [ XX.XXX s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  XX:XX min
[INFO] Finished at: 20XX-XX-XXTXX:XX:XX+0X:00
[INFO] ------------------------------------------------------------------------

The packaging process creates two .war files, that are stored in the \java\jtqj\server\target directory. They contain the web application and can be deployed on any Servlet/JSP container.


Next Chapter: devon4ng Introduction