001/* 002 * 003 * Licensed to the Apache Software Foundation (ASF) under one 004 * or more contributor license agreements. See the NOTICE file 005 * distributed with this work for additional information 006 * regarding copyright ownership. The ASF licenses this file 007 * to you under the Apache License, Version 2.0 (the 008 * "License"); you may not use this file except in compliance 009 * with the License. You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, 014 * software distributed under the License is distributed on an 015 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 016 * KIND, either express or implied. See the License for the 017 * specific language governing permissions and limitations 018 * under the License. 019 * 020 */ 021package org.apache.activemq.transport.amqp.message; 022 023import java.nio.ByteBuffer; 024import java.util.UUID; 025 026import org.apache.activemq.transport.amqp.AmqpProtocolException; 027import org.apache.qpid.proton.amqp.Binary; 028import org.apache.qpid.proton.amqp.UnsignedLong; 029 030/** 031 * Helper class for identifying and converting message-id and correlation-id values between 032 * the AMQP types and the Strings values used by JMS. 033 * 034 * <p>AMQP messages allow for 4 types of message-id/correlation-id: message-id-string, message-id-binary, 035 * message-id-uuid, or message-id-ulong. In order to accept or return a string representation of these 036 * for interoperability with other AMQP clients, the following encoding can be used after removing or 037 * before adding the "ID:" prefix used for a JMSMessageID value:<br> 038 * 039 * {@literal "AMQP_BINARY:<hex representation of binary content>"}<br> 040 * {@literal "AMQP_UUID:<string representation of uuid>"}<br> 041 * {@literal "AMQP_ULONG:<string representation of ulong>"}<br> 042 * {@literal "AMQP_STRING:<string>"}<br> 043 * 044 * <p>The AMQP_STRING encoding exists only for escaping message-id-string values that happen to begin 045 * with one of the encoding prefixes (including AMQP_STRING itself). It MUST NOT be used otherwise. 046 * 047 * <p>When provided a string for conversion which attempts to identify itself as an encoded binary, uuid, or 048 * ulong but can't be converted into the indicated format, an exception will be thrown. 049 * 050 */ 051public class AMQPMessageIdHelper { 052 053 public static final AMQPMessageIdHelper INSTANCE = new AMQPMessageIdHelper(); 054 055 public static final String AMQP_STRING_PREFIX = "AMQP_STRING:"; 056 public static final String AMQP_UUID_PREFIX = "AMQP_UUID:"; 057 public static final String AMQP_ULONG_PREFIX = "AMQP_ULONG:"; 058 public static final String AMQP_BINARY_PREFIX = "AMQP_BINARY:"; 059 060 private static final int AMQP_UUID_PREFIX_LENGTH = AMQP_UUID_PREFIX.length(); 061 private static final int AMQP_ULONG_PREFIX_LENGTH = AMQP_ULONG_PREFIX.length(); 062 private static final int AMQP_STRING_PREFIX_LENGTH = AMQP_STRING_PREFIX.length(); 063 private static final int AMQP_BINARY_PREFIX_LENGTH = AMQP_BINARY_PREFIX.length(); 064 private static final char[] HEX_CHARS = "0123456789ABCDEF".toCharArray(); 065 066 /** 067 * Takes the provided AMQP messageId style object, and convert it to a base string. 068 * Encodes type information as a prefix where necessary to convey or escape the type 069 * of the provided object. 070 * 071 * @param messageId 072 * the raw messageId object to process 073 * 074 * @return the base string to be used in creating the actual id. 075 */ 076 public String toBaseMessageIdString(Object messageId) { 077 if (messageId == null) { 078 return null; 079 } else if (messageId instanceof String) { 080 String stringId = (String) messageId; 081 082 // If the given string has a type encoding prefix, 083 // we need to escape it as an encoded string (even if 084 // the existing encoding prefix was also for string) 085 if (hasTypeEncodingPrefix(stringId)) { 086 return AMQP_STRING_PREFIX + stringId; 087 } else { 088 return stringId; 089 } 090 } else if (messageId instanceof UUID) { 091 return AMQP_UUID_PREFIX + messageId.toString(); 092 } else if (messageId instanceof UnsignedLong) { 093 return AMQP_ULONG_PREFIX + messageId.toString(); 094 } else if (messageId instanceof Binary) { 095 ByteBuffer dup = ((Binary) messageId).asByteBuffer(); 096 097 byte[] bytes = new byte[dup.remaining()]; 098 dup.get(bytes); 099 100 String hex = convertBinaryToHexString(bytes); 101 102 return AMQP_BINARY_PREFIX + hex; 103 } else { 104 throw new IllegalArgumentException("Unsupported type provided: " + messageId.getClass()); 105 } 106 } 107 108 /** 109 * Takes the provided base id string and return the appropriate amqp messageId style object. 110 * Converts the type based on any relevant encoding information found as a prefix. 111 * 112 * @param baseId 113 * the object to be converted to an AMQP MessageId value. 114 * 115 * @return the AMQP messageId style object 116 * 117 * @throws AmqpProtocolException if the provided baseId String indicates an encoded type but can't be converted to that type. 118 */ 119 public Object toIdObject(String baseId) throws AmqpProtocolException { 120 if (baseId == null) { 121 return null; 122 } 123 124 try { 125 if (hasAmqpUuidPrefix(baseId)) { 126 String uuidString = strip(baseId, AMQP_UUID_PREFIX_LENGTH); 127 return UUID.fromString(uuidString); 128 } else if (hasAmqpUlongPrefix(baseId)) { 129 String longString = strip(baseId, AMQP_ULONG_PREFIX_LENGTH); 130 return UnsignedLong.valueOf(longString); 131 } else if (hasAmqpStringPrefix(baseId)) { 132 return strip(baseId, AMQP_STRING_PREFIX_LENGTH); 133 } else if (hasAmqpBinaryPrefix(baseId)) { 134 String hexString = strip(baseId, AMQP_BINARY_PREFIX_LENGTH); 135 byte[] bytes = convertHexStringToBinary(hexString); 136 return new Binary(bytes); 137 } else { 138 // We have a string without any type prefix, transmit it as-is. 139 return baseId; 140 } 141 } catch (IllegalArgumentException e) { 142 throw new AmqpProtocolException("Unable to convert ID value"); 143 } 144 } 145 146 /** 147 * Convert the provided hex-string into a binary representation where each byte represents 148 * two characters of the hex string. 149 * 150 * The hex characters may be upper or lower case. 151 * 152 * @param hexString 153 * string to convert to a binary value. 154 * 155 * @return a byte array containing the binary representation 156 * 157 * @throws IllegalArgumentException if the provided String is a non-even length or contains 158 * non-hex characters 159 */ 160 public byte[] convertHexStringToBinary(String hexString) throws IllegalArgumentException { 161 int length = hexString.length(); 162 163 // As each byte needs two characters in the hex encoding, the string must be an even length. 164 if (length % 2 != 0) { 165 throw new IllegalArgumentException("The provided hex String must be an even length, but was of length " + length + ": " + hexString); 166 } 167 168 byte[] binary = new byte[length / 2]; 169 170 for (int i = 0; i < length; i += 2) { 171 char highBitsChar = hexString.charAt(i); 172 char lowBitsChar = hexString.charAt(i + 1); 173 174 int highBits = hexCharToInt(highBitsChar, hexString) << 4; 175 int lowBits = hexCharToInt(lowBitsChar, hexString); 176 177 binary[i / 2] = (byte) (highBits + lowBits); 178 } 179 180 return binary; 181 } 182 183 /** 184 * Convert the provided binary into a hex-string representation where each character 185 * represents 4 bits of the provided binary, i.e each byte requires two characters. 186 * 187 * The returned hex characters are upper-case. 188 * 189 * @param bytes 190 * the binary value to convert to a hex String instance. 191 * 192 * @return a String containing a hex representation of the bytes 193 */ 194 public String convertBinaryToHexString(byte[] bytes) { 195 // Each byte is represented as 2 chars 196 StringBuilder builder = new StringBuilder(bytes.length * 2); 197 198 for (byte b : bytes) { 199 // The byte will be expanded to int before shifting, replicating the 200 // sign bit, so mask everything beyond the first 4 bits afterwards 201 int highBitsInt = (b >> 4) & 0xF; 202 // We only want the first 4 bits 203 int lowBitsInt = b & 0xF; 204 205 builder.append(HEX_CHARS[highBitsInt]); 206 builder.append(HEX_CHARS[lowBitsInt]); 207 } 208 209 return builder.toString(); 210 } 211 212 //----- Internal implementation ------------------------------------------// 213 214 private boolean hasTypeEncodingPrefix(String stringId) { 215 return hasAmqpBinaryPrefix(stringId) || hasAmqpUuidPrefix(stringId) || 216 hasAmqpUlongPrefix(stringId) || hasAmqpStringPrefix(stringId); 217 } 218 219 private boolean hasAmqpStringPrefix(String stringId) { 220 return stringId.startsWith(AMQP_STRING_PREFIX); 221 } 222 223 private boolean hasAmqpUlongPrefix(String stringId) { 224 return stringId.startsWith(AMQP_ULONG_PREFIX); 225 } 226 227 private boolean hasAmqpUuidPrefix(String stringId) { 228 return stringId.startsWith(AMQP_UUID_PREFIX); 229 } 230 231 private boolean hasAmqpBinaryPrefix(String stringId) { 232 return stringId.startsWith(AMQP_BINARY_PREFIX); 233 } 234 235 private String strip(String id, int numChars) { 236 return id.substring(numChars); 237 } 238 239 private int hexCharToInt(char ch, String orig) throws IllegalArgumentException { 240 if (ch >= '0' && ch <= '9') { 241 // subtract '0' to get difference in position as an int 242 return ch - '0'; 243 } else if (ch >= 'A' && ch <= 'F') { 244 // subtract 'A' to get difference in position as an int 245 // and then add 10 for the offset of 'A' 246 return ch - 'A' + 10; 247 } else if (ch >= 'a' && ch <= 'f') { 248 // subtract 'a' to get difference in position as an int 249 // and then add 10 for the offset of 'a' 250 return ch - 'a' + 10; 251 } 252 253 throw new IllegalArgumentException("The provided hex string contains non-hex character '" + ch + "': " + orig); 254 } 255}