All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.helger.jaxb.GenericJAXBMarshaller Maven / Gradle / Ivy

There is a newer version: 11.1.8
Show newest version
/*
 * Copyright (C) 2014-2024 Philip Helger (www.helger.com)
 * philip[at]helger[dot]com
 *
 * Licensed 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 com.helger.jaxb;

import java.lang.ref.WeakReference;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.function.Function;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import javax.xml.namespace.NamespaceContext;
import javax.xml.namespace.QName;
import javax.xml.validation.Schema;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.helpers.DefaultHandler;

import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.Nonempty;
import com.helger.commons.annotation.OverrideOnDemand;
import com.helger.commons.annotation.ReturnsMutableCopy;
import com.helger.commons.annotation.ReturnsMutableObject;
import com.helger.commons.callback.CallbackList;
import com.helger.commons.callback.exception.IExceptionCallback;
import com.helger.commons.collection.impl.CommonsArrayList;
import com.helger.commons.collection.impl.ICommonsList;
import com.helger.commons.error.list.ErrorList;
import com.helger.commons.io.resource.ClassPathResource;
import com.helger.commons.lang.IHasClassLoader;
import com.helger.commons.state.ESuccess;
import com.helger.commons.string.StringHelper;
import com.helger.commons.string.ToStringGenerator;
import com.helger.jaxb.builder.JAXBBuilderDefaultSettings;
import com.helger.jaxb.validation.IValidationEventHandlerFactory;
import com.helger.jaxb.validation.WrappedCollectingValidationEventHandler;
import com.helger.xml.schema.XMLSchemaCache;

import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.JAXBElement;
import jakarta.xml.bind.JAXBException;
import jakarta.xml.bind.Marshaller;
import jakarta.xml.bind.Unmarshaller;
import jakarta.xml.bind.ValidationEventHandler;

/**
 * This is the generic reader and writer base class for JAXB enabled document
 * types.
 *
 * @author Philip Helger
 * @param 
 *        The JAXB type to be marshaled
 */
@NotThreadSafe
public class GenericJAXBMarshaller  implements
                                   IHasClassLoader,
                                   IJAXBReader ,
                                   IJAXBWriter ,
                                   IJAXBValidator 
{
  private static final Logger LOGGER = LoggerFactory.getLogger (GenericJAXBMarshaller.class);

  private final Class  m_aType;
  private final ICommonsList  m_aXSDs = new CommonsArrayList <> ();
  private final Function > m_aJAXBElementWrapper;
  private ValidationEventHandler m_aEventHandler = JAXBBuilderDefaultSettings.getDefaultValidationEventHandler ();
  private boolean m_bFormattedOutput = JAXBBuilderDefaultSettings.isDefaultFormattedOutput ();
  private NamespaceContext m_aNSContext = JAXBBuilderDefaultSettings.getDefaultNamespaceContext ();
  private Charset m_aCharset = JAXBBuilderDefaultSettings.getDefaultCharset ();
  private String m_sIndentString = JAXBBuilderDefaultSettings.getDefaultIndentString ();
  private boolean m_bUseSchema = JAXBBuilderDefaultSettings.isDefaultUseSchema ();
  private String m_sSchemaLocation = JAXBBuilderDefaultSettings.getDefaultSchemaLocation ();
  private String m_sNoNamespaceSchemaLocation = JAXBBuilderDefaultSettings.getDefaultNoNamespaceSchemaLocation ();
  private boolean m_bUseContextCache = JAXBBuilderDefaultSettings.isDefaultUseContextCache ();
  private WeakReference  m_aClassLoader;
  private final CallbackList > m_aReadExceptionCallbacks = new CallbackList <> ();
  private final CallbackList > m_aWriteExceptionCallbacks = new CallbackList <> ();

  /**
   * Constructor without XSD paths.
   *
   * @param aType
   *        The class of the JAXB document implementation type. May not be
   *        null.
   * @param aQName
   *        The qualified name in which the object should be wrapped. May not be
   *        null.
   * @since 9.1.5
   * @see #createSimpleJAXBElement(QName, Class)
   */
  public GenericJAXBMarshaller (@Nonnull final Class  aType, @Nonnull final QName aQName)
  {
    this (aType, createSimpleJAXBElement (aQName, aType));
  }

  /**
   * Constructor without XSD paths.
   *
   * @param aType
   *        The class of the JAXB document implementation type. May not be
   *        null.
   * @param aWrapper
   *        Wrap the passed domain object into a {@link JAXBElement} for
   *        marshalling (writing). This can usually be done using the
   *        respective's package ObjectFactory implementation. May not be
   *        null.
   */
  public GenericJAXBMarshaller (@Nonnull final Class  aType,
                                @Nonnull final Function > aWrapper)
  {
    this (aType, null, aWrapper);
  }

  /**
   * Constructor with XSD paths.
   *
   * @param aType
   *        The class of the JAXB document implementation type. May not be
   *        null.
   * @param aXSDs
   *        The XSDs used to validate document. May be null or
   *        empty indicating, that no XSD check is needed.
   * @param aJAXBElementWrapper
   *        Wrap the passed domain object into a {@link JAXBElement} for
   *        marshalling (writing). This can usually be done using the
   *        respective's package ObjectFactory implementation. May not be
   *        null.
   */
  public GenericJAXBMarshaller (@Nonnull final Class  aType,
                                @Nullable final List  aXSDs,
                                @Nonnull final Function > aJAXBElementWrapper)
  {
    m_aType = ValueEnforcer.notNull (aType, "Type");
    if (aXSDs != null)
    {
      ValueEnforcer.notEmptyNoNullValue (aXSDs, "XSDs");
      m_aXSDs.addAll (aXSDs);
      for (final ClassPathResource aRes : m_aXSDs)
        ValueEnforcer.isTrue (aRes.hasClassLoader (),
                              () -> "ClassPathResource " + aRes + " should define its ClassLoader for OSGI handling!");
    }

    m_aJAXBElementWrapper = ValueEnforcer.notNull (aJAXBElementWrapper, "JAXBElementWrapper");
    // By default this class loader of the type to be marshaled should be used
    // This is important for OSGI application containers and ANT tasks
    m_aClassLoader = new WeakReference <> (aType.getClassLoader ());
    m_aReadExceptionCallbacks.add (LoggingJAXBReadExceptionHandler.INSTANCE);
    m_aWriteExceptionCallbacks.add (LoggingJAXBWriteExceptionHandler.INSTANCE);
  }

  /**
   * @return The type as passed in the constructor. Never null.
   * @since v9.4.2
   */
  @Nonnull
  protected final Class  getType ()
  {
    return m_aType;
  }

  @Nullable
  public final ClassLoader getClassLoader ()
  {
    return m_aClassLoader.get ();
  }

  /**
   * @return The special JAXB validation event handler to be used. By default
   *         {@link JAXBBuilderDefaultSettings#getDefaultValidationEventHandler()}
   *         is used.
   */
  @Nullable
  public final ValidationEventHandler getValidationEventHandler ()
  {
    return m_aEventHandler;
  }

  /**
   * Set the JAXB validation event handler to be used. May be null.
   *
   * @param aEventHandler
   *        The event handler to be used. May be null.
   * @return this for chaining
   */
  @Nonnull
  public final GenericJAXBMarshaller  setValidationEventHandler (@Nullable final ValidationEventHandler aEventHandler)
  {
    m_aEventHandler = aEventHandler;
    return this;
  }

  /**
   * Set a factory to be used to create {@link ValidationEventHandler} objects.
   * By default none is present. Since v11 this is deprecated, because it was
   * too complicated to use.
   *
   * @param aVEHFactory
   *        The new factory to be used. May be null.
   * @return this for chaining
   * @deprecated Set the validation event handler manually. Use
   *             {@link #setValidationEventHandler(ValidationEventHandler)}
   *             instead.
   */
  @Nonnull
  @Deprecated (forRemoval = true, since = "11.0.0")
  public final GenericJAXBMarshaller  setValidationEventHandlerFactory (@Nullable final IValidationEventHandlerFactory aVEHFactory)
  {
    return setValidationEventHandler (aVEHFactory == null ? null : aVEHFactory.apply (m_aEventHandler));
  }

  /**
   * Special overload of
   * {@link #setValidationEventHandler(ValidationEventHandler)} for the easy
   * version of just collecting the errors and additionally invoking the old
   * validation handler.
   *
   * @param aErrorList
   *        The error list to fill. May not be null.
   * @return this for chaining
   * @since 11.0.0
   */
  @Nonnull
  public final GenericJAXBMarshaller  setCollectErrors (@Nonnull final ErrorList aErrorList)
  {
    ValueEnforcer.notNull (aErrorList, "ErrorList");

    return setValidationEventHandler (new WrappedCollectingValidationEventHandler (aErrorList));
  }

  @Nullable
  public final NamespaceContext getNamespaceContext ()
  {
    return m_aNSContext;
  }

  /**
   * Set the namespace context (prefix to namespace URL mapping) to be used.
   *
   * @param aNSContext
   *        The namespace context to be used. May be null.
   * @return this for chaining
   * @since 8.5.3
   */
  @Nonnull
  public final GenericJAXBMarshaller  setNamespaceContext (@Nullable final NamespaceContext aNSContext)
  {
    m_aNSContext = aNSContext;
    return this;
  }

  public final boolean isFormattedOutput ()
  {
    return m_bFormattedOutput;
  }

  /**
   * Change the way formatting happens when calling write.
   *
   * @param bWriteFormatted
   *        true to write formatted output.
   * @return this for chaining
   * @since 8.5.3
   */
  @Nonnull
  public final GenericJAXBMarshaller  setFormattedOutput (final boolean bWriteFormatted)
  {
    m_bFormattedOutput = bWriteFormatted;
    return this;
  }

  @Nullable
  public final Charset getCharset ()
  {
    return m_aCharset;
  }

  /**
   * Set the charset to be used for writing JAXB objects.
   *
   * @param aCharset
   *        The charset to be used by default. May be null.
   * @return this for chaining
   * @since 8.5.3
   */
  @Nonnull
  public final GenericJAXBMarshaller  setCharset (@Nullable final Charset aCharset)
  {
    m_aCharset = aCharset;
    return this;
  }

  @Nullable
  public final String getIndentString ()
  {
    return m_sIndentString;
  }

  /**
   * Set the indent string to be used for writing JAXB objects.
   *
   * @param sIndentString
   *        The indent string to be used. May be null.
   * @return this for chaining
   * @since 8.5.3
   */
  @Nonnull
  public final GenericJAXBMarshaller  setIndentString (@Nullable final String sIndentString)
  {
    m_sIndentString = sIndentString;
    return this;
  }

  public final boolean isUseSchema ()
  {
    return m_bUseSchema;
  }

  /**
   * Enable or disable the usage of an eventually configured XML Schema.
   *
   * @param bUseSchema
   *        true to use an XML Schema, false to not
   *        use it.
   * @return this for chaining
   * @since 11.0.3
   */
  @Nonnull
  public final GenericJAXBMarshaller  setUseSchema (final boolean bUseSchema)
  {
    m_bUseSchema = bUseSchema;
    return this;
  }

  @Nullable
  public final String getSchemaLocation ()
  {
    return m_sSchemaLocation;
  }

  /**
   * Set the schema location to be used for writing JAXB objects.
   *
   * @param sSchemaLocation
   *        The schema location to be used. May be null.
   * @return this for chaining
   * @since 8.6.0
   */
  @Nonnull
  public final GenericJAXBMarshaller  setSchemaLocation (@Nullable final String sSchemaLocation)
  {
    m_sSchemaLocation = sSchemaLocation;
    return this;
  }

  @Nullable
  public final String getNoNamespaceSchemaLocation ()
  {
    return m_sNoNamespaceSchemaLocation;
  }

  /**
   * Set the no namespace schema location to be used for writing JAXB objects.
   *
   * @param sNoNamespaceSchemaLocation
   *        The no namespace schema location to be used. May be
   *        null.
   * @return this for chaining
   * @since 9.0.0
   */
  @Nonnull
  public final GenericJAXBMarshaller  setNoNamespaceSchemaLocation (@Nullable final String sNoNamespaceSchemaLocation)
  {
    m_sNoNamespaceSchemaLocation = sNoNamespaceSchemaLocation;
    return this;
  }

  public final boolean isUseContextCache ()
  {
    return m_bUseContextCache;
  }

  /**
   * Change whether the context cache should be used or not. Since creating the
   * JAXB context is quite cost intensive it is recommended to leave it enabled.
   *
   * @param bUseContextCache
   *        true to use it (default), false if not.
   * @return this for chaining
   */
  @Nonnull
  public final GenericJAXBMarshaller  setUseContextCache (final boolean bUseContextCache)
  {
    m_bUseContextCache = bUseContextCache;
    return this;
  }

  /**
   * @return Read exception callbacks. Never null.
   * @since 9.2.2
   */
  @Nonnull
  @ReturnsMutableObject
  public final CallbackList > readExceptionCallbacks ()
  {
    return m_aReadExceptionCallbacks;
  }

  /**
   * @return Write exception callbacks. Never null.
   * @since 9.2.2
   */
  @Nonnull
  @ReturnsMutableObject
  public final CallbackList > writeExceptionCallbacks ()
  {
    return m_aWriteExceptionCallbacks;
  }

  /**
   * @return A list of all XSD resources used for validation. Never
   *         null but maybe empty.
   */
  @Nonnull
  @Nonempty
  @ReturnsMutableCopy
  public final ICommonsList  getOriginalXSDs ()
  {
    return m_aXSDs.getClone ();
  }

  /**
   * @return The validation schema to be used. May be null
   *         indicating that no validation is required.
   * @see #isUseSchema()
   * @see #getOriginalXSDs()
   */
  @Nullable
  @OverrideOnDemand
  protected Schema createValidationSchema ()
  {
    // Schema use enabled?
    if (!m_bUseSchema)
      return null;

    // Any XSD provided?
    if (m_aXSDs.isEmpty ())
      return null;

    return XMLSchemaCache.getInstanceOfClassLoader (getClassLoader ()).getSchema (m_aXSDs);
  }

  /**
   * Create the JAXBContext - cached or uncached.
   *
   * @param aClassLoader
   *        The class loader to be used for XML schema resolving. May be
   *        null.
   * @return The created JAXBContext and never null.
   * @throws JAXBException
   *         In case creation fails
   * @since 9.4.2
   */
  @Nonnull
  @OverrideOnDemand
  protected JAXBContext getJAXBContext (@Nullable final ClassLoader aClassLoader) throws JAXBException
  {
    final Package aPackage = m_aType.getPackage ();
    if (m_bUseContextCache)
      return JAXBContextCache.getInstance ()
                             .getFromCache (JAXBContextCacheKey.createForPackage (aPackage, aClassLoader));
    return JAXBContext.newInstance (aPackage.getName (), aClassLoader);
  }

  /**
   * @param aClassLoader
   *        The class loader to be used for XML schema resolving. May be
   *        null.
   * @return The JAXB unmarshaller to use. Never null.
   * @throws JAXBException
   *         In case the creation fails.
   */
  @Nonnull
  private Unmarshaller _createUnmarshaller (@Nullable final ClassLoader aClassLoader) throws JAXBException
  {
    final JAXBContext aJAXBContext = getJAXBContext (aClassLoader);

    // create an Unmarshaller
    final Unmarshaller aUnmarshaller = aJAXBContext.createUnmarshaller ();
    if (m_aEventHandler != null)
      aUnmarshaller.setEventHandler (m_aEventHandler);

    // Set XSD (if any)
    final Schema aValidationSchema = createValidationSchema ();
    if (aValidationSchema != null)
      aUnmarshaller.setSchema (aValidationSchema);

    return aUnmarshaller;
  }

  /**
   * Customize the passed unmarshaller before unmarshalling (reading) something.
   *
   * @param aUnmarshaller
   *        The object to customize. Never null.
   */
  @OverrideOnDemand
  protected void customizeUnmarshaller (@Nonnull final Unmarshaller aUnmarshaller)
  {
    // empty
  }

  @Nullable
  public final JAXBTYPE read (@Nonnull final IJAXBUnmarshaller  aHandler)
  {
    ValueEnforcer.notNull (aHandler, "Handler");

    try
    {
      final Unmarshaller aUnmarshaller = _createUnmarshaller (getClassLoader ());
      customizeUnmarshaller (aUnmarshaller);
      return aHandler.doUnmarshal (aUnmarshaller, m_aType).getValue ();
    }
    catch (final JAXBException ex)
    {
      m_aReadExceptionCallbacks.forEach (x -> x.onException (ex));
    }
    return null;
  }

  /**
   * @param aClassLoader
   *        The class loader to be used for XML schema resolving. May be
   *        null.
   * @return A marshaller for converting document to XML. Never
   *         null.
   * @throws JAXBException
   *         In case of an error.
   */
  @Nonnull
  private Marshaller _createMarshaller (@Nullable final ClassLoader aClassLoader) throws JAXBException
  {
    final JAXBContext aJAXBContext = getJAXBContext (aClassLoader);

    // create an Unmarshaller
    final Marshaller aMarshaller = aJAXBContext.createMarshaller ();
    if (m_aEventHandler != null)
      aMarshaller.setEventHandler (m_aEventHandler);

    if (m_aNSContext != null)
      try
      {
        JAXBMarshallerHelper.setJakartaNamespacePrefixMapper (aMarshaller, m_aNSContext);
      }
      catch (final Exception | NoClassDefFoundError ex)
      {
        // Might be an IllegalArgumentException or a NoClassDefFoundError
        LOGGER.error ("Failed to set the namespace context " +
                      m_aNSContext +
                      ": " +
                      ex.getClass ().getName () +
                      " -- " +
                      ex.getMessage ());
      }

    JAXBMarshallerHelper.setFormattedOutput (aMarshaller, m_bFormattedOutput);

    if (m_aCharset != null)
      JAXBMarshallerHelper.setEncoding (aMarshaller, m_aCharset);

    if (m_sIndentString != null)
      JAXBMarshallerHelper.setJakartaIndentString (aMarshaller, m_sIndentString);

    if (m_sSchemaLocation != null)
      JAXBMarshallerHelper.setSchemaLocation (aMarshaller, m_sSchemaLocation);

    if (m_sNoNamespaceSchemaLocation != null)
      JAXBMarshallerHelper.setNoNamespaceSchemaLocation (aMarshaller, m_sNoNamespaceSchemaLocation);

    // Set XSD (if any)
    final Schema aValidationSchema = createValidationSchema ();
    if (aValidationSchema != null)
      aMarshaller.setSchema (aValidationSchema);

    return aMarshaller;
  }

  /**
   * Customize the passed marshaller before marshalling something.
   *
   * @param aMarshaller
   *        The object to customize. Never null.
   */
  @OverrideOnDemand
  protected void customizeMarshaller (@Nonnull final Marshaller aMarshaller)
  {
    // empty
  }

  @Nonnull
  public final ESuccess write (@Nonnull final JAXBTYPE aObject,
                               @Nonnull final IJAXBMarshaller  aMarshallerFunc)
  {
    ValueEnforcer.notNull (aObject, "Object");
    ValueEnforcer.notNull (aMarshallerFunc, "MarshallerFunc");

    try
    {
      final Marshaller aMarshaller = _createMarshaller (getClassLoader ());
      customizeMarshaller (aMarshaller);

      final JAXBElement  aJAXBElement = m_aJAXBElementWrapper.apply (aObject);

      // Main writing
      aMarshallerFunc.doMarshal (aMarshaller, aJAXBElement);
      return ESuccess.SUCCESS;
    }
    catch (final JAXBException ex)
    {
      m_aWriteExceptionCallbacks.forEach (x -> x.onException (ex));
    }
    return ESuccess.FAILURE;
  }

  /**
   * Customize the passed marshaller before marshalling something.
   *
   * @param aMarshaller
   *        The object to customize. Never null.
   */
  @OverrideOnDemand
  protected void customizeMarshallerForValidation (@Nonnull final Marshaller aMarshaller)
  {
    // empty
  }

  public void validate (@Nonnull final JAXBTYPE aObject, @Nonnull final ErrorList aErrorList)
  {
    ValueEnforcer.notNull (aObject, "Object");
    ValueEnforcer.notNull (aErrorList, "ErrorList");

    final WrappedCollectingValidationEventHandler aEventHandler = new WrappedCollectingValidationEventHandler (aErrorList);
    try
    {
      // create a Marshaller
      final Marshaller aMarshaller = _createMarshaller (getClassLoader ());

      // Overwrite event handler
      aMarshaller.setEventHandler (aEventHandler);

      // Customize on demand
      customizeMarshallerForValidation (aMarshaller);

      if (aMarshaller.getSchema () == null)
        LOGGER.warn ("Running validation on JAXB object of type " +
                     aObject.getClass ().getName () +
                     " makes no sense, because no XML Schema is provided");

      // start marshalling
      final JAXBElement  aJAXBElement = m_aJAXBElementWrapper.apply (aObject);

      // DefaultHandler has very little overhead and does nothing
      aMarshaller.marshal (aJAXBElement, new DefaultHandler ());
    }
    catch (final JAXBException ex)
    {
      // Should already be contained as an entry in the event handler
    }
  }

  @Override
  public String toString ()
  {
    return new ToStringGenerator (this).append ("Type", m_aType)
                                       .append ("XSDs", m_aXSDs)
                                       .append ("JAXBElementWrapper", m_aJAXBElementWrapper)
                                       .append ("EventHandler", m_aEventHandler)
                                       .append ("FormattedOutput", m_bFormattedOutput)
                                       .append ("NSContext", m_aNSContext)
                                       .append ("Charset", m_aCharset)
                                       .append ("IndentString",
                                                m_sIndentString == null ? null
                                                                        : StringHelper.getHexEncoded (m_sIndentString,
                                                                                                      StandardCharsets.ISO_8859_1))
                                       .append ("SchemaLocation", m_sSchemaLocation)
                                       .append ("NoNamespaceSchemaLocation", m_sNoNamespaceSchemaLocation)
                                       .append ("UseContextCache", m_bUseContextCache)
                                       .append ("ClassLoader", m_aClassLoader)
                                       .append ("ReadExceptionHandlers", m_aReadExceptionCallbacks)
                                       .append ("WriteExceptionHandlers", m_aWriteExceptionCallbacks)
                                       .getToString ();
  }

  /**
   * Helper function to create a supplier for {@link JAXBElement} objects.
   *
   * @param aQName
   *        QName to use. May not be null.
   * @param aClass
   *        The implementation class to use.
   * @return Never null.
   * @param 
   *        the type to wrap
   * @since 9.1.5
   * @see #GenericJAXBMarshaller(Class, QName)
   */
  @Nonnull
  public static  Function > createSimpleJAXBElement (@Nonnull final QName aQName,
                                                                           @Nonnull final Class  aClass)
  {
    return aValue -> new JAXBElement <> (aQName, aClass, null, aValue);
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy