BindingURLParser.java
001 package org.apache.qpid.url;
002 /*
003  
004  * Licensed to the Apache Software Foundation (ASF) under one
005  * or more contributor license agreements.  See the NOTICE file
006  * distributed with this work for additional information
007  * regarding copyright ownership.  The ASF licenses this file
008  * to you under the Apache License, Version 2.0 (the
009  * "License"); you may not use this file except in compliance
010  * with the License.  You may obtain a copy of the License at
011  
012  *   http://www.apache.org/licenses/LICENSE-2.0
013  
014  * Unless required by applicable law or agreed to in writing,
015  * software distributed under the License is distributed on an
016  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017  * KIND, either express or implied.  See the License for the
018  * specific language governing permissions and limitations
019  * under the License.
020  
021  */
022 
023 
024 import java.net.URISyntaxException;
025 import java.util.ArrayList;
026 import java.util.HashMap;
027 import java.util.Iterator;
028 import java.util.List;
029 import java.util.Map;
030 
031 import org.apache.qpid.exchange.ExchangeDefaults;
032 import org.apache.qpid.framing.AMQShortString;
033 import org.slf4j.Logger;
034 import org.slf4j.LoggerFactory;
035 
036 public class BindingURLParser
037 {
038     private static final char PROPERTY_EQUALS_CHAR = '=';
039     private static final char PROPERTY_SEPARATOR_CHAR = '&';
040     private static final char ALTERNATIVE_PROPERTY_SEPARATOR_CHAR = ',';
041     private static final char FORWARD_SLASH_CHAR = '/';
042     private static final char QUESTION_MARK_CHAR = '?';
043     private static final char SINGLE_QUOTE_CHAR = '\'';
044     private static final char COLON_CHAR = ':';
045     private static final char END_OF_URL_MARKER_CHAR = '%';
046 
047     private static final Logger _logger = LoggerFactory.getLogger(BindingURLParser.class);
048 
049     private char[] _url;
050     private AMQBindingURL _bindingURL;
051     private BindingURLParserState _currentParserState;
052     private String _error;
053     private int _index = 0;
054     private String _currentPropName;
055     private Map<String,Object> _options = new HashMap<String,Object>();
056 
057     //<exch_class>://<exch_name>/[<destination>]/[<queue>]?<option>='<value>'[,<option>='<value>']*
058     public BindingURLParser(String url,AMQBindingURL bindingURLthrows URISyntaxException
059     {
060         _url = (url + END_OF_URL_MARKER_CHAR).toCharArray();
061         _bindingURL = bindingURL;
062         _currentParserState = BindingURLParserState.BINDING_URL_START;
063         BindingURLParserState prevState = _currentParserState;
064 
065         try
066         {
067             while (_currentParserState != BindingURLParserState.ERROR && _currentParserState != BindingURLParserState.BINDING_URL_END)
068             {
069                 prevState = _currentParserState;
070                 _currentParserState = next();
071             }
072 
073             if (_currentParserState == BindingURLParserState.ERROR)
074             {
075                 _error =
076                         "Invalid URL format [current_state = " + prevState + ", details parsed so far " + _bindingURL + " ] error at (" + _index + ") due to " + _error;
077                 _logger.debug(_error);
078                 URISyntaxException ex;
079                 ex = new URISyntaxException(markErrorLocation(),"Error occured while parsing URL",_index);
080                 throw ex;
081             }
082 
083             processOptions();
084         }
085         catch (ArrayIndexOutOfBoundsException e)
086         {
087                 _error = "Invalid URL format [current_state = " + prevState + ", details parsed so far " + _bindingURL + " ] error at (" + _index + ")";
088                 URISyntaxException ex = new URISyntaxException(markErrorLocation(),"Error occured while parsing URL",_index);
089                 ex.initCause(e);
090                 throw ex;
091         }
092     }
093 
094     enum BindingURLParserState
095     {
096         BINDING_URL_START,
097         EXCHANGE_CLASS,
098         COLON_CHAR,
099         DOUBLE_SEP,
100         EXCHANGE_NAME,
101         EXCHANGE_SEPERATOR_CHAR,
102         DESTINATION,
103         DESTINATION_SEPERATOR_CHAR,
104         QUEUE_NAME,
105         QUESTION_MARK_CHAR,
106         PROPERTY_NAME,
107         PROPERTY_EQUALS,
108         START_PROPERTY_VALUE,
109         PROPERTY_VALUE,
110         END_PROPERTY_VALUE,
111         PROPERTY_SEPARATOR,
112         BINDING_URL_END,
113         ERROR
114     }
115 
116     /**
117      * I am fully ware that there are few optimizations
118      * that can speed up things a wee bit. But I have opted
119      * for readability and maintainability at the expense of
120      * speed, as speed is not a critical factor here.
121      *
122      * One can understand the full parse logic by just looking at this method.
123      */
124     private BindingURLParserState next()
125     {
126         switch (_currentParserState)
127         {
128             case BINDING_URL_START:
129                 return extractExchangeClass();
130             case COLON_CHAR:
131                 _index++; //skip ":"
132                 return BindingURLParserState.DOUBLE_SEP;
133             case DOUBLE_SEP:
134                 _index = _index + 2//skip "//"
135                 return BindingURLParserState.EXCHANGE_NAME;
136             case EXCHANGE_NAME:
137                 return extractExchangeName();
138             case EXCHANGE_SEPERATOR_CHAR:
139                 _index++; // skip '/'
140                 return BindingURLParserState.DESTINATION;
141             case DESTINATION:
142                 return extractDestination();
143             case DESTINATION_SEPERATOR_CHAR:
144                 _index++; // skip '/'
145                 return BindingURLParserState.QUEUE_NAME;
146             case QUEUE_NAME:
147                 return extractQueueName();
148             case QUESTION_MARK_CHAR:
149                 _index++; // skip '?'
150                 return BindingURLParserState.PROPERTY_NAME;
151             case PROPERTY_NAME:
152                 return extractPropertyName();
153             case PROPERTY_EQUALS:
154                 _index++; // skip the equal sign
155                 return BindingURLParserState.START_PROPERTY_VALUE;
156             case START_PROPERTY_VALUE:
157                 _index++; // skip the '\''
158                 return BindingURLParserState.PROPERTY_VALUE;
159             case PROPERTY_VALUE:
160                 return extractPropertyValue();
161             case END_PROPERTY_VALUE:
162                 _index ++;
163                 return checkEndOfURL();
164             case PROPERTY_SEPARATOR:
165                 _index++; // skip '&'
166                 return BindingURLParserState.PROPERTY_NAME;
167             default:
168                 return BindingURLParserState.ERROR;
169         }
170     }
171 
172     private BindingURLParserState extractExchangeClass()
173     {
174         char nextChar = _url[_index];
175 
176         // check for the following special cases.
177         // "myQueue?durable='true'" or just "myQueue";
178 
179         StringBuilder builder = new StringBuilder();
180         while (nextChar != COLON_CHAR && nextChar != QUESTION_MARK_CHAR && nextChar != END_OF_URL_MARKER_CHAR)
181         {
182             builder.append(nextChar);
183             _index++;
184             nextChar = _url[_index];
185         }
186 
187         // normal use case
188         if (nextChar == COLON_CHAR)
189         {
190             _bindingURL.setExchangeClass(builder.toString());
191             return BindingURLParserState.COLON_CHAR;
192         }
193         // "myQueue?durable='true'" use case
194         else if (nextChar == QUESTION_MARK_CHAR)
195         {
196             _bindingURL.setExchangeClass(ExchangeDefaults.DIRECT_EXCHANGE_CLASS.asString());
197             _bindingURL.setExchangeName("");
198             _bindingURL.setQueueName(builder.toString());
199             return BindingURLParserState.QUESTION_MARK_CHAR;
200         }
201         else
202         {
203             _bindingURL.setExchangeClass(ExchangeDefaults.DIRECT_EXCHANGE_CLASS.asString());
204             _bindingURL.setExchangeName("");
205             _bindingURL.setQueueName(builder.toString());
206             return BindingURLParserState.BINDING_URL_END;
207         }
208     }
209 
210     private BindingURLParserState extractExchangeName()
211     {
212         char nextChar = _url[_index];
213         StringBuilder builder = new StringBuilder();
214         while (nextChar != FORWARD_SLASH_CHAR)
215         {
216             builder.append(nextChar);
217             _index++;
218             nextChar = _url[_index];
219         }
220 
221         _bindingURL.setExchangeName(builder.toString());
222         return BindingURLParserState.EXCHANGE_SEPERATOR_CHAR;
223     }
224 
225     private BindingURLParserState extractDestination()
226     {
227         char nextChar = _url[_index];
228 
229         //The destination is and queue name are both optional
230         // This is checking for the case where both are not specified.
231         if (nextChar == QUESTION_MARK_CHAR)
232         {
233             return BindingURLParserState.QUESTION_MARK_CHAR;
234         }
235 
236         StringBuilder builder = new StringBuilder();
237         while (nextChar != FORWARD_SLASH_CHAR && nextChar != QUESTION_MARK_CHAR)
238         {
239             builder.append(nextChar);
240             _index++;
241             nextChar = _url[_index];
242         }
243 
244         // This is the case where the destination is explictily stated.
245         // ex direct://amq.direct/myDest/myQueue?option1='1' ... OR
246         // direct://amq.direct//myQueue?option1='1' ...
247         if (nextChar == FORWARD_SLASH_CHAR)
248         {
249             _bindingURL.setDestinationName(builder.toString());
250             return BindingURLParserState.DESTINATION_SEPERATOR_CHAR;
251         }
252         // This is the case where destination is not explictly stated.
253         // ex direct://amq.direct/myQueue?option1='1' ...
254         else
255         {
256             _bindingURL.setQueueName(builder.toString());
257             return BindingURLParserState.QUESTION_MARK_CHAR;
258         }
259     }
260 
261     private BindingURLParserState extractQueueName()
262     {
263         char nextChar = _url[_index];
264         StringBuilder builder = new StringBuilder();
265         while (nextChar != QUESTION_MARK_CHAR && nextChar != END_OF_URL_MARKER_CHAR)
266         {
267             builder.append(nextChar);
268             nextChar = _url[++_index];
269         }
270         _bindingURL.setQueueName(builder.toString());
271 
272         if(nextChar == QUESTION_MARK_CHAR)
273         {
274             return BindingURLParserState.QUESTION_MARK_CHAR;
275         }
276         else
277         {
278             return BindingURLParserState.BINDING_URL_END;
279         }
280     }
281 
282     private BindingURLParserState extractPropertyName()
283     {
284         StringBuilder builder = new StringBuilder();
285         char next = _url[_index];
286         while (next != PROPERTY_EQUALS_CHAR)
287         {
288             builder.append(next);
289             next = _url[++_index];
290         }
291         _currentPropName = builder.toString();
292 
293         if (_currentPropName.trim().equals(""))
294         {
295             _error = "Property name cannot be empty";
296             return BindingURLParserState.ERROR;
297         }
298 
299         return BindingURLParserState.PROPERTY_EQUALS;
300     }
301 
302     private BindingURLParserState extractPropertyValue()
303     {
304         StringBuilder builder = new StringBuilder();
305         char next = _url[_index];
306         while (next != SINGLE_QUOTE_CHAR)
307         {
308             builder.append(next);
309             next = _url[++_index];
310         }
311         String propValue = builder.toString();
312 
313         if (propValue.trim().equals(""))
314         {
315             _error = "Property values cannot be empty";
316             return BindingURLParserState.ERROR;
317         }
318         else
319         {
320             if (_options.containsKey(_currentPropName))
321             {
322                 Object obj = _options.get(_currentPropName);
323                 if (obj instanceof List)
324                 {
325                     List list = (List)obj;
326                     list.add(propValue);
327                 }
328                 else // it has to be a string
329                 {
330                     List<String> list = new ArrayList();
331                     list.add((String)obj);
332                     list.add(propValue);
333                     _options.put(_currentPropName, list);
334                 }
335             }
336             else
337             {
338                 _options.put(_currentPropName, propValue);
339             }
340 
341 
342             return BindingURLParserState.END_PROPERTY_VALUE;
343         }
344     }
345 
346     private BindingURLParserState checkEndOfURL()
347     {
348         char nextChar = _url[_index];
349         if nextChar ==  END_OF_URL_MARKER_CHAR)
350         {
351             return BindingURLParserState.BINDING_URL_END;
352         }
353         else if (nextChar == PROPERTY_SEPARATOR_CHAR || nextChar == ALTERNATIVE_PROPERTY_SEPARATOR_CHAR)
354         {
355             return BindingURLParserState.PROPERTY_SEPARATOR;
356         }
357         else
358         {
359             return BindingURLParserState.ERROR;
360         }
361     }
362 
363     private String markErrorLocation()
364     {
365         String tmp = String.valueOf(_url);
366         // length -1 to remove ENDOF URL marker
367         return tmp.substring(0,_index"^" + tmp.substring(_index+1> tmp.length()-1?tmp.length()-1:_index+1,tmp.length()-1);
368     }
369 
370     private void processOptions() throws URISyntaxException
371     {
372 //      check for bindingKey
373         if (_options.containsKey(BindingURL.OPTION_BINDING_KEY&& _options.get(BindingURL.OPTION_BINDING_KEY!= null)
374         {
375             Object obj = _options.get(BindingURL.OPTION_BINDING_KEY);
376 
377             if (obj instanceof String)
378             {
379                 AMQShortString[] bindingKeys = new AMQShortString[]{new AMQShortString((String)obj)};
380                 _bindingURL.setBindingKeys(bindingKeys);
381             }
382             else // it would be a list
383             {
384                 List list = (List)obj;
385                 AMQShortString[] bindingKeys = new AMQShortString[list.size()];
386                 int i=0;
387                 for (Iterator it = list.iterator(); it.hasNext();)
388                 {
389                     bindingKeys[inew AMQShortString((String)it.next());
390                     i++;
391                 }
392                 _bindingURL.setBindingKeys(bindingKeys);
393             }
394 
395         }
396         for (String key: _options.keySet())
397         {
398             // We want to skip the bindingKey list
399             if (_options.get(keyinstanceof String)
400             {
401                 _bindingURL.setOption(key, (String)_options.get(key));
402             }
403         }
404 
405 
406         // check if both a binding key and a routing key is specified.
407         if (_options.containsKey(BindingURL.OPTION_BINDING_KEY&& _options.containsKey(BindingURL.OPTION_ROUTING_KEY))
408         {
409             throw new URISyntaxException(String.valueOf(_url),"It is illegal to specify both a routingKey and a bindingKey in the same URL",-1);
410         }
411 
412         // check for durable subscriptions
413         if (_bindingURL.getExchangeClass().equals(ExchangeDefaults.TOPIC_EXCHANGE_CLASS))
414         {
415             String queueName = null;
416             if (Boolean.parseBoolean(_bindingURL.getOption(BindingURL.OPTION_DURABLE)))
417             {
418                 if (_bindingURL.containsOption(BindingURL.OPTION_CLIENTID&& _bindingURL.containsOption(BindingURL.OPTION_SUBSCRIPTION))
419                 {
420                     queueName = _bindingURL.getOption(BindingURL.OPTION_CLIENTID + ":" + BindingURL.OPTION_SUBSCRIPTION);
421                 }
422                 else
423                 {
424                     throw new URISyntaxException(String.valueOf(_url),"Durable subscription must have values for " + BindingURL.OPTION_CLIENTID
425                         " and " + BindingURL.OPTION_SUBSCRIPTION , -1);
426 
427                 }
428             }
429             _bindingURL.setQueueName(queueName);
430         }
431     }
432 
433     public static void main(String[] args)
434     {
435         String[] urls = new String[]
436            {
437              "topic://amq.topic//myTopic?routingkey='stocks.#'",
438              "topic://amq.topic/message_queue?bindingkey='usa.*'&bindingkey='control',exclusive='true'",
439              "topic://amq.topic//?bindingKey='usa.*',bindingkey='control',exclusive='true'",
440              "direct://amq.direct/dummyDest/myQueue?routingkey='abc.*'",
441              "exchange.Class://exchangeName/Destination/Queue",
442              "exchangeClass://exchangeName/Destination/?option='value',option2='value2'",
443              "IBMPerfQueue1?durable='true'",
444              "exchangeClass://exchangeName/Destination/?bindingkey='key1',bindingkey='key2'",
445              "exchangeClass://exchangeName/Destination/?bindingkey='key1'&routingkey='key2'"
446            };
447 
448         try
449         {
450             for (String url: urls)
451             {
452                 System.out.println("URL " + url);
453                 AMQBindingURL bindingURL = new AMQBindingURL(url);
454                 BindingURLParser parser = new BindingURLParser(url,bindingURL);
455                 System.out.println("\nX " + bindingURL.toString() " \n");
456 
457             }
458 
459         }
460         catch(Exception e)
461         {
462             e.printStackTrace();
463         }
464     }
465 
466 }