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}