/***********************************************************
Copyright (C) 2020 VeriSign, Inc.

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library 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
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

http://www.verisign.com/nds/naming/namestore/techdocs.html
 ***********************************************************/
package com.verisign.epp.codec.unhandlednamespaces.v1_0;

import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.apache.xerces.dom.DocumentImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import com.verisign.epp.codec.gen.EPPCodec;
import com.verisign.epp.codec.gen.EPPCodecComponent;
import com.verisign.epp.codec.gen.EPPExtValue;
import com.verisign.epp.codec.gen.EPPPollMessageFilter;
import com.verisign.epp.codec.gen.EPPResponse;
import com.verisign.epp.codec.gen.EPPResult;
import com.verisign.epp.codec.gen.EPPService;
import com.verisign.epp.codec.gen.EPPUtil;
import com.verisign.epp.codec.gen.EPPValue;
import com.verisign.epp.exception.EPPException;

/**
 * {@code EPPUnhandledNamespacesPollMessageFilter} is a utility class used to
 * filter poll message {@link EPPResponse} instances against the list of
 * client-specified object services and extension services, per
 * {@code RFC 9038}. Any non-supported services (object or extension) will be
 * removed from the poll message and moved to an &lt;extValue&gt; element with
 * the full XML block along with a formatted string &lt;reason&gt;. <br>
 * The &lt;value&gt; element contains the full unsupported block of XML whether
 * it be for an object extension or a command / response extension. <br>
 * The &lt;reason&gt; element is encoded using the following ABNF:<br>
 *
 * <pre>
 * REASON        = NAMESPACE-URI " not in login services"
 * NAMESPACE-URI = 1*VCHAR
 * </pre>
 *
 * <br>
 * The class does support a no-operation option to simply identify and log
 * non-supported services without removing them from the poll message or adding
 * &lt;extValue&gt; elements to the result.
 */
public class EPPUnhandledNamespacesPollMessageFilter implements EPPPollMessageFilter {

  /**
   * Category for logging
   */
  private static Logger cat = LoggerFactory.getLogger(EPPUnhandledNamespacesPollMessageFilter.class);

  /**
   * Monitor to use, which defaults to
   * {@link EPPUnhandledNamespacesMonitorLogger}, but it can be overridden.
   */
  private EPPUnhandledNamespacesMonitor monitor;

  /**
   * Default constructor, where the monitor is by default set to
   * {@link EPPUnhandledNamespacesMonitorLogger}.
   */
  public EPPUnhandledNamespacesPollMessageFilter() {
    this.monitor = new EPPUnhandledNamespacesMonitorLogger(cat, false);
  }

  /**
   * Constructor that the monitor to use as a parameter.
   * 
   * @param aMonitor
   *           Monitor to use for unhandled namespaces. Set to {@code null} if
   *           no monitor is desired.
   */
  public EPPUnhandledNamespacesPollMessageFilter(EPPUnhandledNamespacesMonitor aMonitor) {
    this.monitor = aMonitor;
  }

  /**
   * Gets the monitor to use with the filter with the default being
   * {@link EPPUnhandledNamespacesMonitorLogger}.
   *
   * @return Monitor to use with the filter if set; {@code null} otherwise.
   */
  public EPPUnhandledNamespacesMonitor getMonitor() {
    return this.monitor;
  }

  /**
   * Gets the monitor to use with the filter.
   * 
   * @param monitor
   *           Monitor to use with the filter. Set to {@code null} to not use a
   *           monitor.
   */
  public void setMonitor(EPPUnhandledNamespacesMonitor monitor) {
    this.monitor = monitor;
  }

  /**
   * Filter any poll messages that are not supported by the client based on the
   * passed in login services (object extensions) and extension services
   * (command response extensions) from the poll message and encode into the
   * response the information associated with the unsupported extensions
   * (object or command / response).
   *
   * @param aResponse
   *           Source poll message
   * @param aServices
   *           {@code List} of {@link EPPService} login services (object
   *           extensions) supported by the client.
   * @param aExtServices
   *           {@code List} of {@link EPPService} login extension services
   *           (command response extensions) supported by the client.
   * @param aData
   *           Optional data object that will be passed through to the monitor
   *           when there is at least one unhandled namespace identified. This
   *           is useful to set transaction-level attributes based on the use
   *           of unhandled namespaces. Set to {@code null} if a data object is
   *           not needed.
   * @return Filtered poll message {@link EPPResponse} that contains extensions
   *         that the client supports.
   * @throws EPPException
   *            Exception filtering the poll message
   */
  public EPPResponse filter(final EPPResponse aResponse, List<EPPService> aServices, List<EPPService> aExtServices,
        Object aData) throws EPPException {
    return filter(aResponse, aServices, aExtServices, false, aData);
  }

  /**
   * Filter any poll messages that are not supported by the client based on the
   * passed in login services (object extensions) and extension services
   * (command response extensions) from the poll message and encode into the
   * response the information associated with the unsupported extensions
   * (object or command / response).
   *
   * @param aResponse
   *           Source poll message
   * @param aServices
   *           {@code List} of {@link EPPService} login services (object
   *           extensions) supported by the client.
   * @param aExtServices
   *           {@code List} of {@link EPPService} login extension services
   *           (command response extensions) supported by the client.
   * @param aNoOp
   *           Set to {@code true} to only identify unsupported services by
   *           logging them and not removing them or encoding them in the
   *           &lt;extValue&gt; elements.
   * @param aData
   *           Optional data object that will be passed through to the monitor
   *           when there is at least one unhandled namespace identified. This
   *           is useful to set transaction-level attributes based on the use
   *           of unhandled namespaces. Set to {@code null} if a data object is
   *           not needed.
   * @return Filtered poll message {@link EPPResponse} that contains extensions
   *         that the client supports.
   * @throws EPPException
   *            Exception filtering the poll message
   */
  public EPPResponse filter(final EPPResponse aResponse, List<EPPService> aServices, List<EPPService> aExtServices,
        boolean aNoOp, Object aData) throws EPPException {

    cat.debug("filter(EPPResponse, List, List, boolean, Object): enter - NoOp = " + aNoOp);

    EPPResponse theResponse = null;
    boolean theResponseFiltered = false;
    // Pre-condition checks
    if (aResponse == null) {
      throw new EPPException(
            "EPPUnhandledNamespacesPollMessageFilter.filter(): null poll message EPPResponse passed");
    }

    if (!aResponse.hasMsgQueue()) {
      cat.debug(
            "filter(EPPResponse, List, List, boolean, Object): exit - the response does not contain a poll message.");
      return aResponse;
    }

    Set<String> theUnhandledNamespaces = new HashSet<String>();

    try {
      aResponse.getMsgQueueMsg();

      // -- Filter Object Extension --

      // Is the poll message a object extension (derived class of
      // EPPResponse)?
      if (!aResponse.getClass().getName().equals(EPPResponse.class.getName())) {

        // Is the object extension supported by the client?
        if (!supportsNamespace(aResponse.getNamespace(), aServices)) {

          theUnhandledNamespaces.add(aResponse.getNamespace());

          if (!aNoOp) {
            theResponseFiltered = true;

            // Add the extValue element that references the unsupported
            // namespace URI
            EPPExtValue theExtValue = createObjectExtValue(aResponse);
            EPPResult theResult = (EPPResult) aResponse.getResult().clone();
            theResult.addExtValue(theExtValue);

            // Create a standard response and copy the standard response
            // attributes.
            theResponse = new EPPResponse(aResponse.getTransId(), theResult);
            theResponse.setMsgQueue(aResponse.getMsgQueue());
            theResponse.setExtensions(aResponse.getExtensions());
          }
          else {
            theResponse = (EPPResponse) aResponse.clone();
          }

          cat.debug("filter(EPPResponse, List, List, boolean): object service [" + aResponse.getNamespace()
                + "] not supported");
        }
        else {
          theResponse = (EPPResponse) aResponse.clone();
        }

      }
      else {
        theResponse = (EPPResponse) aResponse.clone();
      }

      // -- Filter Command Response Extensions --
      List theExtensions = theResponse.getExtensions();
      if (theExtensions != null) {
        Iterator iterator = theExtensions.iterator();

        while (iterator.hasNext()) {
          EPPCodecComponent theExtension = (EPPCodecComponent) iterator.next();

          // Is the object extension supported by the client?
          if (!supportsNamespace(theExtension.getNamespace(), aExtServices)) {

            theUnhandledNamespaces.add(theExtension.getNamespace());

            if (!aNoOp) {
              theResponseFiltered = true;

              // Add the extValue element that references the unsupported
              // namespace URI
              theResponse.getResult().addExtValue(createExtensionExtValue(theExtension));

              // Remove extension from list of extensions
              iterator.remove();
            }
            cat.debug("filter(EPPResponse, List, List, boolean): extension service ["
                  + theExtension.getNamespace() + "] not supported");
          }
        }
      }
    }
    catch (Exception ex) {
      cat.error("Unexpected exception: " + ex + " filtering the poll message: " + aResponse);
      throw new EPPException("Exception filtering poll message", ex);
    }

    // Monitor set and at least one unhandled namespeace found?
    if ((this.monitor != null) && (!theUnhandledNamespaces.isEmpty())) {
      cat.debug(
            "filter(EPPResponse, List, boolean, Object): At least one unhandled namespace found, calling monitor");
      this.monitor.monitorUnhandledNamespaces(aResponse, theResponse, aNoOp, theUnhandledNamespaces, aData);
    }

    cat.debug("filter(EPPResponse, List, List, Object): exit");
    return theResponse;
  }

  /**
   * Is the specified XML namespace in the list of services?
   *
   * @param aNamespace
   *           XML namespace string
   * @param aServices
   *           {@code List} of {@link EPPService} objects.
   * @return {@code true} if the XML namespace is supported; {@code false}
   *         otherwise.
   */
  private boolean supportsNamespace(String aNamespace, List<EPPService> aServices) {

    cat.debug("supportsNamespace(String, List); Looking for namespace " + aNamespace + " support");

    if (aServices != null) {

      Iterator iterator = aServices.iterator();

      while (iterator.hasNext()) {
        EPPService theService = (EPPService) iterator.next();

        if (theService.getNamespaceURI().equals(aNamespace)) {
          cat.debug("supportsNamespace(String, List); namespace " + aNamespace + " supported");
          return true;
        }
      }
    }

    cat.debug("supportsNamespace(String, List); namespace " + aNamespace + " not supported");
    return false;
  }

  /**
   * Creates an {@link EPPExtValue} instance that includes the full EPP
   * extension XML and an encoded reason.
   *
   * @param aExtension
   *           Unsupported EPP extension to use to create the
   *           {@link EPPExtValue} instance.
   *
   * @return {@link EPPExtValue} instance that includes a EPP extension XML
   *         value and a reason that references the unsupported namespace URI.
   */
  private EPPExtValue createExtensionExtValue(EPPCodecComponent aExtension) throws EPPException {
    EPPExtValue theExtValue = new EPPExtValue();

    // Encode extension to DOM tree
    Document theDoc = new DocumentImpl();
    Element theRootElm = aExtension.encode(theDoc);

    theExtValue.setValue(
          new EPPValue(EPPUtil.toStringNoIndent(theRootElm), theRootElm.getPrefix(), theRootElm.getNamespaceURI()));
    theExtValue.setReason(theRootElm.getNamespaceURI() + " not in login services");

    return theExtValue;
  }

  /**
   * Creates an {@link EPPExtValue} instance that is encoded with the full XML
   * of the unsupported object.
   *
   * @param aResponse
   *           Response that contains the concrete object information
   *
   * @return {@link EPPExtValue} instance that includes a XML value and a
   *         reason that references the unsupported object namespace URI.
   *
   * @exception EPPException
   *               Error creating the {@link EPPExtValue} from the unsupported
   *               {@link EPPResponse).
   */
  private EPPExtValue createObjectExtValue(EPPResponse aResponse) throws EPPException {
    EPPExtValue theExtValue = new EPPExtValue();

    Document theDoc = new DocumentImpl();
    Element theRootElm = aResponse.encode(theDoc);

    // Find the <resData> element
    NodeList theResDataElm = theRootElm.getElementsByTagNameNS(EPPCodec.NS,
          EPPUtil.getLocalName(EPPResponse.ELM_RESPONSE_DATA));
    if (theResDataElm.getLength() != 1) {
      throw new EPPException("Response does not include a <resData> element");
    }
    Element theObjectElm = EPPUtil.getFirstElementChild((Element) theResDataElm.item(0));
    if (theObjectElm == null) {
      throw new EPPException("Response object data element not found");
    }
    String theObjectXml = EPPUtil.toStringNoIndent(theObjectElm);

    theExtValue.setValue(new EPPValue(theObjectXml, theObjectElm.getPrefix(), theObjectElm.getNamespaceURI()));
    theExtValue.setReason(aResponse.getNamespace() + " not in login services");

    return theExtValue;
  }

}
