Jugando con javascript, java 8 y spring reactor

Hace poco donde trabajo tenia un requerimiento de convertir un archivo xml con meta-data a json. para ello inicie con jackson, después con Gson pero en ambos casos en las pruebas que realice se perdía la meta-data del archivo xml y vaya que era mero problema.

Después de pensar varios minutos recordé que una vez me presenté una prueba donde me pidieron usar javascript para cargar java (en ese momento pensé eso se puede… waooo, que genial!!!), entonces me di la tarea de buscar una librería de javascript que lo hiciera correctamente, empece buscanco en el gestor de paquetes de javascript npm y encontré xml-js.js

https://www.npmjs.com/package/xml-js

Descargue e instale node.js, cree ejemplo de xml que está en la documentación de xml-js y ejecutelo, luego agreguele meta-data al xml para ver el comportamiento de pasar de xml a json y viceversa.

Requerimientos:

  1. Java 8
  2. Spring boot (2.1.1)
  3. Maven
  4. Nodejs
  5. npm para instalar las dependencias
  6. Xml-js.js y Sax (dependencia de xml-js)

Cree un proyecto en maven con spring boot usando https://starts.spring.io

Importar el proyecto en su IDE preferido en mi caso utilicé Intellij

Clase principal del proyecto

@SpringBootApplication
public class ExampleSpringConverterXmlJsonApplication {

public static void main(String[] args) {
SpringApplication.run(ExampleSpringConverterXmlJsonApplication.class, args);
}
}

Vamos a crear un Bean de tipo Service que será el encargado de realizar la conversion de xml a json y viceversa.

@Service
public class ConvertFormatServiceImpl implements ConvertFormatService {

protected static final String FUNCTION_NAME_XML2JSON = "xml2json";
protected static final String FUNCTION_NAME_JSON2XML = "json2xml";

@Override
public Mono<String> convertXmlToJson(Mono<String> xml) {
return xml.subscribeOn(Schedulers.elastic())
.flatMap(content-> NativeScriptConverter.getInstance().converter(FUNCTION_NAME_XML2JSON, content));
}

@Override
public Mono<String> convertJsonToXml(Mono<String> json) {
return json.subscribeOn(Schedulers.elastic())
.flatMap(content-> NativeScriptConverter.getInstance().converter(FUNCTION_NAME_JSON2XML, content));
}
}

Creamos una clase que encapsule toda la carga de la libreria de javascript y sus dependencias, esta clase es un implementación del patron singleton.

public class NativeScriptConverter {

private Invocable invocable;
private ClassLoader classLoader = getClass().getClassLoader();
protected static NativeScriptConverter nativeScriptConverter;
protected static final String SETTING_CONVERTER_XML2JSON = "{ignoreComment: true, alwaysChildren: true,compact: false, spaces: 2}";
protected static final String ENGINE_NAME = "nashorn";

public static NativeScriptConverter getInstance() {
try {
if (nativeScriptConverter == null) {
nativeScriptConverter = new NativeScriptConverter();
}
return nativeScriptConverter;
} catch (Exception e) {
log.error("{}", e);
throw new LoadScriptException(e.getMessage());
}
}

private NativeScriptConverter() throws IOException, ScriptException {
ScriptEngine engine = new ScriptEngineManager().getEngineByName(ENGINE_NAME);
loadGlobalVariables(engine);
loadDependencies(engine);
engine.eval(new FileReader(classLoader.getResource("static/js/xml-js.js").getFile()));
invocable = (Invocable) engine;
}

private void loadGlobalVariables(ScriptEngine engine) {
Bindings engineScope = engine.getBindings(ScriptContext.ENGINE_SCOPE);
engineScope.put("window", engineScope);
}

private void loadDependencies(ScriptEngine engine) throws IOException, ScriptException {
String dependencies[] = {"static/js/dependencies/sax.js"};
for (String dependency : dependencies) {
engine.eval(new FileReader(classLoader.getResource(dependency).getFile()));
}
}

public Mono<String> converter(@NonNull String function, @NonNull String contentToConverter) {
try {
return Mono.just((String) invocable.invokeFunction(function, contentToConverter, SETTING_CONVERTER_XML2JSON));
} catch (Exception e) {
log.error("{}", e);
throw new ConverterException(e.getMessage());
}
}
}

creada la parte de conversion del xml o el json, vamos de empezar con la creación de los controles para recibir las peticiones de las conversiones.

Creamos un Handler, su funcion será la comunicación con el bean service que creamos previamente.

public class ConvertHandler {

private final ConvertFormatService convertFormatService;

public ConvertHandler(ConvertFormatService convertFormatService) {
this.convertFormatService = convertFormatService;
}

public Mono<ServerResponse> getConvertXmlToJson(ServerRequest serverRequest) {
Mono<String> xmlRequest = serverRequest.bodyToMono(String.class);
Mono<String> responseJson = convertFormatService.convertXmlToJson(xmlRequest);
return responseJson.flatMap(rta -> ok()
.contentType(APPLICATION_JSON_UTF8)
.body(fromObject(rta)))
.switchIfEmpty(badRequest().build());
}

public Mono<ServerResponse> getConvertJsonToXml(ServerRequest serverRequest) {
Mono<String> requestJson = serverRequest.bodyToMono(String.class);
Mono<String> response = convertFormatService.convertJsonToXml(requestJson);
return response.flatMap(rta -> ok()
.contentType(APPLICATION_XML)
.body(fromObject(rta)))
.switchIfEmpty(badRequest().build());
}
}

ahora definimos los recursos (endpoints) para la invocación, para tal fin creamos en enrutador que inyecta el handler creado anteriormente.

@Configuration
public class ConverterRoute {

private final ConvertHandler convertHandler;

public ConverterRoute(ConvertFormatService convertFormatService) {
this.convertHandler = new ConvertHandler(convertFormatService);
}

@Bean
public RouterFunction<?> route() {
return nest(path("/v1/converts"),
RouterFunctions.route(POST("/xmlTojson").and(accept(MediaType.APPLICATION_XML).and(contentType(MediaType.APPLICATION_XML))), convertHandler::getConvertXmlToJson)
.filter(converterExceptionBadRequest())
.filter(loadScriptExceptionBadRequest())
.andOther(RouterFunctions.route(POST("/jsonToxml").and(accept(MediaType.APPLICATION_JSON_UTF8).and(contentType(MediaType.APPLICATION_JSON_UTF8))), convertHandler::getConvertJsonToXml)
.filter(converterExceptionBadRequest())
.filter(loadScriptExceptionBadRequest())));
}

private HandlerFilterFunction<ServerResponse, ServerResponse> converterExceptionBadRequest() {
return (request, next) -> next.handle(request)
.onErrorResume(ConverterException.class, e -> ServerResponse.badRequest().body(BodyInserters.fromObject(new ErrorMessage("", e.getMessage()))));
}

private HandlerFilterFunction<ServerResponse, ServerResponse> loadScriptExceptionBadRequest() {
return (request, next) -> next.handle(request)
.onErrorResume(LoadScriptException.class, e -> ServerResponse.status(HttpStatus.BAD_GATEWAY).body(BodyInserters.fromObject(new ErrorMessage("", e.getMessage()))));
}
}

Como vemos en el código, está construido de una manera diferente como lo hacemos tradicionalmente, ya que no encontramos un controller con anotaciones @RequestController, @PostMapping o un @RequestMapping, etc.

En vez, se usa RouterFunctions, ServerResponse, Mono y Flux, esta nueva forma de crear sistemas resilientes, responsivos, elasticos y dirigidos por mensajes, esto es lo que se conoce como sistemas reactivos. (manifiesto reactivo). el proyecto de reactor apoyado por spring ayuda a la creación de este tipo de sistemas.

Como último paso podemos ejecutarlo, para la invocacion de los servicios podemos utilizar Postman o cualquier otra herramienta.

XML -> JSON ejemplo simple

JSON->XML ejemplo sencillo

El código fuente lo pueden encontrar en GitHub.

Espero que les guste, es mi primer post.