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!");
      }
   }
}