/***********************************************************
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 com.verisign.epp.codec.gen.EPPCodecComponent;
import com.verisign.epp.codec.gen.EPPExtValue;
import com.verisign.epp.codec.gen.EPPResponse;
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 EPPUnhandledNamespacesResponseFilter} is a utility class used to
 * filter command response extensions, per {@code RFC 9038}. Any non-supported
 * command response extensions will be removed from the response 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 response or adding
 * &lt;extValue&gt; elements to the result.
 */
public class EPPUnhandledNamespacesResponseFilter {

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

  /**
   * Monitor to use, which defaults to
   * {@link EPPUnhandledNamespacesMonitorLogger}, but it can be overridden.
   */
  private EPPUnhandledNamespacesMonitor monitor = new EPPUnhandledNamespacesMonitorLogger(cat, true);

  /**
   * Default constructor, where the monitor is by default set to
   * {@link EPPUnhandledNamespacesMonitorLogger}.
   */
  public EPPUnhandledNamespacesResponseFilter() {
  }

  /**
   * 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 EPPUnhandledNamespacesResponseFilter(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 response extensions that are not supported by the client based
   * on the passed in the extension services (command response extensions) and
   * the response with the pre-filtered extensions.
   *
   * @param aResponse
   *           Source response
   * @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 {@link EPPResponse} that only contains extensions that
   *         the client supports.
   * @throws EPPException
   *            Exception filtering the response
   */
  public EPPResponse filter(final EPPResponse aResponse, List<EPPService> aExtServices, Object aData)
        throws EPPException {
    return filter(aResponse, aExtServices, false, aData);
  }

  /**
   * Filter any response extensions that are not supported by the client based
   * on the passed in the extension services (command response extensions) and
   * the response with the pre-filtered extensions.
   *
   * @param aResponse
   *           Source response
   * @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
   *           calling the monitor and not moving the unsupported services into
   *           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 {@link EPPResponse} that only contains extensions that
   *         the client supports.
   * @throws EPPException
   *            Exception filtering the response
   */
  public EPPResponse filter(final EPPResponse aResponse, List<EPPService> aExtServices, boolean aNoOp, Object aData)
        throws EPPException {

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

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

    EPPResponse theResponse = null;
    // Pre-condition checks
    if (aResponse == null) {
      throw new EPPException("EPPUnhandledNamespacesResponseFilter.filter(): null EPPResponse passed");
    }

    try {
      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) {
              // 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, boolean, Object): extension service ["
                  + theExtension.getNamespace() + "] not supported");
          }
        }
      }
    }
    catch (Exception ex) {
      cat.error("Unexpected exception: " + ex + " filtering response: " + aResponse);
      throw new EPPException("Exception filtering the response", 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, boolean, 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;
  }

}
