javareactive-programmingreactive-streamsmutinysmallrye

How to convert a simple method that returns the List<String> into Multi<String> based on Smallrye Mutiny?


I am developing an application that reads the XML file and creates the Hash ID based on the details present in XML. As of now, everything is working perfectly, and able to get the List<String>.

I would like to convert this application into Reactive Streams using the Smallrye Mutiny so I went through some of the documentation but did not understand clearly how to convert this application into Reactive Streams where I do not have to wait for the completion of all XML file to return the List<String>. Rather I can start returning the Multi<String> as and when the its generated.

Following is the simple XML that I am reading using SAX Parser to create the Hash ID:

<customerList>
    <customer>
        <name>Batman</name>
        <age>25</age>
    </customer>
    <customer>
        <name>Superman</name>
        <age>28</age>
    </customer>
</customerList>

Following is the Main application which will make a call to SaxHandler:

public Multi<String> xmlEventHashGenerator(final InputStream xmlStream) throws SAXException, ParserConfigurationException, IOException {
    final SAXParserFactory factory = SAXParserFactory.newInstance();
    final SaxHandler saxHandler = new SaxHandler();
    factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
    factory.newSAXParser().parse(xmlStream, saxHandler);

    return Multi.createFrom().emitter(em ->{
        saxHandler.getRootNodes().forEach(contextNode -> {
            final String preHashString = contextNode.toString();
            try {
                final StringBuilder hashId = new StringBuilder();
                MessageDigest.getInstance("SHA-256").digest(preHashString.getBytes(StandardCharsets.UTF_8));
                hashId.append(DatatypeConverter.printHexBinary(digest).toLowerCase());
                em.emit(hashId.toString());
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            }
        });
        em.complete();
    });
}

Following is the SaxHandler which will read the XML and create HashIDs:

public class SaxHandler extends DefaultHandler {
    @Getter
    private final List<String> eventHashIds = new ArrayList<>();
    @Getter
    private final List<ContextNode> rootNodes = new ArrayList<>();

    private final HashMap<String, String> contextHeader = new HashMap<>();
    private final String hashAlgorithm;
    private ContextNode currentNode = null;
    private ContextNode rootNode = null;
    private final StringBuilder currentValue = new StringBuilder();

    public SaxHandler(final String hashAlgorithm) {
        this.hashAlgorithm = hashAlgorithm;
    }

    @Override
    public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) {
        if (rootNode == null && qName.equals("customer")) {
            rootNode = new ContextNode(contextHeader);
            currentNode = rootNode;
            rootNode.children.add(new ContextNode(rootNode, "type", qName));
        }else if (currentNode != null) {
            ContextNode n = new ContextNode(currentNode, qName, (String) null);
            currentNode.children.add(n);
            currentNode = n;
        }
    }

    @Override
    public void characters(char[] ch, int start, int length) {
        currentValue.append(ch, start, length);
    }

    @Override
    public void endElement(final String uri, final String localName, final String qName) {

        if (rootNode != null && !qName.equals("customer")) {
            final String value = !currentValue.toString().trim().equals("") ? currentValue.toString().trim() : null;
            currentNode.children.add(new ContextNode(currentNode, qName, value));
        }

        if (qName.equals("customer")) {
            rootNodes.add(rootNode);
            rootNode = null;
        }
        currentValue.setLength(0);
    }
}

Following is the Test:

@Test
    public void xmlTest() throws Exception {
        final HashGenerator eventHashGenerator = new HashGenerator();
        final InputStream xmlStream = getClass().getResourceAsStream("/customer.xml");
        final List<String> eventHashIds = eventHashGenerator.xmlHashGenerator(xmlStream, "sha3-256");
        System.out.println("\nGenerated Event Hash Ids : \n" + eventHashIds);
    }

Can someone please guide me to some example or provide some idea on how to convert this application into SmallRye Mutinty Multi<String> based application?


Solution

  • I think you can refactor xmlEventHashGenerator to

        public Multi<String> xmlEventHashGenerator(final InputStream xmlStream) throws SAXException, ParserConfigurationException, IOException {
            final SAXParserFactory factory = SAXParserFactory.newInstance();
            final SaxHandler saxHandler = new SaxHandler();
            factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
            factory.newSAXParser().parse(xmlStream, saxHandler);
    
            return Multi.createFrom()
                    .iterable( saxHandler.getRootNodes() )
                    .map( RootNode::toString )
                    .map( this::convertDatatype );
        }
    
        private String convertDatatype(String preHashString) {
            try {
                // I think we could create the MessageDigest instance only once
                byte[] digest = MessageDigest.getInstance( "SHA-256" )
                        .digest( preHashString.getBytes( StandardCharsets.UTF_8 ) );
                return DatatypeConverter.printHexBinary( digest ).toLowerCase();
            }
            catch (NoSuchAlgorithmException e) {
                throw new IllegalArgumentException( e );
            }
        }
    

    The test method will look something like:

        @Test
        public void xmlTest() throws Exception {
            final HashGenerator eventHashGenerator = new HashGenerator();
            final InputStream xmlStream = getClass().getResourceAsStream("/customer.xml");
            System.out.println("Generated Event Hash Ids: ");
            eventHashGenerator
                .xmlHashGenerator(xmlStream)
                // Print all the hash codes
                .invoke( hash -> System.out.println( hash )
                .await().indefinitely();
         }
    

    But if you want to concatenate all the hash codes, you can do:

        @Test
        public void xmlTest() throws Exception {
            final HashGenerator eventHashGenerator = new HashGenerator();
            final InputStream xmlStream = getClass()
                .getResourceAsStream("/customer.xml");
            String hash = eventHashGenerator
                .xmlHashGenerator(xmlStream)
                // Concatenate all the results
                .collect().with( Collectors.joining() );
                // Print the hashcode
                .invoke( hashcode -> System.out.println("\nGenerated Event Hash Ids : \n" + hashcode) )
                .await().indefinitely();
         }