# SOAPHeaderImpl.java

    /*
    ** Module   : SOAPHeaderImpl.java
    ** Abstract : Implementation for the SOAP-HEADER resource.
    **
    ** Copyright (c) 2013-2023, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --------------------------------Description-----------------------------------
    ** 001 CA  20130216 Created initial version.
    ** 002 CA  20130221 Added ADM-DATA, UNIQUE-ID support. Fixed resource type.
    ** 003 OM  20130304 Refactored isValid and isUnknown of WrappedResource to valid and unknown.
    ** 004 CA  20130603 resourceDelete must return a boolean value.
    ** 005 CA  20130927 Removed setResourceType, as the type is determined from LegacyResource
    **                  annotation.
    ** 006 HC  20131215 Implemented ADM-DATA and UNIQUE-ID attribute.
    ** 007 CA  20140107 Added runtime support.
    ** 008 VVT 20220306 Uniform toString() support added.
    **     CA  20221006 UNIQUE-ID is kept as Java type instead of BDT.  Refs #6827
    **     CA  20230116 Avoid using handle.unwrap, handle.getReference or other BDT usage from within FWD runtime.
    */
    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program 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 Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.util;

    import java.io.*;
    import java.util.*;

    import javax.xml.parsers.*;
    import javax.xml.soap.*;

    import org.w3c.dom.*;
    import org.w3c.dom.Node;

    import com.goldencode.util.*;
    import com.goldencode.p2j.util.UniqueIdGenerator.IdKind;

    /**
     * A class for implementing the SOAP-HEADER methods and attributes.
     */
    public class SOAPHeaderImpl
    extends HandleChain
    implements SOAPHeader,
               CommonHandleChain
    {
       /** Corresponds to ADM-DATA attribute. */
       private String admData = null;

       /** Corresponds to UNIQUE-ID attribute. */
       private Long uniqueID = null;

       /** All SOAP Header Entries linked with this SOAP Header. */
       private final Set<SOAPHeaderEntryImpl> headers = new HashSet<>();

       /**
        * Mapping of each key of a header entry to a DOM XML Node. This is needed because, when a
        * SOAP Header Entry resource changes its inner DOM XML node, all linked SOAP Header Entries
        * change it too.
        */
       private final Map<Object, Node> entries = new HashMap<>();

       /** List allowing fast indexed access to the header entry keys. */
       private final List<Object> entryKeys = new ArrayList<>();

       /** Flag indicating if this resource was deleted or not. */
       private boolean deleted = false;

       /**
        * Implicit c'tor.
        */
       public SOAPHeaderImpl()
       {
          super(true, false);
       }

       /**
        * Returns the number of SOAP-HEADER-ENTRYREF object entries attached to the SOAP-HEADER
        * object.
        *
        * @return   See above.
        */
       @Override
       public integer getNumHeaderEntries()
       {
          return new integer(entries.size());
       }

       /**
        * Creates a new entry in a SOAP-header object's list of entries and associates the new entry
        * with the given SOAP-HEADER-ENTRY-REF object.
        *
        * @param    entryRef
        *           The SOAP Header Entry.

        * @return   <code>true</code> if the operation was possible.
        */
       @Override
       public logical addHeaderEntry(handle entryRef)
       {
          if (entryRef._isValid() && (entryRef.getResource() instanceof SOAPHeaderEntryImpl))
          {
             SOAPHeaderEntryImpl entry = (SOAPHeaderEntryImpl) entryRef.getResource();
             addHeaderEntry(entry);

             return new logical(true);
          }

          SOAPFactory.throwError("ADD-HEADER-ENTRY", "SOAP-HEADER-ENTRY");

          return new logical(false);
       }

       /**
        * Get the header entry at the given index and assign it to the given handle.
        * <p>
        * This method returns a logical even though the Progress 4GL documentation explicitly states
        * otherwise. Actual working code has proven that the original documentation is incorrect.
        * This implementation matches the actual runtime behavior of the 4GL.
        *
        * @param    target
        *           The handle where the SOAP-HEADER-ENTRY instance will be saved.
        * @param    idx
        *           The index of the SOAP-HEADER-ENTRY instance to retrieve. 4GL allows only integer
        *           and int64 values to be passed to this parameter.
        *
        * @return   <code>true</code> if the operation was possible.
        */
       @Override
       public logical getHeaderEntry(handle target, long idx)
       {
          return getHeaderEntry(target, new int64(idx));
       }

       /**
        * Get the header entry at the given index and assign it to the given handle.
        * <p>
        * This method returns a logical even though the Progress 4GL documentation explicitly states
        * otherwise. Actual working code has proven that the original documentation is incorrect.
        * This implementation matches the actual runtime behavior of the 4GL.
        *
        * @param    target
        *           The handle where the SOAP-HEADER-ENTRY instance will be saved.
        * @param    idx
        *           The index of the SOAP-HEADER-ENTRY instance to retrieve. 4GL allows only integer
        *           and int64 values to be passed to this parameter.
        *
        * @return   <code>true</code> if the operation was possible.
        */
       @Override
       public logical getHeaderEntry(handle target, integer idx)
       {
          return getHeaderEntry(target, new int64(idx));
       }

       /**
        * Get the header entry at the given index and assign it to the given handle.
        * <p>
        * This method returns a logical even though the Progress 4GL documentation explicitly states
        * otherwise. Actual working code has proven that the original documentation is incorrect.
        * This implementation matches the actual runtime behavior of the 4GL.
        *
        * @param    target
        *           The handle where the SOAP-HEADER-ENTRY instance will be saved.
        * @param    idx
        *           The index of the SOAP-HEADER-ENTRY instance to retrieve. 4GL allows only integer
        *           and int64 values to be passed to this parameter.
        *
        * @return   <code>true</code> if the operation was possible.
        */
       @Override
       public logical getHeaderEntry(handle target, int64 idx)
       {
          if (target._isValid() && (target.getResource() instanceof SOAPHeaderEntryImpl))
          {
             if (idx.isUnknown() || idx.intValue() < 1 || idx.intValue() > entries.size())
             {
                // no error is shown or registered, and true is returned.
                return new logical(true);
             }

             // locate the key
             Object key = entryKeys.get(idx.intValue() - 1);

             // this returns really a new resource... it has distinct ADM-DATA/UNIQUE-ID/PRIVATE-DATA,
             // but they share the inner X-NODEREF resource.
             SOAPHeaderEntryImpl targetEntry = (SOAPHeaderEntryImpl) target.getResource();

             targetEntry.setParent(this, key);

             // register this header, so it can be detached if the soap-header gets deleted
             this.headers.add(targetEntry);

             return new logical(true);
          }

          SOAPFactory.throwError("GET-HEADER-ENTRY", "SOAP-HEADER-ENTRY");

          return new logical(false);
       }

       /**
        * Reports if this object is valid for use.
        *
        * @return   <code>true</code> if we are valid (can be used).
        */
       @Override
       public boolean valid()
       {
          return !deleted;
       }

       /**
        * Get the value of the ADM-DATA attribute.
        *
        * @return   See above.
        */
       @Override
       public character getADMData()
       {
          return new character(this.admData);
       }

       /**
        * Set the value of the ADM-DATA attribute.
        *
        * @param    value
        *           The new value.
        */
       @Override
       public void setADMData(String value)
       {
          this.admData = value;
       }

       /**
        * Set the value of the ADM-DATA attribute.
        *
        * @param    value
        *           The new value.
        */
       @Override
       public void setADMData(character value)
       {
          this.admData = value.getValue();
       }

       /**
        * Gets the the unique ID number associated to this object by the underlying system. Not the
        * same as the handle value itself.
        *
        * @return   The integer number of the children.
        */
       @Override
       public integer getUniqueID()
       {
          if (uniqueID == null)
          {
             uniqueID = UniqueIdGenerator.getUniqueId(IdKind.SOAP_HEADER);
          }
          return new integer(this.uniqueID);
       }

       /**
        * Delete the resource.
        *
        * @return   <code>true</code> if the resource was deleted.
        */
       @Override
       protected boolean resourceDelete()
       {
          // do not delete the soap header entries, but unlink them.
          for (SOAPHeaderEntryImpl header : headers)
          {
             header.setParent(null, null);
          }

          // clear all references
          entries.clear();
          entryKeys.clear();
          headers.clear();

          this.deleted = true;

          return true;
       }

       /**
        * Check if this resource supports the NEXT-SIBLING attribute.
        *
        * @return   Always <code>false</code>.
        */
       @Override
       protected boolean hasNextSibling()
       {
          return false;
       }

       /**
        * Check if this resource supports the PREV-SIBLING attribute.
        *
        * @return   Always <code>false</code>.
        */
       @Override
       protected boolean hasPrevSibling()
       {
          return false;
       }

       /**
        * Check if this resource supports the NAME attribute in read-only mode.
        *
        * @return   Always <code>true</code>.
        */
       @Override
       protected boolean hasNameReadOnly()
       {
          return true;
       }

       /**
        * Removes the specified SOAP-Header object from the list of header entries.
        *
        * @param    entry
        *           The SOAP Header Entry.
        * @param    key
        *           The key to the DOM XML Node referenced by this header entry.
        */
       void removeHeaderEntry(SOAPHeaderEntryImpl entry, Object key)
       {
          // cleanup after this key
          entries.remove(key);
          entryKeys.remove(key);
          headers.remove(entry);

          Iterator<SOAPHeaderEntryImpl> itr = headers.iterator();
          while (itr.hasNext())
          {
             SOAPHeaderEntryImpl header = itr.next();

             if (header.getHeaderKey() == key)
             {
                // detach and remove them
                header.setParent(null, null);
                itr.remove();
             }
          }
       }

       /**
        * Creates a new entry in a SOAP-header object's list of entries and associates the new entry
        * with the given SOAP-HEADER-ENTRY-REF object.
        *
        * @param    entry
        *           The SOAP Header Entry.
        */
       void addHeaderEntry(SOAPHeaderEntryImpl entry)
       {
          Object key = new Object();

          entry.setParent(this, key);

          entries.put(key, null);
          entryKeys.add(key);

          headers.add(entry);
       }

       /**
        * Get the string representation of the XML payload associated with this soap header.
        * <p>
        * If there are no header entries, return {@code null}.
        *
        * @return   See above.
        */
       @Override
       public String asString()
       {
          try
          {
             Document doc =  XmlHelper.newDocument();

             // create a HEADER element node, linked with the SOAP 1.1. namespace; to this, each
             // header entry DOM XML node will be added.
             Element root = doc.createElementNS(SOAPConstants.URI_NS_SOAP_1_1_ENVELOPE, "header");
             doc.appendChild(root);

             boolean hadHeaders = false;
             for (Object key : entryKeys)
             {
                Node entry = entries.get(key);

                if (entry == null)
                {
                   // TODO: what if a key has no DOM XML node? this is possible, because the DOM XML
                   // node is not created until SOAP-HEADER-ENTRYREF:SET-NODE or SET-SERIALIZED is
                   // called
                   continue;
                }

                Element clone = (Element) doc.importNode(entry, true);

                String actor = (String) entry.getUserData(SOAPHeaderEntryImpl.ACTOR);
                if (actor != null)
                {
                   clone.setAttributeNS(SOAPConstants.URI_NS_SOAP_1_1_ENVELOPE, "actor", actor);
                }

                Boolean mustUnderstand =
                   (Boolean) entry.getUserData(SOAPHeaderEntryImpl.MUST_UNDERSTAND);
                if (mustUnderstand != null && mustUnderstand)
                {
                   clone.setAttributeNS(SOAPConstants.URI_NS_SOAP_1_1_ENVELOPE, "mustUnderstand", "1");
                }

                root.appendChild(clone);

                hadHeaders = true;
             }

             String headers = null;

             if (hadHeaders)
             {
                ByteArrayOutputStream output = new ByteArrayOutputStream();
                XmlHelper.write(doc, output, false, true);

                headers = output.toString().trim();
             }

             return headers;
          }
          catch (ParserConfigurationException | IOException e)
          {
             throw new RuntimeException("Problems serializing SOAP Headers:", e);
          }
       }

       /**
        * Get the DOM XML node associated with the given key.
        *
        * @param    key
        *           The key to an entry in the {@link #entries} map.
        *
        * @return   The DOM XML node for the given key.
        *
        * @throws   IllegalArgumentException
        *           If the received key is {@code null} or is not part of the {@link #entries} map's
        *           keys.
        */
       Node getHeaderNode(Object key)
       {
          if (key != null && entries.containsKey(key))
          {
             return entries.get(key);
          }
          else
          {
             // this exception should never be thrown; if this code is reached, there is a problem in
             // the SOAP Header resource implementation.
             // was added as a means to mark SOAP Header related problems, to ease debugging.
             throw new IllegalArgumentException("Received an unknown SOAP Header Entry key!");
          }
       }

       /**
        * Associated a new DOM XML node with the given header entry key.
        *
        * @param    key
        *           The key to an entry in the {@link #entries} map.
        * @param    node
        *           The new DOM XML node for the given key.
        *
        * @throws   IllegalArgumentException
        *           If the received key is {@code null} or is not part of the {@link #entries} map's
        *           keys.
        */
       void setHeaderNode(Object key, Node node)
       {
          if (key != null && entries.containsKey(key))
          {
             this.entries.put(key, node);
          }
          else
          {
             // this exception should never be thrown; if this code is reached, there is a problem in
             // the SOAP Header resource implementation.
             // was added as a means to mark SOAP Header related problems, to ease debugging.
             throw new IllegalArgumentException("Received an unknown SOAP Header Entry key!");
          }
       }
    }