Documentation Home

6.2 to 7.0 Migration

These migration notes assumes that you are going from Broadleaf Framework 6.2 to 7.0. There might be additional migration steps necessary for your particular implementation of Broadleaf. If so, please contact our support team support@broadleafcommerce.com or let us know through our Gitter.

Overview of new features in the 7.0.0-GA release are detailed within 7.0.0-GA Release Notes.

Module Compatibility with 7.0

The version for each of the Broadleaf module is defined in broadleaf-bom. If you are using any particular version of any modules, either remove or update it. Here is a compatibility chart listing each of the module and version that is compatible with Broadleaf Framework 7.0.

Module Name Version
broadleaf-common-modules-enterprise 5.0.x
broadleaf-process 3.0.x
broadleaf-api 4.0.x
broadleaf-jobs-events 4.0.x
broadleaf-menu 4.0.x
broadleaf-common-presentation 2.0.x
broadleaf-thymeleaf-presentation 3.0.x
broadleaf-account-credit (giftcard) 5.0.x
broadleaf-enterprise-search 4.0.x
broadleaf-pricelist 5.0.x
broadleaf-enterprise 5.0.x
broadleaf-i18n-enterprise 4.0.x
broadleaf-multitenant-singleschema 5.0.x
broadleaf-theme 4.0.x
broadleaf-advanced-cms 4.0.x
broadleaf-advanced-inventory 4.0x
broadleaf-custom-field 4.0.x
broadleaf-oms 4.0.x
broadleaf-merchandising-group 3.0.x
broadleaf-product-type 3.0.x
broadleaf-import 4.0.x
broadleaf-importer 3.0.x
broadleaf-export 3.0.x
broadleaf-advanced-offer 4.0.x
broadleaf-customer-segment 3.0.x
broadleaf-cart-rules 3.0.x
broadleaf-catalog-access-policy 3.0.x
broadleaf-account 4.0.x
broadleaf-marketplace 3.0.x
broadleaf-quote 3.0.x
broadleaf-affiliate 3.0.x
broadleaf-contenttests 4.0.x
broadleaf-data-feed 3.0.x
broadleaf-subscription 4.0.x

Major feats

Dropped Libraries

  • All 'javax' libraries as part of transition to Jarkarta EE.
  • Commons-text
  • Spring Social. More information below.
  • ESAPI: Replaced with OWASP encoder and encoder-esapi. More information below.

Framework changes

Domain changes

Note: All sql statements below are for MySql. Make sure to change the sql syntax and column types for any other databases accordingly.

  • Broadleaf Framework

    • Add column ENABLE_DEFAULT_SKU_IN_INVENTORY to tableBLC_PRODUCT. Used to be woven in with property enable.weave.use.default.sku.inventory
     ALTER TABLE BLC_PRODUCT ADD COLUMN `ENABLE_DEFAULT_SKU_IN_INVENTORY` bit(1) DEFAULT NULL;
    
    • Add column LONG_DESCRIPTION to tableBLC_PRODUCT_OPTION.
    ALTER TABLE BLC_PRODUCT_OPTION ADD COLUMN `LONG_DESCRIPTION` varchar(255) DEFAULT NULL;
    
    • Remove column IS_FEATURED_PRODUCT from the tableBLC_PRODUCT.
    ALTER TABLE BLC_PRODUCT DROP COLUMN `IS_FEATURED_PRODUCT`;
    
  • Cart Rules

    • Add column QUALIFYING_MIN_TOTAL to tableBLC_CART_RULE. Used to be woven in with property enable.weave.cartrule.qualifyingMinSubTotal
    ALTER TABLE `BLC_CART_RULE` ADD `QUALIFYING_MIN_TOTAL` decimal(19,5);
    
  • Scheduled Jobs and Events

    • Add column NODE_ID and column LOCK_TIMESTAMP to tableBLC_SERIAL_EVENT_LOCK.
     ALTER TABLE `BLC_SERIAL_EVENT_LOCK` ADD `NODE_ID` varchar(255);
     ALTER TABLE `BLC_SERIAL_EVENT_LOCK` ADD `LOCK_TIMESTAMP` datetime;
    
  • Some deprecated classes, fields and methods were removed as part of 7.0.1-GA release. These deprecated tables and columns can be removed when using 7.0.1-GA or greater.

    DROP TABLE BLC_NODE_REGISTRATION;
    DROP TABLE BLC_SYSTEM_EVENT_NODE_FIN;
    DROP TABLE BLC_SNDBX_RLLBCK;
    DROP TABLE BLC_SNDBX_RLLBCK_ITEM;
    DROP TABLE BLC_PAYMENT_LOG;
    ALTER TABLE BLC_WIDGET DROP COLUMN TEMPLATE_PATH;
    ALTER TABLE BLC_FULFILLMENT_ORDER DROP COLUMN SHIPPER_TYPE, TRACKING_NUMBER, EXPECTED_SHIP_DATE, ACTUAL_SHIP_DATE;
    

Creating Schema Changelog

As with all Broadleaf upgrades, it is recommended to create your own Liquibase changelog as a sanity check that all indexes, columns, tables, and constraints are there. The steps to do so are as listed

  1. Generate a database for the existing codebase
  2. Upgrade the application to Broadleaf 7.0
  3. Set the following properties in default-shared.properties

        blPU.hibernate.hbm2ddl.auto=create
        blEventPU.hibernate.hbm2ddl.auto=create
        blSecurePU.hibernate.hbm2ddl.auto=create
        blCMSStorage.hibernate.hbm2ddl.auto=create
    
  4. Start up and then shut down the site application after it fully starts up

  5. Download the Liquibase command line tool here. Make sure the version is 4.3.1 or above.

  6. Copy the database connector JAR from your ~/.m2/repository to the download directory of Liquibase. For MySQL it will be ~/.m2/repository/mysql/mysql-connector-java/5.1.46/mysql-connector-java-5.1.46.jar

  7. Create a file named liquibase.properties and add the following changing the properties according to your database setup. The reference properties all pertain to the 7.0 database we created in step 4.

        changeLogFile: dbchangelog-6.2-to-7.0.xml
        driver: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/broadleaf?characterEncoding=utf8&useUnicode=true&serverTimezone=UTC
        username: un
        password: pass
        referenceDriver: com.mysql.jdbc.Driver
        referenceUrl: jdbc:mysql://localhost:3306/broadleaf_7.0?characterEncoding=utf8&useUnicode=true&serverTimezone=UTC
        referenceUsername: un
        referencePassword: pass
        classpath: mysql-connector-java-5.1.46.jar
    
  8. Run liquibase diffChangeLog which will write the changelog needed to migrate the schema from your previous database to the 7.0 database

  9. Add the changelog to your changelogs if you're using Liquibase for schema changes. Otherwise run liquibase --changeLogFile=<what your changeLogFile property is set to> updateSQL > dbchangelog-6.2-7.0.sql which will generate the SQL equivalent to the XML and output it to that SQL file which then can be used to migrate the database.

Code, Template, and Property Changes

With the spring security upgrade you can face that your servlet and/or security configs are no longer valid. Please consult spring migration guides mentioned above.
Also as an example here it is some pieces from the demo configs, you might find them useful:

@EnableMethodSecurity(securedEnabled = true)
public class AdminSecurityConfig {
    ............
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring().requestMatchers(
                "/css/**",
                "/js/**",
                "/img/**",
                "/fonts/**",
                "/" + assetServerUrlPrefixInternal + "/**",
                "/favicon.ico",
                "/robots.txt"
        );
    }
    ................
    @Bean(name = "blAdminAuthenticationManager")
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .authenticationProvider(authenticationProvider)
                .csrf(AbstractHttpConfigurer::disable)
                .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
                .sessionManagement(
                        sm -> {
                            sm.sessionFixation(SessionManagementConfigurer.SessionFixationConfigurer::migrateSession);
                            sm.enableSessionUrlRewriting(false);
                        }
                )
                .securityContext((securityContext) -> securityContext
                        .securityContextRepository(securityContextRepository)
                )
                .formLogin(
                        form -> form
                                .successHandler(successHandler)
                                .failureHandler(failureHandler)
                                .loginPage("/login")
                                .loginProcessingUrl("/login_admin_post")
                )
                .authorizeHttpRequests(
                        req -> {
                            req.requestMatchers("/global").hasRole("GLOBAL_ADMIN");
                            req.requestMatchers("/sendResetPassword", "/forgotUsername", "/forgotPassword", "/resetPassword", "/login").permitAll();
                            req.requestMatchers("/**").authenticated();
                        }
                )
                .requiresChannel(
                        channel -> channel
                                .requestMatchers("/**")
                                .requiresSecure()
                )
                .logout(
                        logout -> logout
                                .invalidateHttpSession(true)
                                .deleteCookies("ActiveId")
                                .logoutUrl("/adminLogout.htm")
                                .logoutSuccessHandler(logoutSuccessHandler)
                )
                .portMapper(
                        mapper -> mapper
                                .http(httpServerPort).mapsTo(httpsRedirectPort)
                )
                .addFilterBefore(adminCsrfFilter, UsernamePasswordAuthenticationFilter.class)
                .addFilterAfter(new AdminContentSecurityPolicyFilter(getCSPHeader()), AdminSecurityFilter.class);
        return http.build();
    }
}


@EnableMethodSecurity(securedEnabled = true)
public class SiteSecurityConfig {
    ............
    @Bean
    protected SecurityContextRepository blSecurityContextRepository(){
        return new DelegatingSecurityContextRepository(
                new RequestAttributeSecurityContextRepository(),
                new HttpSessionSecurityContextRepository()
        );
    }
    ..............
    @Resource(name = "blSecurityContextRepository")
    protected SecurityContextRepository securityContextRepository;
    ......................

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable)
                .headers(headers -> headers.frameOptions().disable())
                .sessionManagement(
                        sm -> {
                            sm.sessionFixation(SessionManagementConfigurer.SessionFixationConfigurer::migrateSession);
                            sm.enableSessionUrlRewriting(false);
                        }
                )
                .securityContext((securityContext) -> securityContext
                        .securityContextRepository(securityContextRepository)
                )
                .formLogin(
                        form -> form
                                .successHandler(successHandler)
                                .failureHandler(failureHandler)
                                .loginPage("/login")
                                .loginProcessingUrl("/login_post.htm")
                )
                .authorizeHttpRequests(
                        req -> {
                            req.requestMatchers("/account/**").authenticated();
                            req.anyRequest().permitAll();
                        }
                )
                .requiresChannel(
                        channel -> channel
                                .requestMatchers("/")
                                .requiresSecure()
                )
                .logout(
                        logout -> logout
                                .invalidateHttpSession(true)
                                .deleteCookies("ActiveId")
                                .logoutUrl("/logout")
                                .logoutSuccessHandler(getLogoutSuccessHandler())
                )
                .portMapper(
                        mapper -> mapper
                                .http(httpServerPort).mapsTo(httpsRedirectPort)
                )
                .addFilterBefore(securityFilter, UsernamePasswordAuthenticationFilter.class)
                .addFilterAfter(punchout2GoSessionStartFilter(), ChannelProcessingFilter.class)
                .addFilterBefore(punchout2GoAuthenticationFilter(), SwitchUserFilter.class)
                .rememberMe(
                        httpSecurityRememberMeConfigurer -> httpSecurityRememberMeConfigurer
                                .tokenValiditySeconds(TOKEN_VALIDITY_SECONDS)
                                .userDetailsService(userDetailsService)
                );
        return http.build();
    }

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring().requestMatchers(
                "/css/**",
                "/fonts/**",
                "/img/**",
                "/js/**",
                "/widget/js/**",
                "/" + assetServerUrlPrefixInternal + "/**",
                "/favicon.ico");
    }

    @Bean(name = "blAuthenticationManager")
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }
}

@EnableMethodSecurity(securedEnabled = true)
public class ApiSecurityConfig {

    private static final Log LOG = LogFactory.getLog(ApiSecurityConfig.class);

    @Value("${asset.server.url.prefix.internal}")
    protected String assetServerUrlPrefixInternal;

    @Value("${server.port:8445}")
    private int httpsRedirectPort;

    @Value("${http.server.port:8082}")
    protected int httpServerPort;

    @Bean(name = "blAuthenticationManager")
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring().requestMatchers(
                "/swagger-ui.html",
                "/api/*/swagger-ui.html",
                "/swagger-ui*/**",
                "/api/*/swagger-ui*/**",
                "/api/*/swagger-resources/*",
                //this is default url where docs are generated
                "/api/*/v3/api-docs",
                "/api/*/v3/api-docs.yaml",
                "/v3/api-docs/**",
                "/v3/api-docs.yaml",
                "/api/*/api-docs.yaml",
                "/api-docs.yaml"
        );
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .httpBasic(AbstractHttpConfigurer::disable)
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(req -> req.anyRequest().permitAll())
                .sessionManagement(session -> {
                    session.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
                    session.sessionFixation().none();
                    session.enableSessionUrlRewriting(false);
                })
                .requiresChannel(channel -> channel.anyRequest().requires(ChannelDecisionManagerImpl.ANY_CHANNEL))
                .portMapper(
                        mapper -> mapper
                                .http(httpServerPort).mapsTo(httpsRedirectPort)
                )
                .addFilterAfter(apiCustomerStateFilter(), RememberMeAuthenticationFilter.class);
        return http.build();
    }

    @Bean
    public Filter apiCustomerStateFilter() {
        RestApiCustomerStateFilter restApiCustomerStateFilter = new RestApiCustomerStateFilter();
        restApiCustomerStateFilter.setExcludeUrlPatterns(Arrays.asList("/api/*/swagger*", "/api/*/swagger*/**", "/swagger*", "/swagger*/**", "/**/swagger*/**"));
        return restApiCustomerStateFilter;
    }
}

Extracted check of GlobalEvents to a separate scheduler

A new scheduler has been created with the logic of checking global events for completion. Added new properties for configuring the scheduler: global event check interval event.global.check.interval.seconds and batch page size database.event.global.check.pagesize. Property was renamed from database.event.candidate.deletion.pagesize to database.event.deletion.pagesize and value was changed from 50 to 100:

event.global.check.interval.seconds=10
database.event.global.check.pagesize=50
database.event.deletion.pagesize=100

Note: For Oracle DB clients, pagesize must be less than 1000.

Thymeleaf Processors

Thymeleaf has been upgraded to '3.1'. Custom html pages now requires th:include to be replaced with th:insert and fragment expressions needs to be wrapped inside ~{...}.
See more here
Some existing custom Thymeleaf processors were updated and some were removed. Now all these processors implement BroadleafVariableExpression instead of BroadleafVariableModifierExpression, which requires updating HTML tags.

Please check the example for AdminModuleProcessor: before - <blc_admin:admin_module></>, now - ${#admin_module.getAllModules()}. We directly call the method of the processor by the variable name. Please check the documentation for the BroadleafVariableExpression

Updated Processors:

AdminModuleExpression: <blc_admin:admin_module> -> ${#admin_module.getAllModules()}
AdminUserExpression: </blc:admin_user> -> ${#admin_user.getUser()}
NamedOrderExpression: <blc:named_order> -> ${#named_order.getWishlist()}
AdminFieldBuilderExpression: <blc_admin:admin_field_builder> -> ${#admin_field_builder.getFieldWrapper()
CreditCardTypesExpression: <blc:credit_card_types> -> ${#credit_card_types.getTypes()}
RatingsExpression: <blc:ratings> -> ${#ratings.getRatings(product.id)}
ProductOptionsExpression: <blc:product_option_display> -> ${#product_option_display.getDisplayValues(item)
ProductOptionsExpression: <blc:product_options> -> ${#product_options.getData(product.id).get('skuPricing')}
ProductOptionsExpression: <blc:product_options> -> ${#product_options.getDataAddOn(product.id, addOnXrefId).get('skuPricing')}
DataDrivenEnumVariableExpression: <blc:enumeration> -> ${#enumeration.getEnumValues('PRIMARY_RETURN_REASON_TYPE')}
DataDrivenEnumVariableExpression: <blc:enumeration> -> ${#enumeration.getEnumValues('SECONDARY_RETURN_REASON_TYPE')}

Also, please check these Removed Processors list and make sure that you are not using these tags.

Removed Processors:

BreadcrumbProcessor: <blc: breadcrumbs>
ErrorsProcessor: <blc_admin:errors>
ContentProcessor: <blc:content>
ConfigVariableProcessor: <blc:config>
DataDrivenEnumerationProcessor: <blc:enumeration>
CategoriesProcessor: <blc:categories>
OnePageCheckoutProcessor: <blc:one_page_checkout>

Removed Property:

admin.form.validation.errors.hideTopLevelFieldErrors

Removed Deprecated Classes

These deprecated classes are removed and their uses have been removed.

FieldEnumeration.java
FieldEnumerationImpl.java
FieldEnumerationItem.java
FieldEnumerationItemImpl.java
LegacyCartService.java
LegacyCartServiceImpl.java
LegacyMergeCartServiceImpl.java
LegacyOrderService.java
LegacyOrderServiceImpl.java
BLCAnnotationUtils.java
BLResourceBundleMessageSource.java
ResourceBundleExtensionPoint.java

Deprecated Country and State fields removed.

In Address.java, 'country' and 'state' has been removed. They were deprecated in favor of 'ISOCountry' and 'ISOCountrySubdivision' with enhanced support for internationalization.

Spring social removal

Spring social reached its end of life in 2019. It has been removed from the framework and BroadleafSocialRegisterController has been refactored to use OAuth2 implementation. More information is available here

Hibernate mapping changes

After updating to the new version of Hibernate (6.x), some annotations have become deprecated and may soon be removed. So we are replacing Hibernate annotations @Table and @Index and moving to Jakarta.

Also, worth to mention that our basic way of generating an ID is @GenericGenerator, example:

@GenericGenerator(
    name = "entityId",
    type = IdOverrideTableGenerator.class,
        parameters = {
            @Parameter(name = "segment_value", value = "EntityImpl"),
            @Parameter(name = "entity_name", value = "package.of.entity.EntityImpl")
    }
)

Note: Do not forget to check custom entities

LOB/CLOB text mapping changes

To support different databases, we suggest using the following approach:

@lob
@JdbcType(LongVarcharJdbcType.class)
@column(name = "STRUCTURED_DATA_TEXT", length = Integer.MAX_VALUE - 1)
private String structuredDataText;

Added @JdbcType(LongVarcharJdbcType.class) because postgres will fail to read data otherwise. So if you are using mysql you can use just length attribute+LOB, or you can keep the framework approach and use @JdbcType(LongVarcharJdbcType.class).
Also consult with hibernate-doc

Hibernate sequence generator changes

In Hibernate 6 there were changes to the formulas of table sequence generator used to calculate the next value. So when migrating from BLC 6.2 to 7.0 you might need to do be aware of this.
In general, BLC framework has protection from stale sequences; it is controlled by properties:

detect.sequence.generator.inconsistencies
auto.correct.sequence.generator.inconsistencies

So if both are turned on, then on startup your sequences should be fixed automatically.
In case you don't use it, or don't want to enable it, you need to exec the following sql:

UPDATE SEQUENCE_GENERATOR SET ID_VAL=ID_VAL+100;

Here 100 is used, assuming that you didn't change the default increment size (pretty sure you didn't), which is 50.

Hibernate Dialect Changes

Instead of BroadleafXXXDialect use dialect provided by hibernate for your database. Usually dialect is set in properties files via property <persistenceUnitName>.hibernate.dialect.
Consult with the list of dialects

ESAPI replacement

Maven dependency org.owasp.esapi:esapi is replaced with org.owasp.encoder:encoder and org.owasp.encoder:encoder-esapi. Some ESAPI classes are tightly coupled with 'javax' packages and no new version with support for 'jakarta' is available yet.
In general, this should not affect your project, unless you use some of the ESAPI beyond the needs of BLC framework. Otherwise, you should not notice that something has changed. All ESAPI and Antisamy config files are still in use. Here is a short list of things that changed:

  • In UrlUtil regex is used instead of ESAPI.validator().isValidRedirectLocation to validate url.
  • ESAPI.encoder().encodeForHTML is replaced with ESAPIEncoder.getInstance().encodeForHTML
  • ESAPI.httpUtilities().addHeader is replaced with inline code that does the same thing.

API project spring-fox to springdoc update

Spring-fox project has been abandoned and is no longer in active development. It is missing support for jakarta and new spring. Thus, Springdoc is used instead.
Now Swagger needs to use either statically defined schema or autogenerated by Springdoc. Generation is happening on the first request, so it can take some time to fulfill the first request to the Swagger.
It is controlled by the following properties

  • Use the following setup to enable autogeneration of schema yaml

    blc.api.doc.autogeneration.enabled=true
    # make sure that the following properties commented out
    #springdoc.api-docs.enabled
    #springdoc.swagger-ui.url
    
  • If you want(and basically it is advised) to have statically defined schema use the following setup

    springdoc.api-docs.enabled=false
    # This is a url for controller that will return custom yaml file with service definitions
    springdoc.swagger-ui.url=/api-docs.yaml
    # make sure that blc.api.doc.autogeneration.enabled is false or commented out
    

    The file api-docs.yaml already exists in the api project (it was autogenerated with some BLC endpoints) and you can continue editing it, adding your endpoints etc
    You also need a controller that will supply that file to Swagger.
    We have this one:

/**
 * This controller is a part of openapi v3 setup.
 * Its goal to provide custom yaml file with service definition instead of auto-generated.
 * To enable it in default.properties uncomment properties springdoc.api-docs.enabled=false
 * ,springdoc.swagger-ui.url=/api-docs.yaml. Also in uncomment in RestApiMvcConfiguration bean
 * definitions: springDocConfiguration, springDocConfigProperties and comment out definition for customOpenAPI
 */
@Controller
@Hidden
public class GetDocsController {
    @RequestMapping(path="/api-docs.yaml", method = RequestMethod.GET)
    @ResponseBody
    public String getApiDocs() throws IOException {
        File resource = new ClassPathResource("api-docs.yaml").getFile();
        return new String(Files.readAllBytes(resource.toPath()));
    }
}

By default, swagger should be accessible by /api/v1/
Also because of springdoc migration springfox annotations are no longer accessible. Use the following guide

Spring Security Changes

You probably should not be affected by this migration but if you manually login some user you should read it through

In Spring Security 5, the default behavior is for the SecurityContext to automatically be saved to the SecurityContextRepository using the SecurityContextPersistenceFilter. Saving must be done just prior to the HttpServletResponse being committed and just before SecurityContextPersistenceFilter. Unfortunately, automatic persistence of the SecurityContext can surprise users when it is done prior to the request completing (i.e. just prior to committing the HttpServletResponse). It also is complex to keep track of the state to determine if a save is necessary causing unnecessary writes to the SecurityContextRepository (i.e. HttpSession) at times.

In Spring Security 6, the default behavior is that the SecurityContextHolderFilter will only read the SecurityContext from SecurityContextRepository and populate it in the SecurityContextHolder. Users now must explicitly save the SecurityContext with the SecurityContextRepository if they want the SecurityContext to persist between requests. This removes ambiguity and improves performance by only requiring writing to the SecurityContextRepository (i.e. HttpSession) when it is necessar

SecurityContextHolder.setContext(securityContext);

should be replaced with:

SecurityContextHolder.setContext(securityContext);
securityContextRepository.saveContext(securityContext, httpServletRequest, httpServletResponse);