2
\$\begingroup\$

Purpose of code

The goal of this code is to:

  1. Initiate a message propagation to other systems
  2. Validate that the given message adheres to its XSD schema, and then marshall it
  3. Send it on a Camel route when it is validated and marshalled

This code is located on a node that is the lowest in its hierarchy. It is intended to propagate different kinds of message formats upwards to other nodes. To make sure that each node is able to handle the incoming data format, an XSD is used to facilitate marshalling/unmarshalling and validation of data. The XSDs are used to generate POJOs in the projects build step, and these POJOs are located in a shared module that each node has access to. Upon receiving a message on a Camel route, the receiving system will then unmarshall the message and validate its content.

Background

I want to implement a uniform data format and easily share data between interested parties by simply giving them access to the data format (POJOs). I want to ensure that the data sent adheres to the schemas that it is generated from. This is a part in an overhaul of a system that had huge XML messages sent back and forth where only a fraction of the data was used. Removing any part of those messages was highly volatile since it was next to impossible to tell if and where the data was used.

The code

I would like some input on the solution I've been working on recently. I am aware that it violates SRP in PropagationEventListener, but it is a work in progress and apart from that I'd like any input, mostly concering OOP and the SOLID principles.

The project is built in Spring.

The chain starts here, where I issue an event that is intended to propagated outside the current system:

private void publishEvent(InventoryUpdate inventoryUpdate) {
    context.getPublisher().publishEvent(
            new PropagationEvent(new InventoryUpdateWrapper(inventoryUpdate), getInventoryUpdateEndpoint()));
}

The InventoryUpdateWrapper that I send on its way:

public class InventoryUpdateWrapper extends AbstractXsdValidationObject {

    public InventoryUpdateWrapper(Object sourceObject) {
        super(sourceObject);
    }

    @Override
    public String getXsdSchemaPath() {
        return "xsd/InventoryUpdate.xsd";
    }
}

The PropagationEvent class:

public class PropagationEvent extends ApplicationEvent {

    private static final long serialVersionUID = 1851493071586088471L;

    private String targetCamelEndpoint;

    public PropagationEvent(AbstractXsdValidationObject source, final String targetCamelEndpoint) {
        super(source);
        this.targetCamelEndpoint = targetCamelEndpoint;
    }

    public String getTargetCamelEndpoint() {
        return targetCamelEndpoint;
    }
}

The AbstractXsdValidationObject class:

public abstract class AbstractXsdValidationObject implements XsdValidationObject {

    Object sourceObject;

    public AbstractXsdValidationObject(Object sourceObject) {
        this.sourceObject = sourceObject;
    }

    @Override
    public abstract String getXsdSchemaPath();

    @Override
    public Object getSourceObject() {
        return sourceObject;
    }

    public Schema getXsdSchema() throws SAXException {
        File xsdSchemaFile = getXsdSchemaAsFile();
        return SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema(xsdSchemaFile);
    }

    private File getXsdSchemaAsFile() {
        return new File(getPathFromResource());
    }

    private String getPathFromResource() {
        return getClass().getClassLoader().getResource(getXsdSchemaPath()).getFile();
    }

}

And finally, the PropagationEventListener class where it is all sewn together:

public class PropagationEventListener implements ApplicationListener<PropagationEvent> {

    @Autowired
    private JAXBContext jaxbContext;

    private ProducerTemplate producer;

    public PropagationEventListener(final ProducerTemplate producer) {
        this.producer = producer;
    }

    @Override
    public void onApplicationEvent(PropagationEvent event) {
        String messageBody = validateAndMarshall(event);
        producer.sendBody(event.getTargetCamelEndpoint(), messageBody);
    }

    private String validateAndMarshall(PropagationEvent event) {
        AbstractXsdValidationObject source = (AbstractXsdValidationObject) event.getSource();
        Schema schema = getSchema(source);

        Marshaller jaxbMarshaller = createMarshaller(schema);

        return marshallToString(source.getSourceObject(), jaxbMarshaller);
    }

    private Schema getSchema(AbstractXsdValidationObject source) {
        try {
            return source.getXsdSchema();
        } catch (SAXException e) {
            throw new XmlException("Failed to create and XSD schema instance from " + source.getXsdSchemaPath(), e);
        } 
    }

    private Marshaller createMarshaller(Schema schema) {
        try {
            Marshaller jaxbMarshaller = jaxbContext.createMarshaller();
            jaxbMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
            jaxbMarshaller.setSchema(schema);
            return jaxbMarshaller;
        } catch (JAXBException e) {
            throw new XmlException("Failed to create marshaller", e);
        }
    }

    private String marshallToString(Object event, Marshaller jaxbMarshaller) {
        try {
            StringWriter stringWriter = new StringWriter();
            jaxbMarshaller.marshal(event, stringWriter);
            return stringWriter.toString();
        } catch (JAXBException e) {
            throw new XmlException("Failed to marshal XML to String", e);
        }
    }
}

Some thoughts of my own

By using an abstract class to wrap objects that are created from XSD schemas, I want to make it hard to send anything that is not backed by schema to other systems and also adhere to the Dependency Inversion Principle. I want to adhere to the Open/Closed Principle by making the PropagationEventListener unaware of any details. It just knows that it will be able to handle and validate the incoming object and then send it on its way to the given route. I want to easily be able to add any other data format as the system grows.

When errors occur, I've opted to create my own exception that extends RuntimeException. My thinking here is that there is no way to mitigate the error of validation / marshalling if it has gotten this far. I simply want to abort the sequence and get the error in the logs. I only allow one constructor on XmlException to always get the root cause.

I am planning to make the marshalling/unmarshalling/validation into its own class that utilizies the Singleton JAXBContext, and then I think I'm done.

Have I interpreted the principles correctly?

\$\endgroup\$
  • \$\begingroup\$ If you can provide a main() that shows how you expect these classes to be created and used, that would really help reviewers to help you. Lots of us like to build, modify and test the code to show our proposed changes. \$\endgroup\$ – Toby Speight Feb 20 '18 at 8:54
  • \$\begingroup\$ @TobySpeight do you want a main that is able to run the entire chain or simply show how to initiate it and how classes involved are created? I am not sure how to do the former this since a lot of plumbing is supplied by Spring and I can't share the entire project. \$\endgroup\$ – hochas Feb 20 '18 at 9:09
  • \$\begingroup\$ Well, I'm unlikely to be reviewing this myself, as I don't do Java much any more. In general, the more help you can give reviewers to experiment with your code, the better, For example, if you provide the unit tests, that's really helpful. It's not a hard-and-fast rule, just a hint that what you can provide will be helpful; if it's too painful to give a complete working program, then I'm sure that will be understood! \$\endgroup\$ – Toby Speight Feb 20 '18 at 9:46
  • \$\begingroup\$ Thank you Toby, I will try to get some running examples into the post. \$\endgroup\$ – hochas Feb 20 '18 at 10:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.