Documentation Home

Adding exports to a new domain

  1. Add the export module as a dependency in your POM
  2. Create a new class extending ExportEntityType and add a new type in accordance to the domain that you're adding exports to
  3. Create an admin extension handler to add a button for exporting on the page that you want exporting to be done in the admin

    • Most likely this would be done by creating a class that extends AbstractAdminAbstractControllerExtensionHandler and implements addAdditionalMainActions(). Be sure that one of the button classes is export-standard-action in order for the JavaScript in the export module to automatically setup the onclick listener for it. Example:
    @Override
    public ExtensionResultStatusType addAdditionalMainActions(String sectionClassName, List<EntityFormAction> actions) {
    
        if (sectionClassName.equals(CustomerSegment.class.getName()) || sectionClassName.equals(CustomerSegmentImpl.class.getName())) {
            actions.add(new EntityFormAction("export-customer-segment")
                    .withDisplayText("export_customer_segments")
                    .withIconClass("icon-upload")
                    .withButtonClass("export-standard-action")
                    .withUrlOverride("/export/customer-segment")
            );
            return ExtensionResultStatusType.HANDLED_CONTINUE;
        }
    
        return ExtensionResultStatusType.NOT_HANDLED;
    }
    
  4. Create a controller that has a GET and POST for whichever url you chose for the .withUrlOverride property. In order to use the default modal popup provided in the export module be sure that the GET returns "views/configureExportPrompt". An example of the GET and POST created for the CustomerSegment export is shown below

     /**
     * Shows the form that the allows the user to create an export file
     */
    @RequestMapping(value = "/customer-segment", method = RequestMethod.GET)
    public String viewConfigurePrompt(HttpServletRequest request, HttpServletResponse response, Model model) throws Exception {
        model.addAttribute("exportFormats", SupportedExportType.getTypes());
        model.addAttribute("exportEncodings", SupportedExportEncoding.getTypes());
        model.addAttribute("baseUrl", request.getRequestURL().toString());
        return "views/configureExportPrompt";
    }
    
    /**
     * Used by the Export Prompt to schedule a job so that the export can be processed later.
     */
    @RequestMapping(value = "/customer-segment", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
    public @ResponseBody Map<String, String> uploadExport(HttpServletRequest request,
                         @RequestParam(value = "shareable", defaultValue = "false") Boolean shareable,
                         @RequestParam("exportEncoding") String exportEncoding,
                         @RequestParam("exportType") String exportType) {
        Map<String, String> resp = new HashMap<>();
        customerSegmentExportScheduler.scheduleExport(exportType, exportEncoding, shareable);
        resp.put("status", "success");
        return resp;
    }
    
  5. Next create an export event scheduler that extends AbstractExportEventScheduler. In your custom scheduler, you should utilize the parent's controls for Process interactions to correctly interact with status for your background process. For example:

    @Component
    @ConditionalOnExport
    public class CustomerSegmentExportScheduler extends AbstractExportScheduler {
    
        @Resource
        protected CustomerSegmentService customerSegmentService;
    
        @Override
        public void scheduleAllCustomerSegmentExport(String formatType, String encodingType, boolean shareable, String friendlyName) {
            Map<String, String> processParams = buildProcessParams(formatType, encodingType, shareable, friendlyName);
    
            // Register the process
            Long processId = processRegistrationManager.register(friendlyName, CustomerSegmentExportProcessExecutor.PROCESS_TYPE, processParams);
            // Figure out the total records for my process to give statuses
            Long totalRecords = customerSegmentService.getNumberOfCustomerCustomerSegXrefs();
            processStateManager.updateProgressTotals(processId, 0l, totalRecords);
    
            // Kick off the export process
            processActionManager.start(processId);
        }
    }
    
  6. Then create an ExportProcessExector that extends AbstractExportProcessExecutor. You will also notice that some of rthe methods here (like updateProcessProgress()) update the amounts that have been processed as they get exported. For exmple:

    @ConditionalOnExport
    @Component("blCustomerSegmentExportProcessExecutor")
    public class CustomerSegmentExportProcessExecutor extends AbstractExportProcessExecutor {
    
        public static final String CUST_SEG_EXP_FILENAME = "customerSegmentExport.csv";
        public static final String PROCESS_TYPE = "CUSTOMER_SEG_EXPORT";
        public static final String CUSTOMER_SEGMENT_ID = "CUSTOMER_SEGMENT_ID";
    
        @Resource(name = "blCustomerSegmentExportService")
        protected CustomerSegmentExportService customerSegmentExportService;
    
        @Resource(name = "blCustomerSegmentService")
        protected CustomerSegmentService customerSegmentService;
    
        private Integer batchSize = null;
    
        @Override
        protected void export(ExportProcessExecutorContext context) throws IOException {
            boolean isFirst = true;
            Long firstId = Long.MIN_VALUE;
            Long processId = context.getProcessId();
            Long recordsProcessed = 0L;
    
            if (context.getProcessParams().containsKey(CUSTOMER_SEGMENT_ID)) {
                Long customerSegmentId = Long.parseLong(context.getProcessParams().get(CUSTOMER_SEGMENT_ID));
                Long totalRecords = customerSegmentService.getNumberOfXrefsForCustomerSegment(customerSegmentId);
                do {
                    firstId = customerSegmentExportService.exportBatchCustomerSegmentsCsv(customerSegmentId, firstId, getBatchSize(), context.getOutputStream(), isFirst);
                    isFirst = false;
    
                    recordsProcessed = updateProcessProgress(processId, firstId, recordsProcessed, totalRecords);
                } while (firstId != null);
            } else {
                Long totalRecords = customerSegmentService.getNumberOfCustomerCustomerSegXrefs();
                do {
                    firstId = customerSegmentExportService.exportAllBatchCustomerSegmentsCsv(firstId, getBatchSize(), context.getOutputStream(), isFirst);
                    isFirst = false;
    
                    recordsProcessed = updateProcessProgress(processId, firstId, recordsProcessed, totalRecords);
                } while (firstId != null);
            }
        }
    
        protected Long updateProcessProgress(Long processId, Long firstId, Long recordsProcessed, Long totalRecords) {
            // If firstId is null, then we just finished processing the last page, otherwise we just processed a full page
            if (firstId == null) {
                processStateManager.updateProgressTotal(processId, totalRecords);
                return totalRecords;
            } else {
                processStateManager.updateProgressTotal(processId, recordsProcessed + getBatchSize());
                return recordsProcessed + getBatchSize();
            }
        }
    
        @Override
        public String getExportFileName() {
            return CUST_SEG_EXP_FILENAME;
        }
    
        @Override
        public String getEntityType() {
            return CustomerSegmentExportEntityType.CUSTOMER_SEGMENT.getType();
        }
    
        protected int getBatchSize() {
            if (batchSize == null) {
                batchSize = systemPropertiesService.resolveIntSystemProperty("customerSegments.export.batchSize", 100);
            }
            return batchSize;
        }
    
        @Override
        public String getProcessType() {
            return PROCESS_TYPE;
        }
    }
    
  7. Lastly create a service that actually does the work of exporting which will be called by YourEventConsumer.export() method. For example

    @ConditionalOnExport
    @Service("blCustomerSegmentExportService")
    public class CustomerSegmentExportServiceImpl implements CustomerSegmentExportService {
    
        @Resource(name = "blCustomerSegmentService")
        protected CustomerSegmentService customerSegmentService;
    
        @Autowired
        protected ExportWriterProvider exportWriterProvider;
    
        @Autowired
        protected ApplicationContext applicationContext;
    
        @Override
        public void exportAllCustomerSegmentsCsv(OutputStream output, int batchSize) throws IOException {
            boolean isFirst = true;
            Long firstId = Long.MIN_VALUE;
            do {
                firstId = exportAllBatchCustomerSegmentsCsv(firstId, batchSize, output, isFirst);
                isFirst = false;
            } while (firstId != null);
        }
    
        @Override 
        public Long exportAllBatchCustomerSegmentsCsv(Long firstId, int count, OutputStream output, boolean useHeaders) throws IOException {
            List<CustomerCustomerSegmentXref> xrefs = customerSegmentService.findBatchCustomerCustomerSegXrefs(firstId, count);
            return exportCustomerSegments(xrefs, count, output, useHeaders);
        }
    
        @Override
        public Long exportBatchCustomerSegmentsCsv(Long customerSegmentId, Long firstId, int count, OutputStream output, boolean useHeaders) throws IOException {
            List<CustomerCustomerSegmentXref> xrefs = customerSegmentService.findBatchXrefsForCustomerSegment(customerSegmentId, firstId, count);
            return exportCustomerSegments(xrefs, count, output, useHeaders);
        }
    
        protected Long exportCustomerSegments(List<CustomerCustomerSegmentXref> xrefs, int count, OutputStream output, boolean useHeaders) throws JsonGenerationException, JsonMappingException, IOException {
            Class<?> dtoClass = applicationContext.getBean(CustomerSegmentDTO.class.getName(), CustomerSegmentDTO.class).getClass();
            ObjectWriter writer = exportWriterProvider.getCsvWriter(useHeaders, dtoClass);
            List<CustomerSegmentDTO> currentBatch = new ArrayList<>();
            for (CustomerCustomerSegmentXref custXref : xrefs) {
                CustomerSegmentDTO dto = applicationContext.getBean(CustomerSegmentDTO.class.getName(), CustomerSegmentDTO.class);
                dto.wrap(custXref);
                currentBatch.add(dto);
            }
            customerSegmentService.clear();
            writer.writeValue(output, currentBatch);
    
            // Return next ID to be passed in for next batch call or null to indicate last page
            if (CollectionUtils.size(currentBatch) == count) {
                return currentBatch.get(currentBatch.size() - 1).getId() + 1L;
            } else {
                return null;
            }
        }
    }
    

If you would wish for your export to support additional formats or encodings then it's advised to create classes extending SupportedExportEncoding and/or SupportedExportType.