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 bindingURL) throws 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[i] = new 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(key) instanceof 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 }
|