
package de.ivu.elect.jee.exchange;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import javax.xml.bind.*;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.Source;
import javax.xml.transform.sax.SAXSource;
import javax.xml.validation.Schema;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;

import static java.util.Collections.emptyMap;

/**
 * Util-Klasse für das JAXB (Un-)Marshalling
 */
public class JAXBUtil {

  private static final Logger LOG = LoggerFactory.getLogger(JAXBUtil.class);

  public JAXBUtil() {
    super();
  }

  public void marshal(Object jaxbElement, OutputStream os) throws JAXBException {
    marshal(jaxbElement, os, emptyMap());
  }

  public void marshal(Object jaxbElement, OutputStream os, JAXBContext jaxbContext) throws JAXBException {
    marshal(jaxbElement, os, jaxbContext, emptyMap());
  }

  public void marshal(Object jaxbElement, OutputStream os, Map<String, Object> marshallerProperties) throws JAXBException {
    marshal(jaxbElement, os, lookupOrCreateJAXBContext(jaxbElement.getClass()), marshallerProperties);
  }

  public void marshal(Object jaxbElement, OutputStream os, Map<String, Object> marshallerProperties, Class<?>... classesToBoBound) throws JAXBException {
    marshal(jaxbElement, os, createJAXBContext(classesToBoBound), marshallerProperties);
  }

  public void marshal(JAXBElement jaxbElementWrapped, Object jaxbElement, OutputStream os, Map<String, Object> marshallerProperties) throws JAXBException {
    marshal(jaxbElementWrapped, os, lookupOrCreateJAXBContext(jaxbElement.getClass()), marshallerProperties);
  }

  public void marshal(Object jaxbElement, OutputStream os, JAXBContext context, Map<String, Object> marshallerProperties) throws JAXBException {
    Marshaller marshaller = context.createMarshaller();
    marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);

    for (Entry<String, Object> entry : marshallerProperties.entrySet()) {
      if (entry.getKey() != null) {
        marshaller.setProperty(entry.getKey(), entry.getValue());
      }
    }

    marshaller.marshal(jaxbElement, os);
  }

  public <T> T unmarshal(Class<T> targetClass, InputStream is) throws JAXBException, ParserConfigurationException, SAXException {
    return unmarshal(targetClass, is, emptyMap());
  }

  @SuppressWarnings("unchecked")
  public <T> T unmarshal(Class<T> targetClass, InputStream is, Map<String, Object> unmarshallerProperties) throws JAXBException, ParserConfigurationException, SAXException {
    return (T) unmarshal(is, createUnmarshaller(targetClass, unmarshallerProperties));
  }

  @SuppressWarnings("unchecked")
  public <T> T validateAndUnmarshal(Class<T> targetClass, InputStream is, Schema schema, ValidationEventHandler validationHandler, Map<String, Object> unmarshallerProperties)
      throws JAXBException, ParserConfigurationException, SAXException {
    Unmarshaller unmarshaller = createUnmarshaller(targetClass, unmarshallerProperties);

    // NOTE, dass der Validator das Unmarshalling per Exception abbricht, wenn das Schema verletzt wird.
    unmarshaller.setSchema(schema);
    unmarshaller.setEventHandler(validationHandler);

    return (T) unmarshal(is, unmarshaller);
  }

  private <T> Unmarshaller createUnmarshaller(Class<T> targetClass, Map<String, Object> unmarshallerProperties) throws JAXBException {
    JAXBContext context = lookupOrCreateJAXBContext(targetClass);

    Unmarshaller unmarshaller = context.createUnmarshaller();

    for (Entry<String, Object> entry : unmarshallerProperties.entrySet()) {
      if (entry.getKey() != null) {
        unmarshaller.setProperty(entry.getKey(), entry.getValue());
      }
    }

    return unmarshaller;
  }

  private static final Map<Class<?>, JAXBContext> JAXB_CONTEXT_CACHE = new ConcurrentHashMap<>();

  private static JAXBContext lookupOrCreateJAXBContext(Class<?> clazz) throws JAXBException {
    if (!JAXB_CONTEXT_CACHE.containsKey(clazz)) {
      JAXB_CONTEXT_CACHE.put(clazz, JAXBContext.newInstance(clazz));
    }
    return JAXB_CONTEXT_CACHE.get(clazz);
  }

  private static JAXBContext createJAXBContext(Class<?>... clazz) throws JAXBException {
    return JAXBContext.newInstance(clazz);
  }

  protected Object unmarshal(InputStream is, Unmarshaller unmarshaller) throws JAXBException, SAXException, ParserConfigurationException {
    Source xmlSource = new SAXSource(prepareParserFactory().newSAXParser().getXMLReader(), new InputSource(is));
    return unmarshaller.unmarshal(xmlSource);
  }

  public static SAXParserFactory prepareParserFactory() {
    //    // ELECT-5566
    SAXParserFactory spf = SAXParserFactory.newInstance();
    spf.setNamespaceAware(true);
    // there is no point in asking a validation because
    // there is no guarantee that the document will come with
    // a proper schemaLocation.
    spf.setValidating(false);
    try {
      spf.setFeature("http://xml.org/sax/features/external-general-entities", false);
    } catch (Exception e) {
      LOG.warn("Feature not supported: " + "http://xml.org/sax/features/external-general-entities");
      // feature not supported
    }
    try {
      spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
    } catch (Exception e) {
      LOG.warn("Feature not supported: " + "http://xml.org/sax/features/external-parameter-entities");
      // feature not supported
    }
    try {
      spf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
    } catch (Exception e) {
      LOG.warn("Feature not supported: " + "http://apache.org/xml/features/nonvalidating/load-external-dtd");
      // feature not supported
    }
    try {
      spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
    } catch (Exception e) {
      LOG.warn("Feature not supported: " + "http://apache.org/xml/features/disallow-doctype-decl");
      // feature not supported
    }
    return spf;
  }
}