content: /*
   *
   * Licensed to the Apache Software Foundation (ASF) under one
   * or more contributor license agreements.  See the NOTICE file
   * distributed with this work for additional information
   * regarding copyright ownership.  The ASF licenses this file
   * to you under the Apache License, Version 2.0 (the
   * "License"); you may not use this file except in compliance
   * with the License.  You may obtain a copy of the License at
   *
   *   http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing,
   * software distributed under the License is distributed on an
   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   * KIND, either express or implied.  See the License for the
   * specific language governing permissions and limitations
   * under the License.
   *
   */
  package org.apache.activemq.transport.amqp.message;
  
  import java.nio.ByteBuffer;
  import java.util.UUID;
  
  import org.apache.activemq.transport.amqp.AmqpProtocolException;
  import org.apache.qpid.proton.amqp.Binary;
  import org.apache.qpid.proton.amqp.UnsignedLong;
  
  /**
   * Helper class for identifying and converting message-id and correlation-id values between
   * the AMQP types and the Strings values used by JMS.
   *
   * <p>AMQP messages allow for 4 types of message-id/correlation-id: message-id-string, message-id-binary,
   * message-id-uuid, or message-id-ulong. In order to accept or return a string representation of these
   * for interoperability with other AMQP clients, the following encoding can be used after removing or
   * before adding the "ID:" prefix used for a JMSMessageID value:<br>
   *
   * {@literal "AMQP_BINARY:<hex representation of binary content>"}<br>
   * {@literal "AMQP_UUID:<string representation of uuid>"}<br>
   * {@literal "AMQP_ULONG:<string representation of ulong>"}<br>
   * {@literal "AMQP_STRING:<string>"}<br>
   *
   * <p>The AMQP_STRING encoding exists only for escaping message-id-string values that happen to begin
   * with one of the encoding prefixes (including AMQP_STRING itself). It MUST NOT be used otherwise.
   *
   * <p>When provided a string for conversion which attempts to identify itself as an encoded binary, uuid, or
   * ulong but can't be converted into the indicated format, an exception will be thrown.
   */
  public class AMQPMessageIdHelper {
  
      public static final AMQPMessageIdHelper INSTANCE = new AMQPMessageIdHelper();
  
      public static final String AMQP_STRING_PREFIX = "AMQP_STRING:";
      public static final String AMQP_UUID_PREFIX = "AMQP_UUID:";
      public static final String AMQP_ULONG_PREFIX = "AMQP_ULONG:";
      public static final String AMQP_BINARY_PREFIX = "AMQP_BINARY:";
  
      private static final int AMQP_UUID_PREFIX_LENGTH = AMQP_UUID_PREFIX.length();
      private static final int AMQP_ULONG_PREFIX_LENGTH = AMQP_ULONG_PREFIX.length();
      private static final int AMQP_STRING_PREFIX_LENGTH = AMQP_STRING_PREFIX.length();
      private static final int AMQP_BINARY_PREFIX_LENGTH = AMQP_BINARY_PREFIX.length();
      private static final char[] HEX_CHARS = "0123456789ABCDEF".toCharArray();
  
      /**
       * Takes the provided AMQP messageId style object, and convert it to a base string.
       * Encodes type information as a prefix where necessary to convey or escape the type
       * of the provided object.
       *
       * @param messageId
       *      the raw messageId object to process
       *
       * @return the base string to be used in creating the actual id.
       */
      public String toBaseMessageIdString(Object messageId) {
          if (messageId == null) {
              return null;
          } else if (messageId instanceof String) {
              String stringId = (String) messageId;
  
              // If the given string has a type encoding prefix,
              // we need to escape it as an encoded string (even if
              // the existing encoding prefix was also for string)
              if (hasTypeEncodingPrefix(stringId)) {
                  return AMQP_STRING_PREFIX + stringId;
              } else {
                  return stringId;
              }
          } else if (messageId instanceof UUID) {
              return AMQP_UUID_PREFIX + messageId.toString();
          } else if (messageId instanceof UnsignedLong) {
              return AMQP_ULONG_PREFIX + messageId.toString();
          } else if (messageId instanceof Binary) {
              ByteBuffer dup = ((Binary) messageId).asByteBuffer();
  
              byte[] bytes = new byte[dup.remaining()];
              dup.get(bytes);
  
              String hex = convertBinaryToHexString(bytes);
  
              return AMQP_BINARY_PREFIX + hex;
          } else {
              throw new IllegalArgumentException("Unsupported type provided: " + messageId.getClass());
          }
      }
  
      /**
       * Takes the provided base id string and return the appropriate amqp messageId style object.
       * Converts the type based on any relevant encoding information found as a prefix.
       *
       * @param baseId
       *      the object to be converted to an AMQP MessageId value.
       *
       * @return the AMQP messageId style object
       *
       * @throws AmqpProtocolException if the provided baseId String indicates an encoded type but can't be converted to that type.
       */
      public Object toIdObject(String baseId) throws AmqpProtocolException {
          if (baseId == null) {
              return null;
          }
  
          try {
              if (hasAmqpUuidPrefix(baseId)) {
                  String uuidString = strip(baseId, AMQP_UUID_PREFIX_LENGTH);
                  return UUID.fromString(uuidString);
              } else if (hasAmqpUlongPrefix(baseId)) {
                  String longString = strip(baseId, AMQP_ULONG_PREFIX_LENGTH);
                  return UnsignedLong.valueOf(longString);
              } else if (hasAmqpStringPrefix(baseId)) {
                  return strip(baseId, AMQP_STRING_PREFIX_LENGTH);
              } else if (hasAmqpBinaryPrefix(baseId)) {
                  String hexString = strip(baseId, AMQP_BINARY_PREFIX_LENGTH);
                  byte[] bytes = convertHexStringToBinary(hexString);
                  return new Binary(bytes);
              } else {
                  // We have a string without any type prefix, transmit it as-is.
                  return baseId;
              }
          } catch (IllegalArgumentException e) {
              throw new AmqpProtocolException("Unable to convert ID value");
          }
      }
  
      /**
       * Convert the provided hex-string into a binary representation where each byte represents
       * two characters of the hex string.
       *
       * The hex characters may be upper or lower case.
       *
       * @param hexString
       *      string to convert to a binary value.
       *
       * @return a byte array containing the binary representation
       *
       * @throws IllegalArgumentException if the provided String is a non-even length or contains
       *                                  non-hex characters
       */
      public byte[] convertHexStringToBinary(String hexString) throws IllegalArgumentException {
          int length = hexString.length();
  
          // As each byte needs two characters in the hex encoding, the string must be an even length.
          if (length % 2 != 0) {
              throw new IllegalArgumentException("The provided hex String must be an even length, but was of length " + length + ": " + hexString);
          }
  
          byte[] binary = new byte[length / 2];
  
          for (int i = 0; i < length; i += 2) {
              char highBitsChar = hexString.charAt(i);
              char lowBitsChar = hexString.charAt(i + 1);
  
              int highBits = hexCharToInt(highBitsChar, hexString) << 4;
              int lowBits = hexCharToInt(lowBitsChar, hexString);
  
              binary[i / 2] = (byte) (highBits + lowBits);
          }
  
          return binary;
      }
  
      /**
       * Convert the provided binary into a hex-string representation where each character
       * represents 4 bits of the provided binary, i.e each byte requires two characters.
       *
       * The returned hex characters are upper-case.
       *
       * @param bytes
       *      the binary value to convert to a hex String instance.
       *
       * @return a String containing a hex representation of the bytes
       */
      public String convertBinaryToHexString(byte[] bytes) {
          // Each byte is represented as 2 chars
          StringBuilder builder = new StringBuilder(bytes.length * 2);
  
          for (byte b : bytes) {
              // The byte will be expanded to int before shifting, replicating the
              // sign bit, so mask everything beyond the first 4 bits afterwards
              int highBitsInt = (b >> 4) & 0xF;
              // We only want the first 4 bits
              int lowBitsInt = b & 0xF;
  
              builder.append(HEX_CHARS[highBitsInt]);
              builder.append(HEX_CHARS[lowBitsInt]);
          }
  
          return builder.toString();
      }
  
      //----- Internal implementation ------------------------------------------//
  
      private boolean hasTypeEncodingPrefix(String stringId) {
          return hasAmqpBinaryPrefix(stringId) || hasAmqpUuidPrefix(stringId) ||
                 hasAmqpUlongPrefix(stringId) || hasAmqpStringPrefix(stringId);
      }
  
      private boolean hasAmqpStringPrefix(String stringId) {
          return stringId.startsWith(AMQP_STRING_PREFIX);
      }
  
      private boolean hasAmqpUlongPrefix(String stringId) {
          return stringId.startsWith(AMQP_ULONG_PREFIX);
      }
  
      private boolean hasAmqpUuidPrefix(String stringId) {
          return stringId.startsWith(AMQP_UUID_PREFIX);
      }
  
      private boolean hasAmqpBinaryPrefix(String stringId) {
          return stringId.startsWith(AMQP_BINARY_PREFIX);
      }
  
      private String strip(String id, int numChars) {
          return id.substring(numChars);
      }
  
      private int hexCharToInt(char ch, String orig) throws IllegalArgumentException {
          if (ch >= '0' && ch <= '9') {
              // subtract '0' to get difference in position as an int
              return ch - '0';
          } else if (ch >= 'A' && ch <= 'F') {
              // subtract 'A' to get difference in position as an int
              // and then add 10 for the offset of 'A'
              return ch - 'A' + 10;
          } else if (ch >= 'a' && ch <= 'f') {
              // subtract 'a' to get difference in position as an int
              // and then add 10 for the offset of 'a'
              return ch - 'a' + 10;
          }
  
          throw new IllegalArgumentException("The provided hex string contains non-hex character '" + ch + "': " + orig);
      }
  }
  