content: /*
   * Licensed to the Apache Software Foundation (ASF) under one or more
   * contributor license agreements.  See the NOTICE file distributed with
   * this work for additional information regarding copyright ownership.
   * The ASF licenses this file to You under the Apache License, Version 2.0
   * (the "License"); you may not use this file except in compliance with
   * the License.  You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  package org.apache.camel.converter.jaxb;
  
  import java.io.FileNotFoundException;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.InputStreamReader;
  import java.io.OutputStream;
  import java.io.Reader;
  import java.lang.reflect.InvocationTargetException;
  import java.lang.reflect.Method;
  import java.net.MalformedURLException;
  import java.net.URL;
  import java.util.Map;
  import java.util.Map.Entry;
  
  import jakarta.xml.bind.JAXBContext;
  import jakarta.xml.bind.JAXBElement;
  import jakarta.xml.bind.JAXBException;
  import jakarta.xml.bind.JAXBIntrospector;
  import jakarta.xml.bind.MarshalException;
  import jakarta.xml.bind.Marshaller;
  import jakarta.xml.bind.PropertyException;
  import jakarta.xml.bind.Unmarshaller;
  import jakarta.xml.bind.ValidationEvent;
  
  import javax.xml.XMLConstants;
  import javax.xml.namespace.QName;
  import javax.xml.stream.XMLStreamReader;
  import javax.xml.stream.XMLStreamWriter;
  import javax.xml.transform.Source;
  import javax.xml.transform.stream.StreamSource;
  import javax.xml.validation.Schema;
  import javax.xml.validation.SchemaFactory;
  
  import org.xml.sax.SAXException;
  
  import org.apache.camel.CamelContext;
  import org.apache.camel.CamelContextAware;
  import org.apache.camel.Exchange;
  import org.apache.camel.ExchangePropertyKey;
  import org.apache.camel.InvalidPayloadException;
  import org.apache.camel.NoTypeConversionAvailableException;
  import org.apache.camel.TypeConverter;
  import org.apache.camel.spi.DataFormat;
  import org.apache.camel.spi.DataFormatContentTypeHeader;
  import org.apache.camel.spi.DataFormatName;
  import org.apache.camel.spi.annotations.Dataformat;
  import org.apache.camel.support.ResourceHelper;
  import org.apache.camel.support.service.ServiceSupport;
  import org.apache.camel.util.IOHelper;
  import org.apache.camel.util.ObjectHelper;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  /**
   * A <a href="http://camel.apache.org/data-format.html">data format</a> ({@link DataFormat}) using JAXB2 to marshal to
   * and from XML
   */
  @Dataformat("jaxb")
  public class JaxbDataFormat extends ServiceSupport
          implements DataFormat, DataFormatName, DataFormatContentTypeHeader, CamelContextAware {
  
      private static final Logger LOG = LoggerFactory.getLogger(JaxbDataFormat.class);
  
      private SchemaFactory schemaFactory;
      private CamelContext camelContext;
      private JAXBContext context;
      private JAXBIntrospector introspector;
      private String contextPath;
      private boolean contextPathIsClassName;
      private String schema;
      private int schemaSeverityLevel; // 0 = warning, 1 = error, 2 = fatal
      private String schemaLocation;
      private String noNamespaceSchemaLocation;
  
      private boolean prettyPrint = true;
      private boolean objectFactory = true;
      private boolean ignoreJAXBElement = true;
      private boolean mustBeJAXBElement;
      private boolean filterNonXmlChars;
      private String encoding;
      private boolean fragment;
      // partial support
      private QName partNamespace;
      private Class<?> partClass;
      private Map<String, String> namespacePrefix;
      private JaxbNamespacePrefixMapper namespacePrefixMapper;
      private JaxbXmlStreamWriterWrapper xmlStreamWriterWrapper;
      private TypeConverter typeConverter;
      private Schema cachedSchema;
      private Map<String, Object> jaxbProviderProperties;
      private boolean contentTypeHeader = true;
      private String accessExternalSchemaProtocols;
  
      public JaxbDataFormat() {
      }
  
      public JaxbDataFormat(JAXBContext context) {
          this.context = context;
      }
  
      public JaxbDataFormat(String contextPath) {
          this.contextPath = contextPath;
      }
  
      @Override
      public String getDataFormatName() {
          return "jaxb";
      }
  
      @Override
      public void marshal(Exchange exchange, Object graph, OutputStream stream) throws IOException {
          try {
              // must create a new instance of marshaller as its not thread safe
              Marshaller marshaller = createMarshaller();
  
              if (isPrettyPrint()) {
                  marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
              }
              // exchange take precedence over encoding option
              String charset = exchange.getProperty(ExchangePropertyKey.CHARSET_NAME, String.class);
              if (charset == null) {
                  charset = encoding;
                  //Propagate the encoding of the exchange
                  if (charset != null) {
                      exchange.setProperty(ExchangePropertyKey.CHARSET_NAME, charset);
                  }
              }
              if (charset != null) {
                  marshaller.setProperty(Marshaller.JAXB_ENCODING, charset);
              }
              if (isFragment()) {
                  marshaller.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE);
              }
              if (ObjectHelper.isNotEmpty(schemaLocation)) {
                  marshaller.setProperty(Marshaller.JAXB_SCHEMA_LOCATION, schemaLocation);
              }
              if (ObjectHelper.isNotEmpty(noNamespaceSchemaLocation)) {
                  marshaller.setProperty(Marshaller.JAXB_NO_NAMESPACE_SCHEMA_LOCATION, noNamespaceSchemaLocation);
              }
              if (namespacePrefixMapper != null) {
                  marshaller.setProperty(namespacePrefixMapper.getRegistrationKey(), namespacePrefixMapper);
              }
              // Inject any JAX-RI custom properties from the exchange or from the instance into the marshaller
              injectCustomProperties(exchange, marshaller);
              doMarshal(exchange, graph, stream, marshaller, charset);
  
              if (contentTypeHeader) {
                  exchange.getMessage().setHeader(Exchange.CONTENT_TYPE, "application/xml");
              }
          } catch (Exception e) {
              throw new IOException(e);
          }
      }
  
      private void injectCustomProperties(Exchange exchange, Marshaller marshaller) throws PropertyException {
          Map<String, Object> customProperties = exchange.getProperty(JaxbConstants.JAXB_PROVIDER_PROPERTIES, Map.class);
          if (customProperties == null) {
              customProperties = getJaxbProviderProperties();
          }
          if (customProperties != null) {
              for (Entry<String, Object> property : customProperties.entrySet()) {
                  if (LOG.isDebugEnabled()) {
                      LOG.debug("Using JAXB Provider Property {}={}", property.getKey(), property.getValue());
                  }
                  marshaller.setProperty(property.getKey(), property.getValue());
              }
          }
      }
  
      void doMarshal(Exchange exchange, Object graph, OutputStream stream, Marshaller marshaller, String charset)
              throws Exception {
  
          Object element = graph;
          QName partNamespaceOnDataFormat = getPartNamespace();
          String partClassFromHeader = exchange.getIn().getHeader(JaxbConstants.JAXB_PART_CLASS, String.class);
          String partNamespaceFromHeader = exchange.getIn().getHeader(JaxbConstants.JAXB_PART_NAMESPACE, String.class);
          if ((partClass != null || partClassFromHeader != null)
                  && (partNamespaceOnDataFormat != null || partNamespaceFromHeader != null)) {
              element = toElement(graph, partClassFromHeader, partNamespaceFromHeader, partNamespaceOnDataFormat);
          }
  
          // only marshal if its possible
          if (introspector.isElement(element)) {
              tryMarshal(exchange, stream, marshaller, charset, element);
              return;
          } else if (objectFactory && element != null) {
              if (tryFromFactory(exchange, stream, marshaller, charset, element)) {
                  return;
              }
          }
  
          // cannot marshal
          if (!mustBeJAXBElement) {
              // write the graph as is to the output stream
              writeGraph(exchange, graph, stream);
          } else {
              throw new InvalidPayloadException(exchange, JAXBElement.class);
          }
      }
  
      private boolean tryFromFactory(
              Exchange exchange, OutputStream stream, Marshaller marshaller, String charset, Object element)
              throws IllegalAccessException, InvocationTargetException, JAXBException, InstantiationException {
          Method objectFactoryMethod = JaxbHelper.getJaxbElementFactoryMethod(camelContext, element.getClass());
          if (objectFactoryMethod != null) {
              try {
                  tryMarshallFromInstance(exchange, stream, marshaller, charset, objectFactoryMethod, element);
                  return true;
              } catch (Exception e) {
                  // if a schema is set then an MarshallException is thrown when the XML is not valid
                  // and the method must throw this exception as it would when the object in the body is a root element
                  // or a partial class (the other alternatives above)
                  //
                  // it would be best to completely remove the exception handler here but it's left for backwards compatibility reasons.
                  if (MarshalException.class.isAssignableFrom(e.getClass()) && schema != null) {
                      throw e;
                  }
  
                  LOG.debug("Unable to create JAXBElement object for type {} due to {}", element.getClass(),
                          e.getMessage(), e);
              }
          }
          return false;
      }
  
      private static void writeGraph(Exchange exchange, Object graph, OutputStream stream)
              throws NoTypeConversionAvailableException, IOException {
          if (LOG.isDebugEnabled()) {
              LOG.debug("Attempt to marshalling non JAXBElement with type {} as InputStream",
                      ObjectHelper.classCanonicalName(graph));
          }
          InputStream is = exchange.getContext().getTypeConverter().mandatoryConvertTo(InputStream.class, exchange, graph);
          IOHelper.copyAndCloseInput(is, stream);
      }
  
      private void tryMarshallFromInstance(
              Exchange exchange, OutputStream stream, Marshaller marshaller, String charset, Method objectFactoryMethod,
              Object element)
              throws IllegalAccessException, InvocationTargetException, JAXBException, InstantiationException {
          Object instance = objectFactoryMethod.getDeclaringClass().newInstance();
          Object toMarshall = objectFactoryMethod.invoke(instance, element);
          if (asXmlStreamWriter(exchange)) {
              XMLStreamWriter writer = typeConverter.convertTo(XMLStreamWriter.class, exchange, stream);
              if (needFiltering(exchange)) {
                  writer = new FilteringXmlStreamWriter(writer, charset);
              }
              if (xmlStreamWriterWrapper != null) {
                  writer = xmlStreamWriterWrapper.wrapWriter(writer);
              }
              marshaller.marshal(toMarshall, writer);
          } else {
              marshaller.marshal(toMarshall, stream);
          }
          return;
      }
  
      private void tryMarshal(Exchange exchange, OutputStream stream, Marshaller marshaller, String charset, Object element)
              throws JAXBException {
          if (asXmlStreamWriter(exchange)) {
              XMLStreamWriter writer = typeConverter.convertTo(XMLStreamWriter.class, exchange, stream);
              if (needFiltering(exchange)) {
                  writer = new FilteringXmlStreamWriter(writer, charset);
              }
              if (xmlStreamWriterWrapper != null) {
                  writer = xmlStreamWriterWrapper.wrapWriter(writer);
              }
              marshaller.marshal(element, writer);
          } else {
              marshaller.marshal(element, stream);
          }
      }
  
      private Object toElement(
              Object graph, String partClassFromHeader, String partNamespaceFromHeader, QName partNamespaceOnDataFormat)
              throws JAXBException {
          if (partClassFromHeader != null) {
              try {
                  partClass = camelContext.getClassResolver().resolveMandatoryClass(partClassFromHeader, Object.class);
              } catch (ClassNotFoundException e) {
                  throw new JAXBException(e);
              }
          }
          if (partNamespaceFromHeader != null) {
              partNamespaceOnDataFormat = QName.valueOf(partNamespaceFromHeader);
          }
          return new JAXBElement<>(partNamespaceOnDataFormat, (Class<Object>) partClass, graph);
      }
  
      private boolean asXmlStreamWriter(Exchange exchange) {
          return needFiltering(exchange) || xmlStreamWriterWrapper != null;
      }
  
      @Override
      public Object unmarshal(Exchange exchange, InputStream stream) throws Exception {
          return unmarshal(exchange, (Object) stream);
      }
  
      @Override
      public Object unmarshal(Exchange exchange, Object body) throws Exception {
          try {
              Object answer;
  
              XMLStreamReader xmlReader;
              if (needFiltering(exchange)) {
                  xmlReader
                          = typeConverter.convertTo(XMLStreamReader.class, exchange, createNonXmlFilterReader(exchange, body));
              } else {
                  xmlReader = typeConverter.tryConvertTo(XMLStreamReader.class, exchange, body);
                  if (xmlReader == null) {
                      // fallback to input stream
                      InputStream is = getCamelContext().getTypeConverter().mandatoryConvertTo(InputStream.class, exchange, body);
                      xmlReader = typeConverter.convertTo(XMLStreamReader.class, exchange, is);
                  }
              }
              String partClassFromHeader = exchange.getIn().getHeader(JaxbConstants.JAXB_PART_CLASS, String.class);
              if (partClass != null || partClassFromHeader != null) {
                  // partial unmarshalling
                  if (partClassFromHeader != null) {
                      try {
                          partClass = camelContext.getClassResolver().resolveMandatoryClass(partClassFromHeader, Object.class);
                      } catch (ClassNotFoundException e) {
                          throw new JAXBException(e);
                      }
                  }
                  answer = createUnmarshaller().unmarshal(xmlReader, partClass);
              } else {
                  answer = createUnmarshaller().unmarshal(xmlReader);
              }
  
              if (answer instanceof JAXBElement && isIgnoreJAXBElement()) {
                  answer = ((JAXBElement<?>) answer).getValue();
              }
              return answer;
          } catch (JAXBException e) {
              throw new IOException(e);
          }
      }
  
      private NonXmlFilterReader createNonXmlFilterReader(Exchange exchange, Object body)
              throws NoTypeConversionAvailableException {
          Reader reader = getCamelContext().getTypeConverter().tryConvertTo(Reader.class, exchange, body);
          if (reader == null) {
              // fallback to input stream
              InputStream is = getCamelContext().getTypeConverter().mandatoryConvertTo(InputStream.class, exchange, body);
              reader = new InputStreamReader(is);
          }
          return new NonXmlFilterReader(reader);
      }
  
      protected boolean needFiltering(Exchange exchange) {
          // exchange property takes precedence over data format property
          return exchange == null
                  ? filterNonXmlChars : exchange.getProperty(Exchange.FILTER_NON_XML_CHARS, filterNonXmlChars, Boolean.class);
      }
  
      // Properties
      // -------------------------------------------------------------------------
      public boolean isIgnoreJAXBElement() {
          return ignoreJAXBElement;
      }
  
      public void setIgnoreJAXBElement(boolean flag) {
          ignoreJAXBElement = flag;
      }
  
      public boolean isMustBeJAXBElement() {
          return mustBeJAXBElement;
      }
  
      public void setMustBeJAXBElement(boolean mustBeJAXBElement) {
          this.mustBeJAXBElement = mustBeJAXBElement;
      }
  
      public JAXBContext getContext() {
          return context;
      }
  
      public void setContext(JAXBContext context) {
          this.context = context;
      }
  
      public String getContextPath() {
          return contextPath;
      }
  
      public void setContextPath(String contextPath) {
          this.contextPath = contextPath;
      }
  
      public boolean isContextPathIsClassName() {
          return contextPathIsClassName;
      }
  
      public void setContextPathIsClassName(boolean contextPathIsClassName) {
          this.contextPathIsClassName = contextPathIsClassName;
      }
  
      public SchemaFactory getSchemaFactory() throws SAXException {
          return schemaFactory;
      }
  
      public void setSchemaFactory(SchemaFactory schemaFactory) {
          this.schemaFactory = schemaFactory;
      }
  
      public String getSchema() {
          return schema;
      }
  
      public void setSchema(String schema) {
          this.schema = schema;
      }
  
      public int getSchemaSeverityLevel() {
          return schemaSeverityLevel;
      }
  
      public void setSchemaSeverityLevel(int schemaSeverityLevel) {
          this.schemaSeverityLevel = schemaSeverityLevel;
      }
  
      public boolean isPrettyPrint() {
          return prettyPrint;
      }
  
      public void setPrettyPrint(boolean prettyPrint) {
          this.prettyPrint = prettyPrint;
      }
  
      public boolean isObjectFactory() {
          return objectFactory;
      }
  
      public void setObjectFactory(boolean objectFactory) {
          this.objectFactory = objectFactory;
      }
  
      public boolean isFragment() {
          return fragment;
      }
  
      public void setFragment(boolean fragment) {
          this.fragment = fragment;
      }
  
      public boolean isFilterNonXmlChars() {
          return filterNonXmlChars;
      }
  
      public void setFilterNonXmlChars(boolean filterNonXmlChars) {
          this.filterNonXmlChars = filterNonXmlChars;
      }
  
      public String getEncoding() {
          return encoding;
      }
  
      public void setEncoding(String encoding) {
          this.encoding = encoding;
      }
  
      public QName getPartNamespace() {
          return partNamespace;
      }
  
      public void setPartNamespace(QName partNamespace) {
          this.partNamespace = partNamespace;
      }
  
      public Class<?> getPartClass() {
          return partClass;
      }
  
      public void setPartClass(Class<?> partClass) {
          this.partClass = partClass;
      }
  
      public Map<String, String> getNamespacePrefix() {
          return namespacePrefix;
      }
  
      public void setNamespacePrefix(Map<String, String> namespacePrefix) {
          this.namespacePrefix = namespacePrefix;
      }
  
      @Override
      public CamelContext getCamelContext() {
          return camelContext;
      }
  
      @Override
      public void setCamelContext(CamelContext camelContext) {
          this.camelContext = camelContext;
      }
  
      public JaxbXmlStreamWriterWrapper getXmlStreamWriterWrapper() {
          return xmlStreamWriterWrapper;
      }
  
      public void setXmlStreamWriterWrapper(JaxbXmlStreamWriterWrapper xmlStreamWriterWrapper) {
          this.xmlStreamWriterWrapper = xmlStreamWriterWrapper;
      }
  
      public String getSchemaLocation() {
          return schemaLocation;
      }
  
      public void setSchemaLocation(String schemaLocation) {
          this.schemaLocation = schemaLocation;
      }
  
      public String getNoNamespaceSchemaLocation() {
          return noNamespaceSchemaLocation;
      }
  
      public void setNoNamespaceSchemaLocation(String schemaLocation) {
          this.noNamespaceSchemaLocation = schemaLocation;
      }
  
      public Map<String, Object> getJaxbProviderProperties() {
          return jaxbProviderProperties;
      }
  
      public void setJaxbProviderProperties(Map<String, Object> jaxbProviderProperties) {
          this.jaxbProviderProperties = jaxbProviderProperties;
      }
  
      public boolean isContentTypeHeader() {
          return contentTypeHeader;
      }
  
      /**
       * If enabled then JAXB will set the Content-Type header to <tt>application/xml</tt> when marshalling.
       */
      public void setContentTypeHeader(boolean contentTypeHeader) {
          this.contentTypeHeader = contentTypeHeader;
      }
  
      public String getAccessExternalSchemaProtocols() {
          return accessExternalSchemaProtocols;
      }
  
      public void setAccessExternalSchemaProtocols(String accessExternalSchemaProtocols) {
          this.accessExternalSchemaProtocols = accessExternalSchemaProtocols;
      }
  
      @Override
      @SuppressWarnings("unchecked")
      protected void doStart() throws Exception {
          ObjectHelper.notNull(camelContext, "CamelContext");
  
          if (context == null) {
              // if context not injected, create one and resolve partial class up front so they are ready to be used
              context = createContext();
          }
          introspector = context.createJAXBIntrospector();
  
          if (namespacePrefix != null) {
              namespacePrefixMapper = NamespacePrefixMapperFactory.newNamespacePrefixMapper(camelContext, namespacePrefix);
          }
          typeConverter = camelContext.getTypeConverter();
          if (schema != null) {
              cachedSchema = createSchema(getSources());
          }
  
          LOG.debug("JaxbDataFormat [prettyPrint={}, objectFactory={}]", prettyPrint, objectFactory);
      }
  
      @Override
      protected void doStop() throws Exception {
      }
  
      /**
       * Strategy to create JAXB context
       */
      protected JAXBContext createContext() throws Exception {
          if (contextPath != null) {
              // prefer to use application class loader which is most likely to be able to
              // load the class which has been JAXB annotated
              ClassLoader cl = camelContext.getApplicationContextClassLoader();
              if (cl != null) {
                  if (contextPathIsClassName) {
                      LOG.debug("Creating JAXBContext with className: {} and ApplicationContextClassLoader: {}",
                              contextPath, cl);
                      Class clazz = camelContext.getClassResolver().resolveMandatoryClass(contextPath, cl);
                      return JAXBContext.newInstance(clazz);
                  } else {
                      LOG.debug("Creating JAXBContext with contextPath: {} and ApplicationContextClassLoader: {}",
                              contextPath, cl);
                      return JAXBContext.newInstance(contextPath, cl);
                  }
              } else {
                  if (contextPathIsClassName) {
                      LOG.debug("Creating JAXBContext with className: {}", contextPath);
                      Class clazz = camelContext.getClassResolver().resolveMandatoryClass(contextPath);
                      return JAXBContext.newInstance(clazz);
                  } else {
                      LOG.debug("Creating JAXBContext with contextPath: {}", contextPath);
                      return JAXBContext.newInstance(contextPath);
                  }
              }
          } else {
              LOG.debug("Creating JAXBContext");
              return JAXBContext.newInstance();
          }
      }
  
      protected Unmarshaller createUnmarshaller() throws JAXBException {
          Unmarshaller unmarshaller = getContext().createUnmarshaller();
          if (schema != null) {
              unmarshaller.setSchema(cachedSchema);
              unmarshaller.setEventHandler((ValidationEvent event) -> {
                  // continue if the severity is lower than the configured level
                  return event.getSeverity() < getSchemaSeverityLevel();
              });
          }
  
          return unmarshaller;
      }
  
      protected Marshaller createMarshaller() throws JAXBException {
          Marshaller marshaller = getContext().createMarshaller();
          if (schema != null) {
              marshaller.setSchema(cachedSchema);
              marshaller.setEventHandler((ValidationEvent event) -> {
                  // continue if the severity is lower than the configured level
                  return event.getSeverity() < getSchemaSeverityLevel();
              });
          }
  
          return marshaller;
      }
  
      private Schema createSchema(Source[] sources) throws SAXException {
          SchemaFactory factory = createSchemaFactory(accessExternalSchemaProtocols);
          return factory.newSchema(sources);
      }
  
      private Source[] getSources() throws FileNotFoundException, MalformedURLException {
          // we support multiple schema by delimiting by comma
          String[] schemas = schema.split(",");
          Source[] sources = new Source[schemas.length];
          for (int i = 0; i < schemas.length; i++) {
              URL schemaUrl = ResourceHelper.resolveMandatoryResourceAsUrl(camelContext, schemas[i]);
              sources[i] = new StreamSource(schemaUrl.toExternalForm());
          }
          return sources;
      }
  
      private static SchemaFactory createSchemaFactory(String protocols) throws SAXException {
          SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
          factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
          factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
          if (protocols == null || "false".equals(protocols) || "none".equals(protocols)) {
              protocols = "";
              LOG.debug("Configuring SchemaFactory to not allow access to external DTD/Schema");
          } else {
              LOG.debug("Configuring SchemaFactory to allow access to external DTD/Schema using protocols: {}", protocols);
          }
          factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, protocols);
          factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, protocols);
          return factory;
      }
  
  }
  