Broadleaf Microservices
  • v1.0.0-latest-prod

Routes

Overview

Routes represent the relationship between a browser’s location, and the view it renders. These routes typically consist of a path, e.g. /brands, and a componentId, e.g. brands:browse. The admin client loads the entire route configuration on page load, and then uses it to route the user around the application.

Creating Routes

Routes are primarily configured through Spring auto-configuration using the Route DSL. Here is a simple example that creates a single route for a new admin page:

@Configuration
public class BrandMetadataAutoConfiguration {
    @Bean
    public ComponentRouteLocator brandMetadataRoutes(RoutesBuilder routesBuilder) {
        return routesBuilder.routes()
                .route("/brands", r -> r.componentId("brands:browse"))
                .build();
    }
}

This example creates a route that maps the location of /brands to a view component registered with the ID of brands:browse. Assuming a view component has been registered for brands:browse, then we should expect a user to see this view when they visit /brands.

Overriding Existing Routes

Broadleaf provides a number of route configurations out-of-box as part of the default admin configuration. If necessary for certain use cases, these can be overridden by registering a bean matching the name of the original ComponentRouteLocator. By registering a bean matching the same name, the original’s @ConditionalOnMissingBean will cause the out-of-box routes to skip registration. Here is an example of us overriding the routes for the product views to map the same locations to a new set of component IDs:

@Configuration
public class MyProductMetadataAutoConfiguration {
    @Bean
    public ComponentRouteLocator productMetadataRoutes(RoutesBuilder routesBuilder) {
        return routesBuilder.routes()
                .route("/products", r -> r.componentId("my:product:browse"))
                .route("/products/create", r -> r.componentId("my:product:create"))
                .route("/products/:id", r -> r.componentId("my:product:update"))
                .build();
    }
}

Ordering Routes

The ordering of routes is important in understanding which ones will be matched within the client. A good example to understand this is to look at the "create" and "update" routes for entity views. If you are going to put your "create" route at /entity/create, and your "update" route at /entity/:id, then you need to be sure the /entity/create route is registered first. This is because the client processes the routes in the order in which they were registered, and once it finds a viable match, it uses that route and looks no further. This means that if you put /entity/:id in front of /entity/create, then /entity/create will never be matched.

@Configuration
public class BrandMetadataAutoConfiguration {
    @Bean
    public ComponentRouteLocator brandMetadataRoutes(RoutesBuilder routesBuilder) {
        return routesBuilder.routes()
                 // good
                .route("/brands/create", r ->
                        r.componentId("brands:create"))
                .route("/brands/:id", r ->
                        r.componentId("brands:update"))
                // bad, will never be matched
                .route("/brands/create", r ->
                        r.componentId("my:brands:create"))
                .build();
    }
}

Route Predicates

Routes support the use of predicates to narrow the context in which a route exists. This feature is useful to provide views that only show up within certain contexts or for certain users. The most general route predicate is able to be configured using the #predicate(Predicate<WebRequest>) method on the route DSL, for example:

@Configuration
public class BrandMetadataAutoConfiguration {
    @Bean
    public ComponentRouteLocator brandMetadataRoutes(RoutesBuilder routesBuilder) {
        return routesBuilder.routes()
                .route("/brands", r ->
                        r.componentId("brands:browse:es")
                                // route applies when language is "es"
                                .predicate(req -> req.getLocale().getLanguage().equals("es")))
                .build();
    }
}

Tenancy Predicates

One of the more useful predicate patterns provided is around tenancy. These predicates can be used to restrict certain routes to be tenant only, application only, or even be restricted to specific tenants and applications. Here is an example of using these various tenancy predicates to restrict the context these routes exist:

@Configuration
public class BrandMetadataAutoConfiguration {
    @Bean
    public ComponentRouteLocator brandMetadataRoutes(RoutesBuilder routesBuilder) {
        return routesBuilder.routes()
                // only for application with ID of "application_a"
                .route("/brands", r ->
                        r.componentId("application_a:brands:browse")
                                .tenancy(t -> t.application("application_a")))
                // only for tenant with ID of "tenant_a"
                .route("/brands", r ->
                        r.componentId("tenant_a:brands:browse")
                                .tenancy(t -> t.tenant("tenant_a")))
                // only for application-level contexts
                .route("/brands", r ->
                        r.componentId("application:brands:browse:default")
                                .applicationOnly())
                // only for tenant-level contexts
                .route("/brands", r ->
                        r.componentId("tenant:brands:browse:default")
                                .tenantOnly())
                .build();
    }
}

Predicate Factories

There are situations where you may want to re-use common predicate patterns, and that is where implementing the RoutePredicateFactory interface becomes useful. For instance, Broadleaf uses this pattern this internally to build the TenancyRoutePredicateFactory so that tenancy contexts can be evaluated without having to think about how to parse the tenant information from a WebRequest.

To understand how to implement this pattern, we will walk through an example implementation of a RoutePredicateFactory that makes it easier to limit a route to certain languages. First, we will create a new LanguageRoutePredicateFactory class which extends AbstractRoutePredicateFactory:

public class LanguageRoutePredicateFactory extends AbstractRoutePredicateFactory<LanguageConfig> {

    public LanguageRoutePredicateFactory() {
        super(LanguageConfig.class);
    }

    @Override
    public String name() {
        return "Language";
    }

    @Override
    public Predicate<WebRequest> apply(LanguageConfig config) {
        return request -> {
            if (CollectionUtils.isEmpty(config.getLanguages())) {
                // permit, no languages to restrict the request to
                return true;
            }

            Locale locale = request.getLocale();
            String language = locale.getLanguage();

            // permit if the language is a match, otherwise block
            return config.getLanguages().contains(language);
        };
    }

    @Getter(AccessLevel.PROTECTED)
    @Setter(AccessLevel.PROTECTED)
    @EqualsAndHashCode
    @ToString
    public static class LanguageConfig {
        private Set<String> languages = new HashSet<>();

        public LanguageConfig language(String... languages) {
            setLanguages(SetUtils.hashSet(languages));
            return this;
        }
    }
}

After we have created the predicate factory, we should then register it as a bean:

@Configuration
public class MetadataPredicateFactoryAutoConfiguration {

    @Bean
    public LanguageRoutePredicateFactory languageRoutePredicateFactory() {
        return new LanguageRoutePredicateFactory();
    }
}

Finally, we can make use of this factory within our route configuration:

@Configuration
public class BrandMetadataAutoConfiguration {
    @Bean
    public ComponentRouteLocator brandMetadataRoutes(RoutesBuilder routesBuilder, LanguageRoutePredicateFactory languagePredicateFactory) {
        return routesBuilder.routes()
                .route("/brands", r ->
                        r.componentId("brands:browse:es")
                                // route applies when language is "es"
                                .predicate(languagePredicateFactory.apply(l -> l.language("es"))))
                .route("/brands", r ->
                        r.componentId("brands:browse:fr")
                                // route applies when language is "fr"
                                .predicate(languagePredicateFactory.apply(l -> l.language("fr"))))
                .build();
    }
}

Database Driven Routes

In some cases, it may be necessary to have routes that are driven by data in the database. This is primarily used for views created by Business Type domain. The domain for Route includes the following fields:

  • id - Unique identifier for the route

  • path - Path matching the browser location to match to render the component

  • componentKey - Key of the component to render at path

  • componentName - Name of the component

  • scopes - Security scopes needed for a user to access the route

In addition to creating routes through the database, there is also the option to create components in the database. This again facilitates the use of Business Type domain. The domain for RouteComponent includes the following fields:

  • id - Unique identifier for the component

  • componentKey - Key of the component

  • parentComponentKey - Key of the parent component

  • componentName - Name of the component

  • componentType - Type of the component

  • businessTypeKey - Key of the Business Type that generated the component

The key piece to note about the RouteComponent domain is that it has a parentComponentKey field. This field is used to create a hierarchy of components allowing the defined component to inherit all metadata and augmentations from a parent component.