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 */ 050public class AMQPMessageIdHelper { 051 052 public static final AMQPMessageIdHelper INSTANCE = new AMQPMessageIdHelper(); 053 054 public static final String AMQP_STRING_PREFIX = "AMQP_STRING:"; 055 public static final String AMQP_UUID_PREFIX = "AMQP_UUID:"; 056 public static final String AMQP_ULONG_PREFIX = "AMQP_ULONG:"; 057 public static final String AMQP_BINARY_PREFIX = "AMQP_BINARY:"; 058 059 private static final int AMQP_UUID_PREFIX_LENGTH = AMQP_UUID_PREFIX.length(); 060 private static final int AMQP_ULONG_PREFIX_LENGTH = AMQP_ULONG_PREFIX.length(); 061 private static final int AMQP_STRING_PREFIX_LENGTH = AMQP_STRING_PREFIX.length(); 062 private static final int AMQP_BINARY_PREFIX_LENGTH = AMQP_BINARY_PREFIX.length(); 063 private static final char[] HEX_CHARS = "0123456789ABCDEF".toCharArray(); 064 065 /** 066 * Takes the provided AMQP messageId style object, and convert it to a base string. 067 * Encodes type information as a prefix where necessary to convey or escape the type 068 * of the provided object. 069 * 070 * @param messageId 071 * the raw messageId object to process 072 * 073 * @return the base string to be used in creating the actual id. 074 */ 075 public String toBaseMessageIdString(Object messageId) { 076 if (messageId == null) { 077 return null; 078 } else if (messageId instanceof String) { 079 String stringId = (String) messageId; 080 081 // If the given string has a type encoding prefix, 082 // we need to escape it as an encoded string (even if 083 // the existing encoding prefix was also for string) 084 if (hasTypeEncodingPrefix(stringId)) { 085 return AMQP_STRING_PREFIX + stringId; 086 } else { 087 return stringId; 088 } 089 } else if (messageId instanceof UUID) { 090 return AMQP_UUID_PREFIX + messageId.toString(); 091 } else if (messageId instanceof UnsignedLong) { 092 return AMQP_ULONG_PREFIX + messageId.toString(); 093 } else if (messageId instanceof Binary) { 094 ByteBuffer dup = ((Binary) messageId).asByteBuffer(); 095 096 byte[] bytes = new byte[dup.remaining()]; 097 dup.get(bytes); 098 099 String hex = convertBinaryToHexString(bytes); 100 101 return AMQP_BINARY_PREFIX + hex; 102 } else { 103 throw new IllegalArgumentException("Unsupported type provided: " + messageId.getClass()); 104 } 105 } 106 107 /** 108 * Takes the provided base id string and return the appropriate amqp messageId style object. 109 * Converts the type based on any relevant encoding information found as a prefix. 110 * 111 * @param baseId 112 * the object to be converted to an AMQP MessageId value. 113 * 114 * @return the AMQP messageId style object 115 * 116 * @throws AmqpProtocolException if the provided baseId String indicates an encoded type but can't be converted to that type. 117 */ 118 public Object toIdObject(String baseId) throws AmqpProtocolException { 119 if (baseId == null) { 120 return null; 121 } 122 123 try { 124 if (hasAmqpUuidPrefix(baseId)) { 125 String uuidString = strip(baseId, AMQP_UUID_PREFIX_LENGTH); 126 return UUID.fromString(uuidString); 127 } else if (hasAmqpUlongPrefix(baseId)) { 128 String longString = strip(baseId, AMQP_ULONG_PREFIX_LENGTH); 129 return UnsignedLong.valueOf(longString); 130 } else if (hasAmqpStringPrefix(baseId)) { 131 return strip(baseId, AMQP_STRING_PREFIX_LENGTH); 132 } else if (hasAmqpBinaryPrefix(baseId)) { 133 String hexString = strip(baseId, AMQP_BINARY_PREFIX_LENGTH); 134 byte[] bytes = convertHexStringToBinary(hexString); 135 return new Binary(bytes); 136 } else { 137 // We have a string without any type prefix, transmit it as-is. 138 return baseId; 139 } 140 } catch (IllegalArgumentException e) { 141 throw new AmqpProtocolException("Unable to convert ID value"); 142 } 143 } 144 145 /** 146 * Convert the provided hex-string into a binary representation where each byte represents 147 * two characters of the hex string. 148 * 149 * The hex characters may be upper or lower case. 150 * 151 * @param hexString 152 * string to convert to a binary value. 153 * 154 * @return a byte array containing the binary representation 155 * 156 * @throws IllegalArgumentException if the provided String is a non-even length or contains 157 * non-hex characters 158 */ 159 public byte[] convertHexStringToBinary(String hexString) throws IllegalArgumentException { 160 int length = hexString.length(); 161 162 // As each byte needs two characters in the hex encoding, the string must be an even length. 163 if (length % 2 != 0) { 164 throw new IllegalArgumentException("The provided hex String must be an even length, but was of length " + length + ": " + hexString); 165 } 166 167 byte[] binary = new byte[length / 2]; 168 169 for (int i = 0; i < length; i += 2) { 170 char highBitsChar = hexString.charAt(i); 171 char lowBitsChar = hexString.charAt(i + 1); 172 173 int highBits = hexCharToInt(highBitsChar, hexString) << 4; 174 int lowBits = hexCharToInt(lowBitsChar, hexString); 175 176 binary[i / 2] = (byte) (highBits + lowBits); 177 } 178 179 return binary; 180 } 181 182 /** 183 * Convert the provided binary into a hex-string representation where each character 184 * represents 4 bits of the provided binary, i.e each byte requires two characters. 185 * 186 * The returned hex characters are upper-case. 187 * 188 * @param bytes 189 * the binary value to convert to a hex String instance. 190 * 191 * @return a String containing a hex representation of the bytes 192 */ 193 public String convertBinaryToHexString(byte[] bytes) { 194 // Each byte is represented as 2 chars 195 StringBuilder builder = new StringBuilder(bytes.length * 2); 196 197 for (byte b : bytes) { 198 // The byte will be expanded to int before shifting, replicating the 199 // sign bit, so mask everything beyond the first 4 bits afterwards 200 int highBitsInt = (b >> 4) & 0xF; 201 // We only want the first 4 bits 202 int lowBitsInt = b & 0xF; 203 204 builder.append(HEX_CHARS[highBitsInt]); 205 builder.append(HEX_CHARS[lowBitsInt]); 206 } 207 208 return builder.toString(); 209 } 210 211 //----- Internal implementation ------------------------------------------// 212 213 private boolean hasTypeEncodingPrefix(String stringId) { 214 return hasAmqpBinaryPrefix(stringId) || hasAmqpUuidPrefix(stringId) || 215 hasAmqpUlongPrefix(stringId) || hasAmqpStringPrefix(stringId); 216 } 217 218 private boolean hasAmqpStringPrefix(String stringId) { 219 return stringId.startsWith(AMQP_STRING_PREFIX); 220 } 221 222 private boolean hasAmqpUlongPrefix(String stringId) { 223 return stringId.startsWith(AMQP_ULONG_PREFIX); 224 } 225 226 private boolean hasAmqpUuidPrefix(String stringId) { 227 return stringId.startsWith(AMQP_UUID_PREFIX); 228 } 229 230 private boolean hasAmqpBinaryPrefix(String stringId) { 231 return stringId.startsWith(AMQP_BINARY_PREFIX); 232 } 233 234 private String strip(String id, int numChars) { 235 return id.substring(numChars); 236 } 237 238 private int hexCharToInt(char ch, String orig) throws IllegalArgumentException { 239 if (ch >= '0' && ch <= '9') { 240 // subtract '0' to get difference in position as an int 241 return ch - '0'; 242 } else if (ch >= 'A' && ch <= 'F') { 243 // subtract 'A' to get difference in position as an int 244 // and then add 10 for the offset of 'A' 245 return ch - 'A' + 10; 246 } else if (ch >= 'a' && ch <= 'f') { 247 // subtract 'a' to get difference in position as an int 248 // and then add 10 for the offset of 'a' 249 return ch - 'a' + 10; 250 } 251 252 throw new IllegalArgumentException("The provided hex string contains non-hex character '" + ch + "': " + orig); 253 } 254}