/*
 * $Id$
 *
 * Copyright 2006, The jCoderZ.org Project. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *    * Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *    * Redistributions in binary form must reproduce the above
 *      copyright notice, this list of conditions and the following
 *      disclaimer in the documentation and/or other materials
 *      provided with the distribution.
 *    * Neither the name of the jCoderZ.org Project nor the names of
 *      its contributors may be used to endorse or promote products
 *      derived from this software without specific prior written
 *      permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
 package org.jcoderz.commons.util;

import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.ValidationEvent;
import javax.xml.bind.ValidationEventHandler;
import javax.xml.bind.ValidationEventLocator;

import org.jcoderz.commons.ArgumentMalformedException;
import org.jcoderz.commons.RteLogMessage;
import org.xml.sax.InputSource;


/**
 * Utility class to simplify JAXB marshalling/unmarshalling.
 * @author Albrecht Messner
 */
public final class JaxbUtil
{
   private static final Map JAXB_CONTEXT_MAP = new HashMap();


   private JaxbUtil ()
   {
      // avoid instantiation of utility class
   }

   /**
    * Returns a JAXB context for the given context path. Contexts are
    * cached in a hash map.
    * @param contextPath the JAXB context path
    * @return the jaxb context for the given context path
    * @throws JAXBException if the context could not be retrieved
    */
   public static synchronized JAXBContext getJaxbContext (String contextPath)
         throws JAXBException
   {
      JAXBContext ctx = (JAXBContext) JAXB_CONTEXT_MAP.get(contextPath);
      if (ctx == null)
      {
         ctx = JAXBContext.newInstance(contextPath);
         JAXB_CONTEXT_MAP.put(contextPath, ctx);
      }
      return ctx;
   }

   /**
    * Unmarshals the given InputSource and returns the unmarshalled object
    * along with the validation event collector.
    * @param data the data to unmarshal
    * @param ctxPath the context path from which the JAXBContext is created
    *       to create an unmarshaller
    * @return the unmarshalled object along with its validation events
    * @throws JAXBException if unmarshalling or validation fails.
    */
   public static UnmarshalResult unmarshal (InputSource data, String ctxPath)
         throws JAXBException
   {
      final JAXBContext ctx = getJaxbContext(ctxPath);
      return unmarshal(data, ctx);
   }

   /**
    * Unmarshals the given InputSource and returns the unmarshalled object
    * along with the validation event collector.
    * @param data the data to unmarshal
    * @param ctx the JAXBContext from which the unmarshaller should be
    *       retrieved
    * @return the unmarshalled object along with its validation events
    * @throws JAXBException if unmarshalling or validation fails.
    */
   public static UnmarshalResult unmarshal (InputSource data, JAXBContext ctx)
         throws JAXBException
   {
      final Unmarshaller unmarsh = ctx.createUnmarshaller();
      unmarsh.setValidating(true);
      final ValidationEventCollector evtHandler
            = new ValidationEventCollector();
      unmarsh.setEventHandler(evtHandler);
      final Object parsedData = unmarsh.unmarshal(data);
      return new UnmarshalResult(parsedData, evtHandler);
   }

   /**
    * Serializes (marshals) a given JAXB object and returns the result as
    * byte array, along with the validation events collected during
    * marshalling.
    * @param data the object to marshal
    * @param contextPath the context path to retrieve the corresponding JAXB
    *       context for.
    * @return the marshalled object and marshalling events
    * @throws JAXBException if marshalling or validation fails.
    */
   public static MarshalResult marshal (Object data, String contextPath)
         throws JAXBException
   {
      final JAXBContext ctx = getJaxbContext(contextPath);
      return marshal(data, ctx);
   }

   /**
    * Serializes (marshals) a given JAXB object and returns the result as
    * byte array, along with the validation events collected during
    * marshalling.
    * @param data the object to marshal
    * @param ctx the JAXBContext from which the unmarshaller can be retrieved.
    * @return the marshalled object and marshalling events
    * @throws JAXBException if marshalling or validation fails.
    */
   public static MarshalResult marshal (Object data, JAXBContext ctx)
         throws JAXBException
   {
      final Marshaller marsh = ctx.createMarshaller();
      final ValidationEventCollector evtHandler
            = new ValidationEventCollector();
      marsh.setEventHandler(evtHandler);
      final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
      marsh.marshal(data, outStream);
      final MarshalResult result
            = new MarshalResult(outStream.toByteArray(), evtHandler);
      return result;
   }

   /**
    * Holds the Result of an unmarshal operation.
    * @author Albrecht Messner
    */
   public static class UnmarshalResult
   {
      private final Object mParsedData;
      private final ValidationEventCollector mValidationEvents;

      UnmarshalResult (Object parsedData, ValidationEventCollector evtHandler)
      {
         mParsedData = parsedData;
         mValidationEvents = evtHandler;
      }

      /**
       * Returns the parsed (unmarshalled) object.
       * @return the parsed (unmarshalled) object.
       */
      public Object getParsedData ()
      {
         return mParsedData;
      }

      /**
       * Returns the validation events of the unmarshal operation.
       * @return the validation events of the unmarshal operation.
       */
      public ValidationEventCollector getValidationEvents ()
      {
         return mValidationEvents;
      }
   }

   /**
    * Holds the Result of an Marshal operation.
    * @author Albrecht Messner
    */
   public static class MarshalResult
   {
      private final byte[] mMarshalledData;
      private final ValidationEventCollector mValidationEvents;

      MarshalResult (byte[] marshalledData, ValidationEventCollector evtHandler)
      {
         mMarshalledData = marshalledData;
         mValidationEvents = evtHandler;
      }

      /**
       * Returns the marshalled object.
       * @return the marshalled object.
       */
      public byte[] getMarshalledData ()
      {
         return mMarshalledData;
      }

      /**
       * Returns the validation events of the marshal operation.
       * @return the validation events of the marshal operation.
       */
      public ValidationEventCollector getValidationEvents ()
      {
         return mValidationEvents;
      }
   }

   /**
    * Validation handler for JAXB.
    *
    * @author Michael Griffel
    */
   public static class ValidationEventCollector
         implements ValidationEventHandler
   {
      private static final String CLASSNAME
            = ValidationEventCollector.class.getName();

      private static final Logger logger = Logger.getLogger(CLASSNAME);

      private final List mEvents = new ArrayList();

      /**
       * Returns all the collected errors and warnings or an empty list
       * if there weren't any. The result is an unmodifiable list.
       *
       * @return all the collected errors and warnings or an empty list
       *      if there weren't any. The result is an unmodifiable list.
       */
      public List getEvents ()
      {
         return Collections.unmodifiableList(mEvents);
      }

      /**
       * Clear all collected errors and warnings.
       */
      public void reset ()
      {
         mEvents.clear();
      }

      /**
       * Returns true if this event collector contains at least one
       * ValidationEvent.
       *
       * @return true if this event collector contains at least one
       *         ValidationEvent, false otherwise
       */
      public boolean hasEvents ()
      {
         return mEvents.size() != 0;
      }

      /** {@inheritDoc} */
      public boolean handleEvent (ValidationEvent event)
      {
         final String methodName = "handleEvent";
         if (logger.isLoggable(Level.FINER))
         {
            logger.entering(CLASSNAME, methodName, event);
            logger.finer("Event details: "
                  + eventToString(new StringBuffer(), event));
         }

         mEvents.add(event);

         final boolean doContinue
               = event.getSeverity() != ValidationEvent.FATAL_ERROR;

         if (logger.isLoggable(Level.FINER))
         {
            logger.exiting(CLASSNAME, methodName, String.valueOf(doContinue));
         }
         return doContinue;
      }

      /**
       * Returns a summary of the validation events as String.
       * @return a summary of the validation events as String.
       */
      public String toString ()
      {
         final StringBuffer sb = new StringBuffer();
         for (int i = 0; i < mEvents.size(); ++i)
         {
            sb.append('[');
            sb.append(i + 1);
            sb.append('/');
            sb.append(mEvents.size());
            sb.append("] ");
            final ValidationEvent e = (ValidationEvent) mEvents.get(i);
            eventToString(sb, e);
         }
         return sb.toString().trim();
      }

      private StringBuffer eventToString (
            final StringBuffer sb, final ValidationEvent e)
      {
         appendLocator(sb, e.getLocator());

         if (e.getLinkedException() != null)
         {
            appendLinkedException(sb, e);
         }
         else
         {
            sb.append(e.getMessage());
         }
         appendSpace(sb);

         return sb;
      }

      private void appendLinkedException (final StringBuffer sb,
            final ValidationEvent e)
      {
         final String causeMessage = e.getLinkedException().getMessage();
         if (!e.getMessage().equals(causeMessage))
         {
            if (e.getLinkedException() instanceof ArgumentMalformedException)
            {
               final ArgumentMalformedException ame
                     = (ArgumentMalformedException) e.getLinkedException();
               sb.append("The Argument ");
               sb.append(getParameter(
                     ame, RteLogMessage.ArgumentMalformed.PARAM_ARGUMENT_NAME));
               sb.append(" with the value '");
               sb.append(getParameter(ame,
                     RteLogMessage.ArgumentMalformed.PARAM_ARGUMENT_VALUE));
               sb.append("' is malformed. ");
               sb.append(getParameter(
                     ame, RteLogMessage.ArgumentMalformed.PARAM_HINT));
            }
            else
            {
               sb.append(e.getMessage());
               if (causeMessage != null)
               {
                  sb.append(" Cause: ");
                  sb.append(causeMessage);
               }
            }
         }
         else
         {
            sb.append(e.getMessage());
         }
      }

      private void appendLocator (final StringBuffer sb,
            ValidationEventLocator locator)
      {
         if (locator != null)
         {
            if (locator.getObject() != null)
            {
              sb.append("Object: ");
              sb.append(locator.getObject());
              appendSpace(sb);
            }
            if (locator.getNode() != null)
            {
              sb.append("Node: ");
              sb.append(locator.getObject());
              appendSpace(sb);
            }
            if (locator.getOffset() >= 0)
            {
               sb.append("Offset: ");
               sb.append(locator.getOffset());
               appendSpace(sb);
            }
         }
      }

      /** Simply appends a spece to the given StringBuffer. */
      private final StringBuffer appendSpace (final StringBuffer sb)
      {
         return sb.append(' ');
      }

      private String getParameter (ArgumentMalformedException ex, String name)
      {
         final List parameters = ex.getParameter(name);
         final String result;
         if (parameters != null)
         {
            result = (String) parameters.get(0);
         }
         else
         {
            result = "";
         }
         return result;
      }
   }

}
