Patch releases mechanism implementation

This implementation allows you to import your release impexes during initialization/update process and treat them as patches by using Patching Framework (see SAP Help Portal ) so they are executed only once.

Goals and differences from OOTB hybris patches Default patches mechanism give you opportunity use the @SystemSetup annotation in any ServiceLayer class to hook ServiceLayer code into the SAP Commerce initialization and update life-cycle events. In this way, you can provide a means for creating essential and project data for any extension.

By using annotations, no interfaces have to be implemented or parent classes extended, keeping the classes loosely coupled. Methods are called in an order that respects the extension build order. You can define when a method should be executed, and pass context information to it. Whereas with this implemantation we need only perform belows steps and put impexes in right path.

Implementation steps:

1) Perform ant extgen and generate releasesextension

2) Add it to your localextensions.xml

3) You can delete redundant generated files.

4) Create release folder in YOUR_EXTENSION/import. Each folder inside the release dir is considered as separate patch, every impex file is executed only once in scope of the patch.

5) Example of folders structure:

image.png

6) Go to YourExtensionNameConstants and specify a a path to your releases folder

public static final String RELEASE_FOLDER = "/YOUR_EXTENSION/import/releases";

7) Go to your_extension_name.setup package and create ReleasesSystemSetupCollectorImpl.java. This one extends default collector and create/execute patches per folder. You can find the implementation bellow (Change YOUR_EXTENSIONConstants.RELEASE_FOLDER to suitable one:

public class ReleasesSystemSetupCollectorImpl extends SystemSetupCollectorImpl {

    private static final Logger LOG = Logger.getLogger(ReleasesSystemSetupCollectorImpl.class);
    private ApplicationContext applicationContext;
    private SystemSetupAuditDAO systemSetupAuditDAO;

    @Override
    public List<SystemSetupCollectorResult> getApplicablePatches(String extensionName) {
        List<SystemSetupCollectorResult> applicablePatches = super.getApplicablePatches(extensionName);

        if (extensionContainsReleasesSetup(extensionName)) {
            applicablePatches.addAll(getReleasePatches());
        }

        return applicablePatches.stream().filter(SystemSetupCollectorResult::isPatch).filter(this::isReleasePatchNotApplied).collect(Collectors.toList());
    }

    @Override
    public void executeMethods(SystemSetupContext ctx) {
        super.executeMethods(ctx);

        if (extensionContainsReleasesSetup(ctx.getExtensionName())) {
            for (SystemSetupCollectorResult releasePatch : getReleasePatches()) {
                if (releasePatch.isPatch() && shouldApplyReleasePatch(ctx, releasePatch)) {

                    ReflectionUtils.invokeMethod(releasePatch.getMethod(), releasePatch.getObject());
                    systemSetupAuditDAO.storeSystemPatchAction(releasePatch);

                }
            }
        }
    }

    private boolean extensionContainsReleasesSetup(String currentExtension) {
        SystemSetup classAnnotation = AnnotationUtils.findAnnotation(ReleasesSystemSetup.class, SystemSetup.class);

        if (classAnnotation == null) {
            return false;
        }

        return currentExtension.equals(classAnnotation.extension());
    }

    private boolean shouldApplyReleasePatch(SystemSetupContext ctx, SystemSetupCollectorResult systemSetupObject) {
        boolean applyPatch;
        if (ctx.isFilteredPatches()) {
            List<String> patchHashesToApply = ctx.getPatchHashesToApply(ctx.getExtensionName());
            applyPatch = patchHashesToApply.contains(systemSetupObject.getHash()) && this.isReleasePatchNotApplied(systemSetupObject);
        } else {
            applyPatch = this.isReleasePatchNotApplied(systemSetupObject);
        }

        return applyPatch;
    }

    private boolean isReleasePatchNotApplied(SystemSetupCollectorResult collectorResult) {
        return !this.systemSetupAuditDAO.isPatchApplied(collectorResult.getHash());
    }


    public List<SystemSetupCollectorResult> getReleasePatches() {

        List<SystemSetupCollectorResult> applicableReleasePatches = new ArrayList<>();

        try {
            final URL url = this.getClass().getResource(YOUR_EXTENSIONConstants.RELEASE_FOLDER);

            if (null == url) {
                return applicableReleasePatches;
            }

            final URI uri = url.toURI();
            final File releasesFolder = new File(uri);

            if (releasesFolder.isDirectory()) {
                final File[] folders = releasesFolder.listFiles();

                if (folders != null && folders.length > 0) {
                    for (final File folder : folders) {

                        createPatchForReleaseFolder(folder, applicableReleasePatches);

                    }
                }
            }

        } catch (final URISyntaxException e) {
            LOG.error(e.getMessage(), e);
        }

        return applicableReleasePatches;
    }

    public void createPatchForReleaseFolder(File folder, List<SystemSetupCollectorResult> applicableReleasePatches) {
        if (folder.isDirectory()) {

            ReleasesSystemSetup importObject = this.applicationContext.getBean(ReleasesSystemSetup.class);
            importObject.setReleaseName(folder.getName());
            Class<? extends ReleasesSystemSetup> importClass = importObject.getClass();
            Method[] methods = importClass.getMethods();

            SystemSetup classAnnotation = AnnotationUtils.findAnnotation(importClass, SystemSetup.class);
            for (Method method : methods) {
                SystemSetup methodAnnotation = AnnotationUtils.findAnnotation(method, SystemSetup.class);
                if (methodAnnotation != null && classAnnotation != null) {

                    String extensionName = fetchExtensionNameFromAnnotation(classAnnotation, methodAnnotation);
                    String description = fetchDescriptionFromAnnotation(classAnnotation, methodAnnotation);

                    if (StringUtils.isNotEmpty(extensionName)) {
                        applicableReleasePatches.add(new SystemSetupCollectorResult(classAnnotation, methodAnnotation, importObject, extensionName, ReleasesSystemSetup.class + folder.getName(), method, folder.getName(), description, false, true));
                    }
                }
            }
        }
    }

    private String fetchExtensionNameFromAnnotation(SystemSetup classAnnotation, SystemSetup methodAnnotation) {
        return StringUtils.isNotEmpty(methodAnnotation.extension()) ? methodAnnotation.extension() : classAnnotation.extension();
    }

    private String fetchDescriptionFromAnnotation(SystemSetup classAnnotation, SystemSetup methodAnnotation) {
        return StringUtils.isNotEmpty(methodAnnotation.description()) ? methodAnnotation.description() : classAnnotation.description();
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        super.setApplicationContext(applicationContext);
        this.applicationContext = applicationContext;
    }

    @Override
    public void setSystemSetupAuditDAO(SystemSetupAuditDAO systemSetupAuditDAO) {
        super.setSystemSetupAuditDAO(systemSetupAuditDAO);
        this.systemSetupAuditDAO = systemSetupAuditDAO;
    }
}

8) Create ReleasesSystemSetup.java. This one contains a method that import impex files and get executed per each patch. You can find the implementation bellow (ChangeYOUR_EXTENSIONConstants to suitable one:

@SystemSetup(extension = YOUR_EXTENSIONConstants.EXTENSIONNAME)
public class ReleasesSystemSetup {

    private static final Logger LOG = Logger.getLogger(ReleasesSystemSetup.class);
    private static final String SLASH = "/";

    private SetupImpexService setupImpexService;
    private String releaseName;

    @SystemSetup
    public void applyReleasePatches() {
        if (StringUtils.isNotEmpty(releaseName)) {
            try {
                final URI uri = this.getClass().getResource(YOUR_EXTENSIONConstants.RELEASE_FOLDER + SLASH + releaseName).toURI();
                final File releaseFolder = new File(uri);
                if (releaseFolder.isDirectory()) {
                    LOG.info("Filtering only impex files...");
                    final File[] files = releaseFolder.listFiles((dir, name) ->
                    {
                        LOG.info(name);
                        return name.endsWith(".impex");
                    });

                    if (files != null) {
                        processImpexFiles(releaseName, files);
                    } else {
                        LOG.info("Release folder is empty, skipping impex import.");
                    }
                }
            } catch (final URISyntaxException e) {
                LOG.error(e.getMessage(), e);
            }
        }
    }

    private void processImpexFiles(final String parameterValue, final File[] files) {
        LOG.info(String.format("Start processing impex files for release %s ...", parameterValue));

        logFilesImportOrder(files);

        for (final File file : files) {
            final String fileName = parameterValue + SLASH + file.getName();
            final String filePath = YOUR_EXTENSIONConstants.RELEASE_FOLDER + SLASH + fileName;
            getSetupImpexService().importImpexFile(filePath, false);

        }
    }


    private static void logFilesImportOrder(File[] files) {
        if (LOG.isDebugEnabled()) {
            for (File file : files) {
                LOG.debug(file.getName());
            }
        }
    }

    public String getReleaseName() {
        return releaseName;
    }

    public void setReleaseName(String releaseName) {
        this.releaseName = releaseName;
    }

    public SetupImpexService getSetupImpexService() {
        return setupImpexService;
    }

    public void setSetupImpexService(SetupImpexService setupImpexService) {
        this.setupImpexService = setupImpexService;
    }
}

9) Go to your extensioninfo.xml and add:

<requires-extension name="commerceservices"/>

(delete <webmodule/> if you want to clear web module for the extension)

10) Go to your_extension-spring.xml and add 2 beans

<alias alias="systemSetupCollector" name="drivenbrandsSystemSetupCollector"/>
<bean id="drivenbrandsSystemSetupCollector" class="com.releases.setup.ReleasesSystemSetupCollectorImpl">
  <property name="systemSetupAuditDAO" ref="defaultSystemSetupAuditDAO"/>
</bean>

<bean id="releasesSystemSetup" class="com.releases.setup.ReleasesSystemSetup" scope="prototype">
   <property name="setupImpexService" ref="defaultSetupImpexService"/>
</bean>

Now you need to build the project.

Example of usage:

Go to hac->platform update/initialization. You will see the unapplied patches. It’s important to select your_release_extension as well.

image.png After that you will see that patches were applied, impexes were executed and new records for type “SystemSetupAudit” were created:

image.png You can find the example of releaseextension here.