//
// FlowHelper.java - Useful things used during flow processing
//

package nyangau.jmsadapter;

//...simports:0:
import java.util.Map;
import java.util.HashMap;
import java.util.Set;
import java.util.Enumeration;
import java.util.StringTokenizer;
import java.util.Properties;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.CharArrayWriter;
import java.io.PrintWriter;
import java.io.IOException;
import java.io.Serializable;
import java.net.URL;
import javax.jms.JMSException;
import javax.jms.MessageEOFException;
import javax.xml.namespace.NamespaceContext;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.Text;
//...e

public class FlowHelper
  {
//...sXML scanning helpers:2:
// Code used for scanning <?xml text, looking for encoding="XYZ"

//...swsChar:2:
protected static boolean wsChar(char c)
  {
  return c == ' ' || c == '\t' || c == '\n' || c == '\r';
  }
//...e
//...snameStartChar:2:
protected static boolean nameStartChar(char c)
  {
  return ( c >= 'A' && c <= 'Z' ) ||
         ( c >= 'a' && c <= 'z' ) ||
         c == ':' || c == '_' ||
         ( c >= '\u00c0' && c <= '\u00d6' ) ||
         ( c >= '\u00d8' && c <= '\u00f6' ) ||
         ( c >= '\u00f8' && c <= '\u02ff' ) ||
         ( c >= '\u0370' && c <= '\u037d' ) ||
         ( c >= '\u037f' && c <= '\u1fff' ) ||
         ( c >= '\u200c' && c <= '\u200d' ) ||
         ( c >= '\u2070' && c <= '\u218f' ) ||
         ( c >= '\u2c00' && c <= '\u2fef' ) ||
         ( c >= '\u3001' && c <= '\ud7ff' ) ||
         ( c >= '\uf900' && c <= '\ufdcf' ) ||
         ( c >= '\ufdf0' && c <= '\ufffd' );
  }
//...e
//...snameChar:2:
protected static boolean nameChar(char c)
  {
  return nameStartChar(c) ||
         ( c >= '0' && c <= '9' ) ||
         c == '-' || c == '.' ||
         c == (char)0xb7 ||
         ( c >= '\u0300' && c <= '\u036f' ) ||
         ( c >= '\u203f' && c <= '\u2040' );
  }
//...e

// Two almost identical implementations of nextToken, so as to avoid the
// need to convert between String <-> byte[] before using it.

//...snextToken byte\91\\93\:2:
protected static int nextToken(byte[] b, int i, StringBuffer sb)
  {
  while ( i < b.length && wsChar((char)b[i]) )
    i++;
  if ( i == b.length )
    return -1; // no more tokens
  if ( (char)b[i] == '?' && i+2 <= b.length && (char)b[i+1] == '>' )
    return -1; // no more tokens, as we hit ?>
  if ( nameStartChar((char)b[i]) )
    {
    sb.append((char)b[i++]);
    while ( i < b.length && nameChar((char)b[i]) )
      sb.append((char)b[i++]);
    return i;
    }
  if ( (char)b[i] == '"' )
    {
    ++i;
    sb.append("\""); // so caller knows its a quoted string
    while ( i <= b.length && (char)b[i] != '"' )
      sb.append((char)b[i++]);
    if ( i == b.length )
      return -1; // unterminated string
    return i+1;
    }
  else if ( (char)b[i] == '=' )
    {
    sb.append('=');
    return i+1;
    }
  else
    return -1; // something unexpected
  }
//...e
//...snextToken String:2:
protected static int nextToken(String s, int i, StringBuffer sb)
  {
  while ( i < s.length() && wsChar(s.charAt(i)) )
    i++;
  if ( i == s.length() )
    return -1; // no more tokens
  if ( s.charAt(i) == '?' && i+2 <= s.length() && s.charAt(i+1) == '>' )
    return -1; // no more tokens, as we hit ?>
  if ( nameStartChar(s.charAt(i)) )
    {
    sb.append(s.charAt(i++));
    while ( i < s.length() && nameChar(s.charAt(i)) )
      sb.append(s.charAt(i++));
    return i;
    }
  if ( s.charAt(i) == '"' )
    {
    ++i;
    sb.append("\""); // so caller knows its a quoted string
    while ( i <= s.length() && s.charAt(i) != '"' )
      sb.append(s.charAt(i++));
    if ( i == s.length() )
      return -1; // unterminated string
    return i+1;
    }
  else if ( s.charAt(i) == '=' )
    {
    sb.append('=');
    return i+1;
    }
  else
    return -1; // something unexpected
  }
//...e
//...e
//...sbytesToString:2:
public static String bytesToString(byte[] b, String enc)
  throws FlowProcessorException
  {
  int skip = 0;
  if ( enc == null )
    enc = "UTF-8";
  if ( enc.equalsIgnoreCase("XML") )
    // Try to autodetect encoding used, based on BOM or XML header
    {
    if ( b.length >= 3 &&
         b[0] == (byte) 0xef && b[1] == (byte) 0xbb && b[2] == (byte) 0xbf )
      {
      enc = "UTF-8";
      skip = 3;
      }
    else if ( b.length >= 2 &&
         b[0] == (byte) 0xfe && b[1] == (byte) 0xff )
      {
      enc = "UTF-16BE";
      skip = 2;
      }
    else if ( b.length >= 2 &&
         b[0] == (byte) 0xff && b[1] == (byte) 0xfe )
      {
      enc = "UTF-16LE";
      skip = 3;
      }
    else if ( b.length >= 6 &&
         b[0] == '<' && b[1] == '?' && b[2] == 'x' && b[3] == 'm' && b[4] == 'l' &&
         wsChar((char)b[5]) )
      // No BOM, but it looks like XML, so try scanning the <?xml text
      {
      String token2 = "", token1 = "", token0 = "";
      int i = 6;
      for ( ;; )
        {
        StringBuffer sb = new StringBuffer();
        i = nextToken(b, i, sb);
        if ( i == -1 )
          {
          enc = "UTF-8";
          break;
          }
        token2 = token1; token1 = token0; token0 = sb.toString();
        if ( token2.equals("encoding") && token1.equals("=") && token0.startsWith("\"") )
          {
          enc = token0.substring(1);
          if ( enc.equals("UTF-16") )
            throw new FlowProcessorException("can't convert byte[] to String using encoding XML, there was no BOM, but we found <?xml encoding=\"UTF-16\", which is illegal");
          break;
          }
        }
      }
    else
      // No BOM, not XML, might as well try UTF-8
      enc = "UTF-8";
    }
  try
    {
    return new String(b, skip, b.length-skip, enc);
    }
  catch ( UnsupportedEncodingException e )
    {
    throw new FlowProcessorException("can't convert byte[] to String using encoding "+enc, e);
    }
  }
//...e
//...sstringToBytes:2:
public static byte[] stringToBytes(String s, String enc)
  throws FlowProcessorException
  {
  if ( enc == null )
    enc = "UTF-8";
  try
    {
    if ( enc.equalsIgnoreCase("XML") )
      {
      if ( s.startsWith("<?xml") && wsChar(s.charAt(5)) )
        {
        String token2 = "", token1 = "", token0 = "";
        int i = 6;
        for ( ;; )
          {
          StringBuffer sb = new StringBuffer();
          i = nextToken(s, i, sb);
          if ( i == -1 )
            {
            enc = "UTF-8";
            break;
            }
          token2 = token1; token1 = token0; token0 = sb.toString();
          if ( token2.equals("encoding") && token1.equals("=") && token0.startsWith("\"") )
            {
            enc = token0.substring(1);
            if ( enc.equals("UTF-16BE") || enc.equals("UTF-16LE") )
              throw new FlowProcessorException("can't convert String to byte[] using encoding XML, we found <?xml encoding=\""+enc+"\", which is illegal");
            // getBytes() will return BOM preceeded big endian data, as per
            // http://java.sun.com/javase/6/docs/api/java/nio/charset/Charset.html
            break;
            }
          }
        }
      else
        enc = "UTF-8"; // no <?xml prefix, so use default
      }
    return s.getBytes(enc);
    }
  catch ( UnsupportedEncodingException e )
    {
    throw new FlowProcessorException("can't convert String to byte[] using encoding "+enc, e);
    }
  }
//...e
//...sfetchURL:2:
public static String fetchURL(String url)
  throws IOException
  {
  StringBuffer sb = new StringBuffer();
  InputStream is = new URL(url).openStream();
  InputStreamReader isr = new InputStreamReader(is);
  BufferedReader br = new BufferedReader(isr);
  String line;
  while ( (line = br.readLine()) != null )
    {
    sb.append(line);
    sb.append("\n");
    }
  br.close();
  return sb.toString();
  }
//...e
//...sgetNamespaceContext:2:
// Wherever XPath expressions are used, we also allow the use of a
// namespace context along side it. This might look like this
//   "myns=http://myns/ otherns=http://otherns/"

public static NamespaceContext getNamespaceContext(String s)
  throws FlowProcessorException
  {
  FlowNamespaceContext nsctx = new FlowNamespaceContext();
  StringTokenizer st = new StringTokenizer(s);
  while ( st.hasMoreTokens() )
    {
    String s2 = st.nextToken();
    int i = s2.indexOf("=");
    if ( i == -1 )
      throw new FlowProcessorException("invalid nsctx specification");
    nsctx.addPrefix(s2.substring(0, i), s2.substring(i+1));
    }
  return nsctx;
  }
//...e
//...ssubstituteProps:2:
// Expand the following
//   ${property}
//   ${property_${nestedproperty}}
//   ${property:default}
//   ${property:default_${nesteddefaultproperty}}

protected static int substituteProps(
  String s,         // input stream
  int i,            // cursor in input stream
  Properties p,     // for looking up properties in
  String delims,    // stop scanning if this is found
  StringBuffer sb   // output stream
  )
  throws FlowProcessorException
  {
  int len = s.length();
  while ( i < len )
    {
    char c = s.charAt(i);
    if ( delims.indexOf(c) != -1 )
      break;
    ++i;
    if ( c != '$' )
      sb.append(c);
    else if ( i == len )
      sb.append('$');
    else
      {
      c = s.charAt(i);
      if ( c != '{' )
        sb.append('$');
      else
        {
        StringBuffer sb2 = new StringBuffer();
        i = substituteProps(s, i+1, p, "}:", sb2);
        if ( i == len )
          throw new FlowProcessorException("unterminated ${ property or system property");
        String pvDef = null;
        if ( s.charAt(i) == ':' )
          {
          StringBuffer sb3 = new StringBuffer();
          i = substituteProps(s, i+1, p, "}", sb3);
          if ( i == len )
            throw new FlowProcessorException("unterminated default value in ${ property or system property");
          pvDef = sb3.toString();
          }
        ++i; // skip past }
        String pn = sb2.toString();
        String pv;
        if ( (pv = p.getProperty(pn)) != null )
          ; // found a property
        else if ( (pv = System.getProperty(pn)) != null )
          ; // found a system property
        else if ( pvDef != null )
          pv = pvDef;
        else
          throw new FlowProcessorException("can't expand ${"+pn+"} as property or system property");
        sb.append(pv);
        }
      }
    }
  return i;
  }

public static String substituteProps(String s, Properties p)
  throws FlowProcessorException
  {
  StringBuffer sb = new StringBuffer();
  substituteProps(s, 0, p, "", sb);
  return sb.toString();
  }
//...e
//...sdiagForm\40\Node\41\:2:
// Here is logic to return a String representation of a Documents as a tree of
// nodes, but note that this does not understand all node types and does not
// display all the available attributes/properties of some of the node types.
// It will however, show the elements and associated attributes, and any text,
// with a view to helping the user develop XSLT transformations with reasonable
// behaviour with respect to whitespace handling.

public static String diagForm(Node node, String indent)
  {
  switch ( node.getNodeType() )
    {
    case Node.DOCUMENT_NODE:
      {
      Document d = (Document)node;
      StringBuffer sb = new StringBuffer(indent+"<?xml...\n");
      if ( d.getDoctype() != null )
        sb.append(indent+"<!DOCTYPE...\n");
      sb.append(diagForm(d.getDocumentElement(), indent));
      return sb.toString();
      }
    case Node.DOCUMENT_TYPE_NODE:
      return indent+"<!DOCTYPE "+node.getNodeName()+">\n";
    case Node.ELEMENT_NODE:
      {
      StringBuffer sb = new StringBuffer(indent+"<"+node.getNodeName());
      NamedNodeMap nnm = node.getAttributes();
      for ( int i = 0; i < nnm.getLength(); i++ )
        {
        Node a = nnm.item(i);
        sb.append(" "+a.getNodeName()+"=\""+a.getNodeValue()+"\"");
        }
      NodeList nl = node.getChildNodes();
      if ( nl != null && nl.getLength() != 0 )
        {
        sb.append(">\n");
        for ( int i = 0; i < nl.getLength(); i++ )
          sb.append(diagForm(nl.item(i), indent+"- "));
        sb.append(indent+"</"+node.getNodeName()+">\n");
        }
      else
        sb.append("/>\n");
      return sb.toString();
      }
    case Node.ENTITY_REFERENCE_NODE:
      return indent+"&"+node.getNodeValue()+";\n";
    case Node.CDATA_SECTION_NODE:
      return indent+"<![CDATA["+node.getNodeValue()+"]]>\n";
    case Node.TEXT_NODE:
      return indent+"\""+node.getNodeValue()+"\"\n";
    case Node.PROCESSING_INSTRUCTION_NODE:
      return indent+"<?"+node.getNodeName()+" "+node.getNodeValue()+"?>\n";
    case Node.COMMENT_NODE:
      return indent+"<!-- "+node.getNodeValue()+" -->\n";
    case Node.ATTRIBUTE_NODE:
      // Note that Node.ELEMENT_NODE doesn't recurse to use this
      // This branch only used if the caller explicitly passes in an attribute
      return indent+node.getNodeName()+"=\""+node.getNodeValue()+"\"\n";
    }
  return indent+"?\n";
  }
//...e
//...sdiagForm\40\Character\41\:2:
protected static String diagForm(Character c)
  {
  String hex = "0123456789abcdef";
  char ch = c.charValue();
  if ( ch == '\'' )
    return "'\\\'";
  else if ( ch == '\\' )
    return "'\\\\'";
  else if ( ch >= ' ' && ch <= '~' )
    return "'"+ch+"'";
  else if ( ch == '\t' )
    return "'\\t'";
  else if ( ch == '\n' )
    return "'\\n'";
  else if ( ch == '\r' )
    return "'\\r'";
  else if ( ch == '\b' )
    return "'\\b'";
  else
    return "'\\u"+
           hex.charAt((ch>>>12)&0xf)+
           hex.charAt((ch>>> 8)&0xf)+
           hex.charAt((ch>>> 4)&0xf)+
           hex.charAt( ch      &0xf)+
           "'";
  }
//...e
//...sdiagForm\40\byte\91\\93\\41\:2:
protected static String diagForm(byte[] b)
    {
    String hex = "0123456789abcdef";
    StringBuffer sb = new StringBuffer();
    String sep = "";
    for ( int i = 0; i < b.length; i++ )
      {
      if ( i == 10 )
        {
        sb.append(",...");
        break;
        }
      sb.append(sep);
      sb.append("0x");
      sb.append(hex.charAt((b[i]>>>4)&0xf));
      sb.append(hex.charAt( b[i]     &0xf));
      sep = ",";
      }
    sb.append(" ("+b.length+" bytes)");
    return sb.toString();
    }
//...e
//...sdiagForm\40\Exception\41\:2:
protected static String diagForm(Exception e)
  {
  CharArrayWriter caw = new CharArrayWriter();
  PrintWriter pw = new PrintWriter(caw);
  e.printStackTrace(pw);
  pw.flush();
  return caw.toString();
  }
//...e
//...sdiagForm\40\Object\41\:2:
// This code to return a diagnostic String representation of an Object
// has some significant limitations.
// The data is returned in a format intended to try to make it clear what the
// data is, and what type it is, without necessarily making it easy or
// possible to cut and paste it out and use it, or process it further.
// Not all the available data is presented (eg: only the first few bytes of
// anything that is a byte[]).
// You don't get to choose the way in which the data is presented - each
// Java class is presented using a particular fixed format.

public static String diagForm(Object o)
  {
  if ( o instanceof String )
    return "String \""+(String) o+"\"";
  else if ( o instanceof Document )
    return "Document\n"+diagForm((Document)o, "        ");
  else if ( o instanceof Node )
    return "Node\n"+diagForm((Node)o, "        ");
  else if ( o instanceof Boolean )
    return "(Boolean) "+((Boolean)o).toString();
  else if ( o instanceof Byte )
    return "Byte "+((Byte)o).byteValue();
  else if ( o instanceof Character )
    return "Character "+diagForm((Character)o);
  else if ( o instanceof Short )
    return "Short "+((Short)o).shortValue();
  else if ( o instanceof Integer )
    return "Integer "+((Integer)o).intValue();
  else if ( o instanceof Long )
    return "Long "+((Long)o).intValue();
  else if ( o instanceof Float )
    return "Float "+((Float)o).floatValue();
  else if ( o instanceof Double )
    return "Double "+((Double)o).doubleValue();
  else if ( o instanceof byte[] )
    return "byte[] "+diagForm((byte[])o);
  else if ( o instanceof FlowNullValue )
    return "Null";
  else if ( o instanceof Exception )
    return "Exception "+diagForm((Exception)o);
  else
    return "Object "+o.toString();
  }
//...e
//...sdiagForm\40\FlowContext\41\:2:
public static String diagForm(
  FlowContext c,
  boolean includemessage,
  boolean includevars,
  String vars
  )
  throws JMSException
  {
  StringBuffer sb = new StringBuffer();
  if ( includemessage )
//...slog message:6:
{
sb.append("Message propertes:\n");
JmsaMessage m = c.getMessage();
@SuppressWarnings({"unchecked"}) // ugly, but necessary
Enumeration<String> e = (Enumeration<String>) m.getPropertyNames();
while ( e.hasMoreElements() )
  {
  String p = e.nextElement();
  Object o = m.getObjectProperty(p);
  sb.append("  prop \""+p+"\" = "+diagForm(o)+"\n");
  }
if ( m instanceof JmsaBytesMessage )
  {
  JmsaBytesMessage m2 = (JmsaBytesMessage) m;
  m2.reset(); // make readable
  int len = (int) m2.getBodyLength(); 
  byte[] b = new byte[len];
  m2.readBytes(b);
  sb.append("Message body (BytesMessage):\n");
  sb.append("  bytes = "+diagForm(b)+"\n");
  m2.reset(); // reset to start
  }
else if ( m instanceof JmsaTextMessage )
  {
  JmsaTextMessage m2 = (JmsaTextMessage) m;
  sb.append("Message body (TextMessage):\n");
  sb.append("  text = \""+m2.getText()+"\"\n");
  }
else if ( m instanceof JmsaMapMessage )
  {
  JmsaMapMessage m2 = (JmsaMapMessage) m;
  sb.append("Message body (MapMessage):\n");
  @SuppressWarnings({"unchecked"}) // ugly, but necessary
  Enumeration<String> e2 = (Enumeration<String>) m2.getMapNames();
  while ( e2.hasMoreElements() )
    {
    String i = e2.nextElement();
    Object o = m2.getObject(i);
    sb.append("  item \""+i+"\" = "+diagForm(o)+"\n");
    } 
  }
else if ( m instanceof JmsaStreamMessage )
  {
  JmsaStreamMessage m2 = (JmsaStreamMessage) m;
  m2.reset(); // make readable 
  sb.append("Message body (StreamMessage):\n");
  for ( int i = 0; ; i++ )
    try
      {
      Object o = m2.readObject();
      sb.append("  stream["+i+"] = "+diagForm(o)+"\n");
      }
    catch ( MessageEOFException me )
      {
      break;
      }
  m2.reset(); // reset to start
  }
else if ( m instanceof JmsaObjectMessage )
  {
  JmsaObjectMessage m2 = (JmsaObjectMessage) m;
  Serializable o = m2.getObject();
  sb.append("Message body (ObjectMessage):\n");
  sb.append("  object = "+diagForm(o)+"\n");
  }
else
  {
  sb.append("Message body (Message):\n");
  sb.append("  nothing\n");
  }
}
//...e
  if ( includevars )
//...slog variables:6:
{
sb.append("Variables:\n");
if ( vars != null )
  {
  StringTokenizer st = new StringTokenizer(vars, ", ");
  while ( st.hasMoreTokens() )
    {
    String var = st.nextToken();
    Object o = c.getVar(var);
    if ( o != null )
      sb.append("  var \""+var+"\" = "+diagForm(o)+"\n");
    else
      sb.append("  var \""+var+"\" undefined\n");
    }
  }
else
  {  
  Map<String,Object> map = c.getVars();
  for ( String var : map.keySet() )
    sb.append("  var \""+var+"\" = "+diagForm(map.get(var))+"\n");
  }
}
//...e
  return sb.toString();
  }
//...e
  }
