| /* |
| * |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| * |
| */ |
| |
| package org.apache.qpid.server.url; |
| |
| |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import java.net.URISyntaxException; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.apache.qpid.server.url.AMQBindingURL; |
| |
| public class BindingURLParser |
| { |
| private static final char PROPERTY_EQUALS_CHAR = '='; |
| private static final char PROPERTY_SEPARATOR_CHAR = '&'; |
| private static final char ALTERNATIVE_PROPERTY_SEPARATOR_CHAR = ','; |
| private static final char FORWARD_SLASH_CHAR = '/'; |
| private static final char QUESTION_MARK_CHAR = '?'; |
| private static final char SINGLE_QUOTE_CHAR = '\''; |
| private static final char COLON_CHAR = ':'; |
| private static final char END_OF_URL_MARKER_CHAR = '%'; |
| |
| private static final Logger LOGGER = LoggerFactory.getLogger(BindingURLParser.class); |
| |
| private char[] _url; |
| private AMQBindingURL _bindingURL; |
| private BindingURLParserState _currentParserState; |
| private String _error; |
| private int _index = 0; |
| private String _currentPropName; |
| private Map<String,Object> _options; |
| |
| |
| public BindingURLParser() |
| { |
| } |
| |
| //<exch_class>://<exch_name>/[<destination>]/[<queue>]?<option>='<value>'[,<option>='<value>']* |
| public void parse(String url, AMQBindingURL bindingURL) throws URISyntaxException |
| { |
| _url = (url + END_OF_URL_MARKER_CHAR).toCharArray(); |
| _bindingURL = bindingURL; |
| _currentParserState = BindingURLParserState.BINDING_URL_START; |
| BindingURLParserState prevState = _currentParserState; |
| _index = 0; |
| _currentPropName = null; |
| _error = null; |
| _options = new HashMap<>(); |
| |
| try |
| { |
| while (_currentParserState != BindingURLParserState.ERROR && _currentParserState != BindingURLParserState.BINDING_URL_END) |
| { |
| prevState = _currentParserState; |
| _currentParserState = next(); |
| } |
| |
| if (_currentParserState == BindingURLParserState.ERROR) |
| { |
| _error = |
| "Invalid URL format [current_state = " + prevState + ", details extracted so far " + _bindingURL + " ] error at (" + _index + ") due to " + _error; |
| LOGGER.debug(_error); |
| URISyntaxException ex; |
| ex = new URISyntaxException(markErrorLocation(),"Error occurred while parsing URL",_index); |
| throw ex; |
| } |
| |
| processOptions(); |
| } |
| catch (ArrayIndexOutOfBoundsException e) |
| { |
| _error = "Invalid URL format [current_state = " + prevState + ", details parsed so far " + _bindingURL + " ] error at (" + _index + ")"; |
| URISyntaxException ex = new URISyntaxException(markErrorLocation(),"Error occurred while parsing URL",_index); |
| ex.initCause(e); |
| throw ex; |
| } |
| } |
| |
| enum BindingURLParserState |
| { |
| BINDING_URL_START, |
| EXCHANGE_CLASS, |
| COLON_CHAR, |
| HIERARCHY_PREFIX, |
| EXCHANGE_NAME, |
| EXCHANGE_SEPERATOR_CHAR, |
| DESTINATION, |
| DESTINATION_SEPERATOR_CHAR, |
| QUEUE_NAME, |
| QUESTION_MARK_CHAR, |
| PROPERTY_NAME, |
| PROPERTY_EQUALS, |
| START_PROPERTY_VALUE, |
| PROPERTY_VALUE, |
| END_PROPERTY_VALUE, |
| PROPERTY_SEPARATOR, |
| BINDING_URL_END, |
| ERROR |
| } |
| |
| /** |
| * I am fully ware that there are few optimizations |
| * that can speed up things a wee bit. But I have opted |
| * for readability and maintainability at the expense of |
| * speed, as speed is not a critical factor here. |
| * |
| * One can understand the full parse logic by just looking at this method. |
| */ |
| private BindingURLParserState next() |
| { |
| switch (_currentParserState) |
| { |
| case BINDING_URL_START: |
| return extractExchangeClass(); |
| case COLON_CHAR: |
| _index++; //skip ":" |
| return BindingURLParserState.HIERARCHY_PREFIX; |
| case HIERARCHY_PREFIX: |
| return consumeHierarchyPrefix(); |
| case EXCHANGE_NAME: |
| return extractExchangeName(); |
| case EXCHANGE_SEPERATOR_CHAR: |
| _index++; // skip '/' |
| return BindingURLParserState.DESTINATION; |
| case DESTINATION: |
| return extractDestination(); |
| case DESTINATION_SEPERATOR_CHAR: |
| _index++; // skip '/' |
| return BindingURLParserState.QUEUE_NAME; |
| case QUEUE_NAME: |
| return extractQueueName(); |
| case QUESTION_MARK_CHAR: |
| _index++; // skip '?' |
| return BindingURLParserState.PROPERTY_NAME; |
| case PROPERTY_NAME: |
| return extractPropertyName(); |
| case PROPERTY_EQUALS: |
| _index++; // skip the equal sign |
| return BindingURLParserState.START_PROPERTY_VALUE; |
| case START_PROPERTY_VALUE: |
| _index++; // skip the '\'' |
| return BindingURLParserState.PROPERTY_VALUE; |
| case PROPERTY_VALUE: |
| return extractPropertyValue(); |
| case END_PROPERTY_VALUE: |
| _index ++; |
| return checkEndOfURL(); |
| case PROPERTY_SEPARATOR: |
| _index++; // skip '&' |
| return BindingURLParserState.PROPERTY_NAME; |
| default: |
| return BindingURLParserState.ERROR; |
| } |
| } |
| |
| private BindingURLParserState extractExchangeClass() |
| { |
| char nextChar = _url[_index]; |
| |
| StringBuilder builder = new StringBuilder(); |
| while (nextChar != COLON_CHAR && nextChar != END_OF_URL_MARKER_CHAR) |
| { |
| builder.append(nextChar); |
| _index++; |
| nextChar = _url[_index]; |
| } |
| |
| // normal use case |
| if (nextChar == COLON_CHAR) |
| { |
| if (builder.length() == 0) |
| { |
| _error = "Exchange class is absent"; |
| return BindingURLParserState.ERROR; |
| } |
| _bindingURL.setExchangeClass(builder.toString()); |
| return BindingURLParserState.COLON_CHAR; |
| } |
| else |
| { |
| return BindingURLParserState.ERROR; |
| } |
| } |
| |
| private BindingURLParserState consumeHierarchyPrefix() |
| { |
| char nextChar; |
| int loop = 0; |
| do |
| { |
| nextChar = _url[_index++]; |
| loop++; |
| } |
| while (nextChar == FORWARD_SLASH_CHAR && loop < 2 ); |
| |
| if (nextChar == FORWARD_SLASH_CHAR) |
| { |
| return BindingURLParserState.EXCHANGE_NAME; |
| } |
| else |
| { |
| _error = "Unexpected character '" + nextChar + "' encountered when expecting hierarchy prefix '/'"; |
| return BindingURLParserState.ERROR; |
| } |
| } |
| |
| private BindingURLParserState extractExchangeName() |
| { |
| char nextChar = _url[_index]; |
| StringBuilder builder = new StringBuilder(); |
| while (nextChar != FORWARD_SLASH_CHAR) |
| { |
| builder.append(nextChar); |
| _index++; |
| nextChar = _url[_index]; |
| } |
| |
| _bindingURL.setExchangeName(builder.toString()); |
| return BindingURLParserState.EXCHANGE_SEPERATOR_CHAR; |
| } |
| |
| private BindingURLParserState extractDestination() |
| { |
| char nextChar = _url[_index]; |
| |
| //The destination is and queue name are both optional |
| // This is checking for the case where both are not specified. |
| if (nextChar == QUESTION_MARK_CHAR) |
| { |
| return BindingURLParserState.QUESTION_MARK_CHAR; |
| } |
| |
| StringBuilder builder = new StringBuilder(); |
| while (nextChar != FORWARD_SLASH_CHAR && nextChar != QUESTION_MARK_CHAR) |
| { |
| builder.append(nextChar); |
| _index++; |
| nextChar = _url[_index]; |
| } |
| |
| // This is the case where the destination is explicitly stated. |
| // ex direct://amq.direct/myDest/myQueue?option1='1' ... OR |
| // direct://amq.direct//myQueue?option1='1' ... |
| if (nextChar == FORWARD_SLASH_CHAR) |
| { |
| _bindingURL.setDestinationName(builder.toString()); |
| return BindingURLParserState.DESTINATION_SEPERATOR_CHAR; |
| } |
| // This is the case where destination is not explicitly stated. |
| // ex direct://amq.direct/myQueue?option1='1' ... |
| else |
| { |
| _bindingURL.setQueueName(builder.toString()); |
| return BindingURLParserState.QUESTION_MARK_CHAR; |
| } |
| } |
| |
| private BindingURLParserState extractQueueName() |
| { |
| char nextChar = _url[_index]; |
| StringBuilder builder = new StringBuilder(); |
| while (nextChar != QUESTION_MARK_CHAR && nextChar != END_OF_URL_MARKER_CHAR) |
| { |
| builder.append(nextChar); |
| nextChar = _url[++_index]; |
| } |
| _bindingURL.setQueueName(builder.toString()); |
| |
| if(nextChar == QUESTION_MARK_CHAR) |
| { |
| return BindingURLParserState.QUESTION_MARK_CHAR; |
| } |
| else |
| { |
| return BindingURLParserState.BINDING_URL_END; |
| } |
| } |
| |
| private BindingURLParserState extractPropertyName() |
| { |
| StringBuilder builder = new StringBuilder(); |
| char next = _url[_index]; |
| while (next != PROPERTY_EQUALS_CHAR) |
| { |
| builder.append(next); |
| next = _url[++_index]; |
| } |
| _currentPropName = builder.toString(); |
| |
| if (_currentPropName.trim().equals("")) |
| { |
| _error = "Property name cannot be empty"; |
| return BindingURLParserState.ERROR; |
| } |
| |
| return BindingURLParserState.PROPERTY_EQUALS; |
| } |
| |
| private BindingURLParserState extractPropertyValue() |
| { |
| StringBuilder builder = new StringBuilder(); |
| char next = _url[_index]; |
| while (next != SINGLE_QUOTE_CHAR) |
| { |
| builder.append(next); |
| next = _url[++_index]; |
| } |
| String propValue = builder.toString(); |
| |
| if (propValue.trim().equals("")) |
| { |
| _error = "Property values cannot be empty"; |
| return BindingURLParserState.ERROR; |
| } |
| else |
| { |
| if (_options.containsKey(_currentPropName)) |
| { |
| Object obj = _options.get(_currentPropName); |
| if (obj instanceof List) |
| { |
| List list = (List)obj; |
| list.add(propValue); |
| } |
| else // it has to be a string |
| { |
| List<String> list = new ArrayList(); |
| list.add((String)obj); |
| list.add(propValue); |
| _options.put(_currentPropName, list); |
| } |
| } |
| else |
| { |
| _options.put(_currentPropName, propValue); |
| } |
| |
| |
| return BindingURLParserState.END_PROPERTY_VALUE; |
| } |
| } |
| |
| private BindingURLParserState checkEndOfURL() |
| { |
| char nextChar = _url[_index]; |
| if ( nextChar == END_OF_URL_MARKER_CHAR) |
| { |
| return BindingURLParserState.BINDING_URL_END; |
| } |
| else if (nextChar == PROPERTY_SEPARATOR_CHAR || nextChar == ALTERNATIVE_PROPERTY_SEPARATOR_CHAR) |
| { |
| return BindingURLParserState.PROPERTY_SEPARATOR; |
| } |
| else |
| { |
| return BindingURLParserState.ERROR; |
| } |
| } |
| |
| private String markErrorLocation() |
| { |
| String tmp = String.valueOf(_url); |
| // length -1 to remove ENDOF URL marker |
| return tmp.substring(0,_index) + "^" + tmp.substring(_index+1> tmp.length()-1?tmp.length()-1:_index+1,tmp.length()-1); |
| } |
| |
| private void processOptions() throws URISyntaxException |
| { |
| // check for bindingKey |
| if (_options.containsKey(BindingURL.OPTION_BINDING_KEY) && _options.get(BindingURL.OPTION_BINDING_KEY) != null) |
| { |
| Object obj = _options.get(BindingURL.OPTION_BINDING_KEY); |
| |
| if (obj instanceof String) |
| { |
| String[] bindingKeys = new String[]{(String)obj}; |
| _bindingURL.setBindingKeys(bindingKeys); |
| } |
| else // it would be a list |
| { |
| List list = (List)obj; |
| String[] bindingKeys = new String[list.size()]; |
| int i=0; |
| for (Iterator it = list.iterator(); it.hasNext();) |
| { |
| bindingKeys[i] = (String)it.next(); |
| i++; |
| } |
| _bindingURL.setBindingKeys(bindingKeys); |
| } |
| |
| } |
| for (String key: _options.keySet()) |
| { |
| // We want to skip the bindingKey list |
| if (_options.get(key) instanceof String) |
| { |
| _bindingURL.setOption(key, (String)_options.get(key)); |
| } |
| } |
| |
| |
| // check if both a binding key and a routing key is specified. |
| if (_options.containsKey(BindingURL.OPTION_BINDING_KEY) && _options.containsKey(BindingURL.OPTION_ROUTING_KEY)) |
| { |
| throw new URISyntaxException(String.valueOf(_url),"It is illegal to specify both a routingKey and a bindingKey in the same URL",-1); |
| } |
| } |
| } |