XMLUtil.java
/*
* This file is part of Indicators.
*
* Indicators is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Indicators is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Indicators. If not, see <https://www.gnu.org/licenses/>.
*/
package fr.inrae.agroclim.indicators.xml;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Source;
import org.w3c.dom.ls.LSInput;
import org.w3c.dom.ls.LSResourceResolver;
import org.xml.sax.SAXException;
import fr.inrae.agroclim.indicators.exception.IndicatorsException;
import fr.inrae.agroclim.indicators.exception.type.XmlErrorType;
import fr.inrae.agroclim.indicators.model.EvaluationSettings;
import fr.inrae.agroclim.indicators.model.Knowledge;
import jakarta.xml.bind.JAXBException;
import jakarta.xml.bind.Marshaller;
import jakarta.xml.bind.Unmarshaller;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.log4j.Log4j2;
/**
* Helper to serialize/deserialize EvaluationSettings.
*
* @author jcufi
*/
@Log4j2
public abstract class XMLUtil {
/**
* Public ID of doctype for an evaluation.
*/
private static final String DTD_PUBLIC_ID_EVALUATION = "-//INRAE AgroClim.//DTD Evaluation 1.1//EN";
/**
* Public ID of doctype for knowledge.
*/
private static final String DTD_PUBLIC_ID_KNOWLEDGE = "-//INRAE AgroClim.//DTD Knowledge 1.1//EN";
/**
* DOCTYPE header for an evaluation.
*/
private static final String DOCTYPE_EVALUATION = "<!DOCTYPE evaluationSettings PUBLIC\n"
+ " \"" + DTD_PUBLIC_ID_EVALUATION + "\"\n"
+ " \"https://agroclim.inrae.fr/getari/dtd/1.1/evaluation.dtd\">";
/**
* DOCTYPE header for Knowledge.
*/
private static final String DOCTYPE_KNOWLEDGE = "<!DOCTYPE knowledge PUBLIC\n"
+ " \"" + DTD_PUBLIC_ID_KNOWLEDGE + "\"\n"
+ " \"https://agroclim.inrae.fr/getari/dtd/1.1/knowledge.dtd\">";
/**
* DTD resolver for jakarta.xml.validation.Validator.
*/
public static final class CustomLSResourceResolver
implements LSResourceResolver {
@Override
public LSInput resolveResource(final String type,
final String namespaceURI, final String publicId,
final String systemId, final String baseURI) {
InputStream input = null;
final Map<String, InputStream> dtds = getDtds();
if (dtds.containsKey(publicId)) {
input = dtds.get(publicId);
}
return new CustomLSInput(systemId, publicId, baseURI, input);
}
}
/**
* DTD resource for resolver for CustomLSResourceResolver.
*/
private static class CustomLSInput implements LSInput {
/**
* The system identifier, a URI reference [IETF RFC 2396], for this
* input source.
*/
@Getter
@Setter
private String systemId;
/**
* The public identifier for this input source.
*/
@Getter
@Setter
private String publicId;
/**
* The base URI to be used (see section 5.1.4 in [IETF RFC 2396]) for
* resolving a relative systemId to an absolute URI.
*/
@Getter
@Setter
private String baseURI;
/**
* DTD stream.
*/
private final InputStream input;
/**
* Constructor.
*
* @param inputSystemId
* The system identifier, a URI reference [IETF RFC 2396],
* for this input source.
* @param inputPublicId
* The public identifier for this input source.
* @param inputBaseURI
* The base URI to be used (see section 5.1.4 in [IETF RFC
* 2396]) for resolving a relative systemId to an absolute
* URI.
* @param inputStream
* DTD stream.
*/
CustomLSInput(final String inputSystemId, final String inputPublicId,
final String inputBaseURI, final InputStream inputStream) {
this.systemId = inputSystemId;
this.publicId = inputPublicId;
this.baseURI = inputBaseURI;
this.input = inputStream;
}
@Override
public Reader getCharacterStream() {
return null;
}
@Override
public void setCharacterStream(final Reader characterStream) {
// do nothing
}
@Override
public InputStream getByteStream() {
return null;
}
@Override
public void setByteStream(final InputStream byteStream) {
// do nothing
}
@Override
public String getStringData() {
if (this.input == null) {
return null;
}
try {
final byte[] data = new byte[this.input.available()];
if (this.input.read(data) > 0) {
return new String(data, StandardCharsets.UTF_8);
}
LOGGER.error("Failed to read data");
return null;
} catch (final IOException e) {
LOGGER.catching(e);
return null;
}
}
@Override
public void setStringData(final String stringData) {
// Do nothing
}
@Override
public String getEncoding() {
return null;
}
@Override
public void setEncoding(final String encoding) {
// Do nothing
}
@Override
public boolean getCertifiedText() {
return false;
}
@Override
public void setCertifiedText(final boolean certifiedText) {
// Do nothing
}
}
/**
* @return Public ID of doctype → Resource for the doctype file.
*/
public static Map<String, InputStream> getDtds() {
return Map.of(//
DTD_PUBLIC_ID_EVALUATION, getResourceAsStream("evaluation.dtd"), //
DTD_PUBLIC_ID_KNOWLEDGE, getResourceAsStream("knowledge.dtd"));
}
/**
* Finds a resource with a given name.
*
* @param filename relative name
* @return found resource
*/
private static InputStream getResourceAsStream(final String filename) {
return XMLUtil.class.getResourceAsStream("/fr/inrae/agroclim/indicators/" + filename);
}
/**
* Load from file.
*
* @param xmlFile
* XML file
* @param clazz
* used classes
* @return object for the XML file
* @throws IndicatorsException
* exception from JAXBException
*/
public static Object load(final File xmlFile, final Class<?>... clazz) throws IndicatorsException {
try (InputStream inputStream = new FileInputStream(xmlFile);) {
return loadResource(inputStream, clazz);
} catch (final FileNotFoundException ex) {
throw new IndicatorsException(XmlErrorType.FILE_NOT_FOUND, ex, xmlFile.getAbsolutePath());
} catch (final IOException ex) {
throw new IndicatorsException(XmlErrorType.UNABLE_TO_LOAD, ex);
}
}
/**
* Load from InputStream.
*
* @param inputStream
* XML file
* @param clazz
* used classes
* @return object for the XML file
* @throws IndicatorsException
* exception from JAXBException
*/
public static Object loadResource(final InputStream inputStream, final Class<?>... clazz)
throws IndicatorsException {
if (inputStream == null) {
throw new IndicatorsException(XmlErrorType.FILE_NOT_FOUND, "InputStream should not be null!");
}
try {
UnmarshallerBuilder builder = new UnmarshallerBuilder();
builder.setDtds(getDtds());
builder.setClassesToBeBound(clazz);
final Unmarshaller um = builder.build();
final Source source = builder.buildSource(inputStream);
return um.unmarshal(source);
} catch (JAXBException | ParserConfigurationException | SAXException ex) {
// details of exception are not shown using LOGGER.catching(ex)
final Writer buffer = new StringWriter();
final PrintWriter pw = new PrintWriter(buffer);
ex.printStackTrace(pw);
LOGGER.fatal("Unable to deserialize : {}", buffer);
throw new IndicatorsException(XmlErrorType.UNABLE_TO_LOAD, ex);
}
}
/**
* Serialize to OutputStream.
*
*
* @param o
* object to serialize
* @param outputStream
* output stream
* @param clazz
* used classes
* @throws IndicatorsException
* exception from JAXBException
*/
public static void serialize(final Object o, final OutputStream outputStream, final Class<?>... clazz)
throws IndicatorsException {
try (OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);) {
serialize(o, writer, clazz);
} catch (final IOException ex) {
throw new IndicatorsException(XmlErrorType.UNABLE_TO_SERIALIZE, ex,
"Unable to create OutputStreamWriter : " + o);
}
}
/**
* Serialize to file.
*
* @param o
* object to serialize
* @param fileName
* file path
* @param clazz
* used classes
* @throws IndicatorsException
* exception from JAXBException
*/
public static void serialize(final Object o, final String fileName, final Class<?>... clazz)
throws IndicatorsException {
try (FileOutputStream stream = new FileOutputStream(fileName);) {
serialize(o, new OutputStreamWriter(stream, StandardCharsets.UTF_8), clazz);
} catch (final IOException ex) {
LOGGER.catching(ex);
throw new IndicatorsException(XmlErrorType.UNABLE_TO_SERIALIZE, ex,
"Unable to create FileOutputStream : " + o);
}
}
/**
* Serialize to writer.
*
* @param o
* object to serialize
* @param writer
* writer
* @param clazz
* used classes
* @throws IndicatorsException
* exception from JAXBException
*/
private static void serialize(final Object o, final Writer writer, final Class<?>... clazz)
throws IndicatorsException {
try {
final MarshallerBuilder builder = new MarshallerBuilder();
if (o instanceof EvaluationSettings) {
builder.setDocType(DOCTYPE_EVALUATION);
} else if (o instanceof Knowledge) {
builder.setDocType(DOCTYPE_KNOWLEDGE);
}
builder.setClassesToBeBound(clazz);
final Marshaller marshaller = builder.build();
marshaller.marshal(o, writer);
} catch (final JAXBException ex) {
// details of exception are not shown using LOGGER.catching(ex)
final Writer buffer = new StringWriter();
final PrintWriter pw = new PrintWriter(buffer);
ex.printStackTrace(pw);
LOGGER.fatal("Unable to serialize : " + buffer.toString(), ex);
throw new IndicatorsException(XmlErrorType.UNABLE_TO_SERIALIZE, ex);
}
}
/**
* No constructor for utility class.
*/
private XMLUtil() {
}
}