Skip to content

Latest commit

 

History

History
345 lines (286 loc) · 13.7 KB

README.md

File metadata and controls

345 lines (286 loc) · 13.7 KB

Calcmaster Backend

The Backend Application written with Spring Boot will be used for 2 Aspects:

  • serve the REST-APIs
  • serve the Angular Application as static Files

serving Frontend Application

Dependencies and Thymeleaf Configuration

The Angular Application will be provided as a Webjar. The File index.html has been changed to be a Thymeleaf natural Template.

This is the Reason why you need the Dependency spring-boot-starter-thymeleaf

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
  </dependency>
</dependencies>

The Webjar of the Frontend Application will be included as a Dependency:

<dependencies>
  <dependency>
    <groupId>${project.groupId}</groupId>
    <artifactId>calcmaster-frontend</artifactId>
    <version>${project.version}</version>
  </dependency>
</dependencies>

Because sometimes it would be necessary to build the Backend without the Frontend-Application (i.E. just to test the REST-API) one can disable building and linking the Frontend-Application by using Option -DskipFrontend.

Beside of the Dependencies you have to redefine the Path, where Thymeleaf would look for its Templates inside application.yml:

spring:
  thymeleaf:
    prefix: "classpath:/META-INF/resources/frontend/"

If you want to deploy the Application with a Context-Path of your like you have to enable the ForwardedHeaderFilter within your application.yml:

server:
  forward-headers-strategy: framework

WebMvcConfiguration for multilingual Angular Application

Angular Application are bookmarkable. That means, that any missing Link (HTTP 404) must return the Index-Page so that the Angular Application will handle the HTTP 404 correctly.
You can define this Behaviour with the WebMvcConfigurer:

@Configuration
@EnableWebMvc
public class WebMvcConfiguration implements WebMvcConfigurer {
  private static final String[] ANGULAR_RESOURCES = {
      "/favicon.ico",
      "/main.*.js",
      "/polyfills.*.js",
      "/runtime.*.js",
      "/styles.*.css",
      "/deeppurple-amber.css",
      "/indigo-pink.css",
      "/pink-bluegrey.css",
      "/purple-green.css",
      "/3rdpartylicenses.txt"
  };
  private static final List<Locale> SUPPORTED_LANGUAGES = List.of(Locale.GERMAN, Locale.ENGLISH);
  private static final Locale DEFAULT_LOCALE = Locale.ENGLISH;

  private final String prefix;

  public WebMvcConfiguration(@Value("${spring.thymeleaf.prefix:" + ThymeleafProperties.DEFAULT_PREFIX + "}") String prefix) {
    this.prefix = StringUtils.appendIfMissing(prefix, "/");
  }

  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.setOrder(1);
    registry.addResourceHandler("/webjars/**")
            .addResourceLocations("classpath:/META-INF/resources/webjars/")
            .resourceChain(true); // necessary for the webjars-locator to be used
    SUPPORTED_LANGUAGES.forEach(registerLocalizedAngularResourcesTo(registry));
  }

  private Consumer<Locale> registerLocalizedAngularResourcesTo(ResourceHandlerRegistry registry) {
    return language -> {
      final var relativeAngularResources = Stream.of(ANGULAR_RESOURCES)
                                                 .filter(resource -> StringUtils.contains(resource, "*"))
                                                 .map(resource -> "/" + language.getLanguage() + resource)
                                                 .toArray(String[]::new);
      registry.addResourceHandler(relativeAngularResources)
              .addResourceLocations(prefix + language.getLanguage() + "/");

      final var fixedAngularResources = Stream.of(ANGULAR_RESOURCES)
                                              .filter(resource -> !StringUtils.contains(resource, "*"))
                                              .map(resource -> "/" + language.getLanguage() + resource)
                                              .toArray(String[]::new);
      registry.addResourceHandler(fixedAngularResources)
              .addResourceLocations(prefix);

      registry.addResourceHandler("/" + language.getLanguage() + "/assets/**")
              .addResourceLocations(prefix + language.getLanguage() + "/assets/");
    };
  }

  @Override
  public void addViewControllers(ViewControllerRegistry registry) {
    registry.setOrder(2);
    SUPPORTED_LANGUAGES.forEach(language -> registry.addViewController("/" + language.getLanguage() + "/**").setViewName(language.getLanguage() + "/index"));
  }

  @Bean
  public RouterFunction<ServerResponse> routerFunction() {
    return route(GET("/"), this::defaultLandingPage);
  }

  private ServerResponse defaultLandingPage(ServerRequest request) {
    final var locale = Optional.ofNullable(Locale.lookup(request.headers().acceptLanguage(), SUPPORTED_LANGUAGES))
                               .orElse(DEFAULT_LOCALE);
    return ServerResponse.status(HttpStatus.TEMPORARY_REDIRECT).render("redirect:/" + locale.getLanguage());
  }
}

Here you have several Aspects to recognize:

  1. All Angular-Resources will be served first. (registry.setOrder(1))
  2. only if this is not an Angular-Resource (or a REST-API) the index.html will be served (registry.setOrder(2))
  3. the Resources must be distinguished between relative Resources (with *) or fixed Resources (without *)
  4. all Languages, which will be provided by the Frontend Application must be available on SUPPORTED_LANGUAGES

WebMvcConfiguration for uni-lingual Angular Application

If you don't have a multilingual Angular Application the WebMvcConfiguration would be a bit easier:

@Configuration
@EnableWebMvc
public class WebMvcConfiguration implements WebMvcConfigurer {
  private static final String[] ANGULAR_RESOURCES = {
      "/favicon.ico",
      "/main.*.js",
      "/main-*.*.js",
      "/polyfills.*.js",
      "/polyfills-*.*.js",
      "/runtime.*.js",
      "/runtime-*.*.js",
      "/styles.*.css",
      "/deeppurple-amber.css",
      "/indigo-pink.css",
      "/pink-bluegrey.css",
      "/purple-green.css",
      "/3rdpartylicenses.txt"
  };

  private final String prefix;

  public WebMvcConfiguration(@Value("${spring.thymeleaf.prefix:" + ThymeleafProperties.DEFAULT_PREFIX + "}") String prefix) {
    this.prefix = StringUtils.appendIfMissing(prefix, "/");
  }

  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.setOrder(1);
    registry.addResourceHandler("/webjars/**")
            .addResourceLocations("classpath:/META-INF/resources/webjars/")
            .resourceChain(true); // necessary for the webjars-locator to be used
    registry.addResourceHandler(ANGULAR_RESOURCES)
            .addResourceLocations(prefix);
    registry.addResourceHandler("/assets/**")
            .addResourceLocations(prefix + "assets/");
  }

  @Override
  public void addViewControllers(ViewControllerRegistry registry) {
    registry.setOrder(2);
    registry.addViewController("/**").setViewName("index");
  }
}

WebFluxConfiguration for multilingual Angular Application

If you use Spring Reactive you have to implement an Implementation of WebFluxConfiguration

@Configuration
@EnableWebFlux
public class WebFluxConfiguration implements WebFluxConfigurer {
  private static final String[] ANGULAR_RESOURCES = {
      "/favicon.ico",
      "/main.*.js",
      "/polyfills.*.js",
      "/runtime.*.js",
      "/styles.*.css",
      "/deeppurple-amber.css",
      "/indigo-pink.css",
      "/pink-bluegrey.css",
      "/purple-green.css",
      "/3rdpartylicenses.txt"
  };
  private static final List<Locale> SUPPORTED_LANGUAGES = List.of(Locale.GERMAN, Locale.ENGLISH);
  private static final Locale DEFAULT_LANGUAGE = Locale.ENGLISH;

  private final String prefix;
  private final ThymeleafReactiveViewResolver thymeleafReactiveViewResolver;

  public WebFluxConfiguration(
      @Value("${spring.thymeleaf.prefix:" + ThymeleafProperties.DEFAULT_PREFIX + "}") String prefix, 
      ThymeleafReactiveViewResolver thymeleafReactiveViewResolver) {
    this.prefix = StringUtils.appendIfMissing(prefix, "/");
    this.thymeleafReactiveViewResolver = thymeleafReactiveViewResolver;
  }

  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/webjars/**")
            .addResourceLocations("classpath:/META-INF/resources/webjars/")
            .resourceChain(true); // necessary for the webjars-locator to be used
    SUPPORTED_LANGUAGES.forEach(language -> registry.addResourceHandler("/" + language.getLanguage() + "/**")
                                                    .addResourceLocations(prefix + language.getLanguage() + "/"));
  }

  @Override
  public void configureViewResolvers(ViewResolverRegistry registry) {
    registry.viewResolver(thymeleafReactiveViewResolver);
  }

  @Bean
  public RouterFunction<ServerResponse> routerFunction() {
    final var routerFunctionBuilder = route().GET("/", this::defaultLandingPage);
    SUPPORTED_LANGUAGES.forEach(addLocalizedLandingPageTo(routerFunctionBuilder));
    return routerFunctionBuilder.build();
  }

  private Mono<ServerResponse> defaultLandingPage(ServerRequest request) {
    final var locale = Optional.ofNullable(Locale.lookup(request.headers().acceptLanguage(), SUPPORTED_LANGUAGES))
                               .orElse(DEFAULT_LANGUAGE);
    return ServerResponse.temporaryRedirect(request.uriBuilder().path(locale.getLanguage()).build()).build();
  }

  private Consumer<Locale> addLocalizedLandingPageTo(RouterFunctions.Builder routerFunctionBuilder) {
    return language -> {
      var requestPredicate = Stream.of(ANGULAR_RESOURCES)
                                   .map(angularResource -> "/" + language.getLanguage() + angularResource)
                                   .reduce(GET("/" + language.getLanguage() + "/**"), (predicate, route) -> predicate.and(GET(route).negate()), RequestPredicate::and);
      requestPredicate = requestPredicate.and(GET("/" + language.getLanguage() + "/assets/**").negate());
      routerFunctionBuilder.route(requestPredicate, request -> ServerResponse.ok()
                                                                             .contentType(MediaType.TEXT_HTML)
                                                                             .render(language.getLanguage() + "/index"));
    };
  }
}

Within Spring Reactive we have no WebFluxConfiguration.addViewController() Method. Instead we have to instrument the RouterFunction to exclude the defined Resources and return index.html only if this is an undefined Resource.

I assume that there is no RestController defined under any Language-specific Context-Path (i.e. no /en/api/v1/... nor /de/api/v1/...). If you have these Constellation you also have to exclude these Paths within the RouterFunction.

WebFluxConfiguration for uni-lingual Angular Application

As for the uni-lingual WebMvcConfiguration the WebFluxConfiguration for a uni-lingual Angular Application is much simpler:

@Configuration
@EnableWebFlux
public class WebFluxConfiguration implements WebFluxConfigurer {
  private static final String[] ANGULAR_RESOURCES = {
      "/favicon.ico",
      "/main.*.js",
      "/polyfills.*.js",
      "/runtime.*.js",
      "/styles.*.css",
      "/deeppurple-amber.css",
      "/indigo-pink.css",
      "/pink-bluegrey.css",
      "/purple-green.css",
      "/3rdpartylicenses.txt"
  };
  private final String prefix;
  private final ThymeleafReactiveViewResolver thymeleafReactiveViewResolver;

  public WebFluxConfiguration(
      @Value("${spring.thymeleaf.prefix:" + ThymeleafProperties.DEFAULT_PREFIX + "}") String prefix,
      ThymeleafReactiveViewResolver thymeleafReactiveViewResolver) {
    this.prefix = StringUtils.appendIfMissing(prefix, "/");
    this.thymeleafReactiveViewResolver = thymeleafReactiveViewResolver;
  }

  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/webjars/**")
            .addResourceLocations("classpath:/META-INF/resources/webjars/")
            .resourceChain(true); // necessary for the webjars-locator to be used
    registry.addResourceHandler("/**")
            .addResourceLocations(prefix);
  }

  @Override
  public void configureViewResolvers(ViewResolverRegistry registry) {
    registry.viewResolver(thymeleafReactiveViewResolver);
  }

  @Bean
  public RouterFunction<ServerResponse> routerFunction() {
    final var requestPredicate = Stream.concat(
        Stream.of(ANGULAR_RESOURCES),
        Stream.of("/assets/**", "/api/**", "/webjars/**")
    ).reduce(GET("/**"), (predicate, route) -> predicate.and(GET(route).negate()), RequestPredicate::and);
    return route(requestPredicate, request -> ServerResponse.ok()
                                                            .contentType(MediaType.TEXT_HTML)
                                                            .render("index"));
  }
}

Just as WebFluxConfiguration for multilingual Application I assume that the RestController will be defined under the Context-Path /api/**. So we have to exclude these Paths before routing to index.html.

We also have to separately exclude /assets/** and /webjars/** so these two will also be handled by the ResourceHandler.