1
votes

I have a directory that is being read by an inbound file adapter which is piped into a priority channel that sorts the files by their name. I've created a transaction synchronization factory for moving the files after processing is done which works fine for the inbound adapter and all the transformations/aggregations that are happening in an additional file writer flow. As soon as I add the PriorityChannel, the transaction seems to be finished and it's not being passed to the transformation/aggregration logic.

Here is the inbound flow

return IntegrationFlows
                .from(fileReadingMessageSource,
                        c -> c.poller(Pollers.fixedDelay(period)
                                             .taskExecutor(taskExecutor)
                                             .maxMessagesPerPoll(maxMessagesPerPoll)))
                                             .transactionSynchronizationFactory(transactionSynchronizationFactory())
                                             .transactional(transactionManager())))
                .channel("alphabetically")
                .bridge(s -> s.poller(Pollers.fixedDelay(100)))
                .channel(ApplicationConfiguration.INBOUND_CHANNEL)
                .get();

And the transaction synchronization strategy

@Bean
    TransactionSynchronizationFactory transactionSynchronizationFactory() {
        ExpressionParser parser = new SpelExpressionParser();
        ExpressionEvaluatingTransactionSynchronizationProcessor syncProcessor = new ExpressionEvaluatingTransactionSynchronizationProcessor();
        syncProcessor.setBeanFactory(applicationContext.getAutowireCapableBeanFactory());
        syncProcessor.setAfterCommitExpression(parser.parseExpression(
                "payload.renameTo(new java.io.File(@inboundProcessedDirectory.path " + " + T(java.io.File).separator + payload.name))"));
        syncProcessor.setAfterRollbackExpression(parser.parseExpression(
                "payload.renameTo(new java.io.File(@inboundFailedDirectory.path " + " + T(java.io.File).separator + payload.name))"));
        return new DefaultTransactionSynchronizationFactory(syncProcessor);
    }

Any idea how to span this transaction in combination with the priority queue channel? Or is there any other way that I could implement reading of files in an alphabetical order?

EDIT1

According to Gary, this should work (providing whole example as asked):

@Configuration
class FilePollingIntegrationFlow {

    @Autowired
    public File inboundReadDirectory;

    @Autowired
    private ApplicationContext applicationContext;

    @Bean
    public IntegrationFlow inboundFileIntegration(@Value("${inbound.file.poller.fixed.delay}") long period,
                                                  @Value("${inbound.file.poller.max.messages.per.poll}") int maxMessagesPerPoll, TaskExecutor taskExecutor,
                                                  MessageSource<File> fileReadingMessageSource) {
        return IntegrationFlows
                .from(fileReadingMessageSource,
                        c -> c.poller(Pollers.fixedDelay(period)
                                             .taskExecutor(taskExecutor)
                                             .maxMessagesPerPoll(maxMessagesPerPoll)
                                             .transactionSynchronizationFactory(transactionSynchronizationFactory())
                                             .transactional(transactionManager())))
                .channel(ApplicationConfiguration.INBOUND_CHANNEL)
                .get();
    }

    @Bean
    TaskExecutor taskExecutor(@Value("${inbound.file.poller.thread.pool.size}") int poolSize) {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(poolSize);
        return taskExecutor;
    }

    @Bean
    PseudoTransactionManager transactionManager() {
        return new PseudoTransactionManager();
    }

    @Bean
    TransactionSynchronizationFactory transactionSynchronizationFactory() {
        ExpressionParser parser = new SpelExpressionParser();
        ExpressionEvaluatingTransactionSynchronizationProcessor syncProcessor = new ExpressionEvaluatingTransactionSynchronizationProcessor();
        syncProcessor.setBeanFactory(applicationContext.getAutowireCapableBeanFactory());
        syncProcessor.setAfterCommitExpression(parser.parseExpression(
                "payload.renameTo(new java.io.File(@inboundProcessedDirectory.path " + " + T(java.io.File).separator + payload.name))"));
        syncProcessor.setAfterRollbackExpression(parser.parseExpression(
                "payload.renameTo(new java.io.File(@inboundFailedDirectory.path " + " + T(java.io.File).separator + payload.name))"));
        return new DefaultTransactionSynchronizationFactory(syncProcessor);
    }

    @Bean
    public FileReadingMessageSource fileReadingMessageSource(DirectoryScanner directoryScanner) {
        FileReadingMessageSource source = new FileReadingMessageSource();
        source.setDirectory(this.inboundReadDirectory);
        source.setScanner(directoryScanner);
        source.setAutoCreateDirectory(true);
        return source;
    }

    @Bean
    public DirectoryScanner directoryScanner(@Value("${inbound.filename.regex}") String regex) {
        DirectoryScanner scanner = new RecursiveDirectoryScanner();
        CompositeFileListFilter<File> filter = new CompositeFileListFilter<>(
                Arrays.asList(new AcceptOnceFileListFilter<>(), new RegexPatternFileListFilter(regex), new AlphabeticalFileListFilter()));
        scanner.setFilter(filter);
        return scanner;
    }

    private class AlphabeticalFileListFilter implements FileListFilter<File> {
        @Override
        public List<File> filterFiles(File[] files) {
            List<File> list = Arrays.asList(files);
            list.sort(Comparator.comparing(File::getName));
            return list;
        }
    }
}

@Configuration
public class FilePollingConfiguration {

    @Bean(name="inboundReadDirectory")
    public File inboundReadDirectory(@Value("${inbound.read.path}") String path) {
        return makeDirectory(path);
    }

    @Bean(name="inboundProcessedDirectory")
    public File inboundProcessedDirectory(@Value("${inbound.processed.path}") String path) {
        return makeDirectory(path);
    }

    @Bean(name="inboundFailedDirectory")
    public File inboundFailedDirectory(@Value("${inbound.failed.path}") String path) {
        return makeDirectory(path);
    }

    @Bean(name="inboundOutDirectory")
    public File inboundOutDirectory(@Value("${inbound.out.path}") String path) {
        return makeDirectory(path);
    }

    private File makeDirectory(String path) {
        File file = new File(path);
        file.mkdirs();
        return file;
    }

}

By doing this and removing the PriorityChannel, it still seems that the transaction isn't working as I would thought. Using this flow, the file is not available in the Http outbound gateway. Any idea why?

@Component
public class MessageProcessingIntegrationFlow {

    public static final String OUTBOUND_FILENAME_GENERATOR = "outboundFilenameGenerator.handler";
    public static final String FILE_WRITING_MESSAGE_HANDLER = "fileWritingMessageHandler";
    @Autowired
    public File inboundOutDirectory;

    @Bean
    public IntegrationFlow writeToFile(@Value("${api.base.uri}") URI uri,
                                       @Value("${out.filename.dateFormat}") String dateFormat, @Value("${out.filename.suffix}") String filenameSuffix) {
        return IntegrationFlows.from(ApplicationConfiguration.INBOUND_CHANNEL)
                               .enrichHeaders(h -> h.headerFunction(IntegrationMessageHeaderAccessor.CORRELATION_ID, m -> ((String) m
                                       .getHeaders()
                                       .get(FileHeaders.FILENAME)).substring(0, 17)))
                               .aggregate(a -> a.groupTimeout(2000)
                                                .sendPartialResultOnExpiry(true))
                               .transform(m -> {
                                   MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
                                   //noinspection unchecked
                                   ((List<File>) m).forEach(f -> body.add("documents", new FileSystemResource((File) f)));
                                   return body;
                               })
                               .handle(Http.outboundGateway(uri)
                                           .httpMethod(HttpMethod.POST)
                                           .expectedResponseType(byte[].class))
                               .handle(Files.outboundGateway(inboundOutDirectory)
                                            .autoCreateDirectory(true)
                                            .fileNameGenerator(
                                                    m -> m.getHeaders()
                                                          .get(FileHeaders.FILENAME) + "_" + DateTimeFormatter.ofPattern(dateFormat)
                                                                                                              .format(LocalDateTime
                                                                                                                      .now()) + filenameSuffix))
                               .log(LoggingHandler.Level.INFO)
                               .get();
    }
}
2

2 Answers

2
votes

You cannot switch threads with Spring transactions; the transaction is bound to the thread.

You can use a custom FileListFilter in the message source instead and sort the files there.

0
votes

Thanks to Gary Russel I came up with the following solution:

@Bean
    public IntegrationFlow inboundFileIntegration(@Value("${inbound.file.poller.fixed.delay}") long period,
                                                  @Value("${inbound.file.poller.max.messages.per.poll}") int maxMessagesPerPoll,
                                                  @Value("${inbound.file.poller.thread.pool.size}") int poolSize,
                                                  MessageSource<File> fileReadingMessageSource) {
        return IntegrationFlows
                .from(fileReadingMessageSource,
                        c -> c.poller(Pollers.fixedDelay(period)
                                             .taskExecutor(Executors.newFixedThreadPool(poolSize))
                                             .maxMessagesPerPoll(maxMessagesPerPoll)))
                .channel("alphabetically")
                .bridge(s -> s.poller(Pollers.fixedDelay(100)))
                .channel(ApplicationConfiguration.INBOUND_CHANNEL)
                .get();
    }

Advices with spec:

@Bean
    public Advice fileMoveAdvice() {
        ExpressionEvaluatingRequestHandlerAdvice advice = new ExpressionEvaluatingRequestHandlerAdvice();
        advice.setOnSuccessExpression(new FunctionExpression<Message<?>>(m -> renameMultiValueMapFiles(m, this.inboundProcessedDirectory)));
        advice.setOnFailureExpression(new FunctionExpression<Message<?>>(m -> renameMultiValueMapFiles(m, this.inboundFailedDirectory)));
        return advice;
    }

    @Bean
    public Consumer<GenericEndpointSpec<HttpRequestExecutingMessageHandler>> outboundSpec() {
        return new Consumer<GenericEndpointSpec<HttpRequestExecutingMessageHandler>>() {
            @Override
            public void accept(GenericEndpointSpec<HttpRequestExecutingMessageHandler> spec) {
                spec.advice(fileMoveAdvice(), retryAdvice());
            }
        };
    }

@SneakyThrows(IOException.class)
    private boolean renameMultiValueMapFiles(Message<?> m, File directory) {
        MultiValueMap<String, Resource> files = (MultiValueMap<String, Resource>) m.getPayload();
        List<File> list = new ArrayList<>();
        // no lambda to avoid ThrowsFunction type
        for (List<Resource> l : files.values()) {
            for (Resource v : l) {
                list.add(v.getFile());
            }
        }
        list.forEach(v -> v.renameTo(new File(directory.getPath(), v.getName())));
        return true;
    }

Added spec to handle:

.handle(Http.outboundGateway(uri)
                            .httpMethod(HttpMethod.POST)
                            .expectedResponseType(byte[].class), this.advices.outboundSpec())