package org.marketcetera.marketdata; import static org.marketcetera.marketdata.Messages.INVALID_ASSET_CLASS; import static org.marketcetera.marketdata.Messages.INVALID_CONTENT; import java.util.*; import javax.annotation.concurrent.NotThreadSafe; import org.marketcetera.core.Util; import org.marketcetera.util.log.I18NBoundMessage1P; import org.marketcetera.util.misc.ClassVersion; /* $License$ */ /** * Builds {@link MarketDataRequest} objects. * * @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a> * @version $Id: MarketDataRequestBuilder.java 16154 2012-07-14 16:34:05Z colin $ * @since 2.1.0 */ @NotThreadSafe @ClassVersion("$Id: MarketDataRequestBuilder.java 16154 2012-07-14 16:34:05Z colin $") public class MarketDataRequestBuilder { /** * the key used to identify the asset class in the string representation of the market data request */ public static final String ASSETCLASS_KEY = "assetclass"; //$NON-NLS-1$ /** * the key used to identify the exchange in the string representation of the market data request */ public static final String EXCHANGE_KEY = "exchange"; //$NON-NLS-1$ /** * the key used to identify the content in the string representation of the market data request */ public static final String CONTENT_KEY = "content"; //$NON-NLS-1$ /** * the key used to identify the provider in the string representation of the market data request */ public static final String PROVIDER_KEY = "provider"; //$NON-NLS-1$ /** * the key used to identify the underlying symbols in the string representation of the market data request */ public static final String UNDERLYINGSYMBOLS_KEY = "underlyingsymbols"; //$NON-NLS-1$ /** * the key used to identify the symbols in the string representation of the market data request */ public static final String SYMBOLS_KEY = "symbols"; //$NON-NLS-1$ /** * the key used to identify the parameters in the string representation of the market data request */ public static final String PARAMETERS_KEY = "parameters"; //$NON-NLS-1$ /** * the delimiter used to distinguish between symbols in the string representation of the symbol collection */ public static final String SYMBOL_DELIMITER = ","; //$NON-NLS-1$ /** * Creates a <code>MarketDataRequest</code>. * * <p>The <code>String</code> parameter should be a set of key/value pairs delimited * by {@link Util#KEY_VALUE_DELIMITER}. The set of keys that this method understands * is as follows: * <ul> * <li>{@link MarketDataRequestBuilder#SYMBOLS_KEY} - the symbols for which to request market data</li> * <li>{@link MarketDataRequestBuilder#UNDERLYINGSYMBOLS_KEY} - the underlying symbols for which to request market data</li> * <li>{@link MarketDataRequestBuilder#PROVIDER_KEY} - the provider from which to request market data</li> * <li>{@link MarketDataRequestBuilder#CONTENT_KEY} - the content of the market data</li> * <li>{@link MarketDataRequestBuilder#EXCHANGE_KEY} - the exchange for which to request market data</li> * <li>{@link MarketDataRequestBuilder#ASSETCLASS_KEY} - the asset class for which to request market data</li> * <li>{@link MarketDataRequestBuilder#PARAMETERS_KEY} - the parameters to add to the market data request</li> * </ul> * * <p>Example: * <pre> * "symbols=GOOG,ORCL,MSFT:provider=marketcetera:content=TOP_OF_BOOK" * </pre> * * <p>The key/value pairs are validated according to the rules established for each * component. Extraneous key/value pairs, i.e., key/value pairs with a key that * does not match one of the above list are ignored. Additional validation is performed * according to the rules defined at {@link Util#propertiesFromString(String)}. * * <p>Validation is performed on each key/value pair as it is processed with respect to that * key/value pair only. After all key/value pairs are processed and validated, a * second, comprehensive validation checks that all key/value pairs are valid with respect * to each other. * * @param inRequest a <code>String</code> value * @return a <code>MarketDataRequest</code> value * @throws IllegalArgumentException if the request cannot be constructed */ public static MarketDataRequest newRequestFromString(String inRequest) { if(inRequest.isEmpty()) { throw new IllegalArgumentException(); } Properties props = Util.propertiesFromString(inRequest); Map<String,String> sanitizedProps = new HashMap<String,String>(); for(Object key : props.keySet()) { sanitizedProps.put(((String)key).toLowerCase().trim(), ((String)props.get(key)).trim()); } MarketDataRequestBuilder builder = newRequest(); if(sanitizedProps.containsKey(SYMBOLS_KEY)) { builder.withSymbols(sanitizedProps.get(SYMBOLS_KEY).split(SYMBOL_DELIMITER)); } if(sanitizedProps.containsKey(UNDERLYINGSYMBOLS_KEY)) { builder.withUnderlyingSymbols(sanitizedProps.get(UNDERLYINGSYMBOLS_KEY).split(SYMBOL_DELIMITER)); } if(sanitizedProps.containsKey(PROVIDER_KEY)) { builder.withProvider(sanitizedProps.get(PROVIDER_KEY)); } if(sanitizedProps.containsKey(CONTENT_KEY)) { builder.withContent(sanitizedProps.get(CONTENT_KEY).split(SYMBOL_DELIMITER)); } if(sanitizedProps.containsKey(EXCHANGE_KEY)) { builder.withExchange(sanitizedProps.get(EXCHANGE_KEY)); } if(sanitizedProps.containsKey(ASSETCLASS_KEY)) { builder.withAssetClass(sanitizedProps.get(ASSETCLASS_KEY)); } if(sanitizedProps.containsKey(PARAMETERS_KEY)) { Properties params = Util.propertiesFromString(sanitizedProps.get(PARAMETERS_KEY)); for(Map.Entry<Object,Object> entry : params.entrySet()) { String key = ((String)entry.getKey()).trim(); String value = ((String)entry.getValue()).trim(); builder.withParameter(key, value); } } return builder.create(); } /** * Creates a new <code>MarketDataRequestBuilder</code>. * * @return a <code>MarketDataRequestBuilder</code> value */ public static MarketDataRequestBuilder newRequest() { return new MarketDataRequestBuilder(); } /** * Creates a {@link MarketDataRequest} with the builder's current values. * * @return a <code>MarketDataRequest</code> value * @throws IllegalArgumentException if the object cannot be created */ public MarketDataRequest create() { return new MarketDataRequest(marketdata); } /** * Adds the given symbols to the market data request. * * <p>Either symbols or underlying symbols ({@link #withUnderlyingSymbols(String)} or * {@link #withUnderlyingSymbols(String[])}) must be specified and no default is provided. * * <p>The given symbols replace any existing symbols. If the given symbols are <code>null</code> * or empty, any existing symbols are removed. * * @param inSymbols a <code>String[]</code> value containing symbols to add to the request or <code>null</code> * @return a <code>MarketDataRequest</code> value */ public MarketDataRequestBuilder withSymbols(String... inSymbols) { if(inSymbols == null || inSymbols.length == 0) { marketdata.setSymbols(EMPTY_SYMBOLS); return this; } return withSymbols(Arrays.asList(inSymbols)); } /** * Adds the given symbols to the market data request. * * <p>Either symbols or underlying symbols ({@link #withUnderlyingSymbols(String)} or * {@link #withUnderlyingSymbols(String[])}) must be specified and no default is provided. * * <p>The given symbols replace any existing symbols. If the given symbols are <code>null</code> * or empty, any existing symbols are removed. * * @param inSymbols a <code>Collection<String></code> value containing symbols to add to the request or <code>null</code> * @return a <code>MarketDataRequest</code> value */ public MarketDataRequestBuilder withSymbols(Collection<String> inSymbols) { if(inSymbols == null || inSymbols.isEmpty()) { marketdata.setSymbols(EMPTY_SYMBOLS); } else { marketdata.setSymbols(inSymbols); } return this; } /** * Adds the given symbols to the market data request. * * <p>The symbols may be a single symbol or a series of symbols delimited by * {@link MarketDataRequestBuilder#SYMBOL_DELIMITER}. * * <p>Either symbols or underlying symbols ({@link #withUnderlyingSymbols(String)} or * {@link #withUnderlyingSymbols(String[])}) must be specified and no default is provided. * * <p>The given symbols replace any existing symbols. If the given symbols are <code>null</code> * or empty, any existing symbols are removed. * * @param inSymbols a <code>String</code> value containing symbols separated by {@link MarketDataRequestBuilder#SYMBOL_DELIMITER} * to add to the request or <code>null</code> * @return a <code>MarketDataRequest</code> value */ public MarketDataRequestBuilder withSymbols(String inSymbols) { if(inSymbols == null || inSymbols.length() == 0) { marketdata.setSymbols(EMPTY_SYMBOLS); return this; } return withSymbols(inSymbols.split(SYMBOL_DELIMITER)); } /** * Adds the given underlying symbols to the market data request. * * <p>Either symbols ({@link #withSymbols(String)} or {@link #withSymbols(String[])}) or * underlying symbols must be specified and no default is provided. * * <p>The given underlying symbols replace any existing underlying symbols. If the given underlying symbols are <code>null</code> * or empty, any existing underlying symbols are removed. * * @param inUnderlyingSymbols a <code>String[]</code> value containing underlying symbols to add to the request or <code>null</code> * @return a <code>MarketDataRequest</code> value */ public MarketDataRequestBuilder withUnderlyingSymbols(String... inUnderlyingSymbols) { if(inUnderlyingSymbols == null || inUnderlyingSymbols.length == 0) { marketdata.setUnderlyingSymbols(EMPTY_SYMBOLS); } else { Collection<String> symbols = new ArrayList<String>(); for(String symbol : inUnderlyingSymbols) { symbols.add(symbol); } marketdata.setUnderlyingSymbols(symbols); } return this; } /** * Adds the given underlying symbols to the market data request. * * <p>Either symbols or underlying symbols ({@link #withUnderlyingSymbols(String)} or * {@link #withUnderlyingSymbols(String[])}) must be specified and no default is provided. * * <p>The given underlying symbols replace any existing underlying symbols. If the given underlying symbols are <code>null</code> * or empty, any existing underlying symbols are removed. * * @param inUnderlyingSymbols a <code>Collection<String></code> value containing underlying symbols to add to the * request or <code>null</code> * @return a <code>MarketDataRequest</code> value */ public MarketDataRequestBuilder withUnderlyingSymbols(Collection<String> inUnderlyingSymbols) { if(inUnderlyingSymbols == null || inUnderlyingSymbols.isEmpty()) { marketdata.setUnderlyingSymbols(EMPTY_SYMBOLS); } else { marketdata.setUnderlyingSymbols(inUnderlyingSymbols); } return this; } /** * Adds the given underlying symbols to the market data request. * * <p>The underlying symbols may be a single symbol * or a series of symbols delimited by {@link MarketDataRequestBuilder#SYMBOL_DELIMITER}. * * <p>Either symbols ({@link #withSymbols(String)} or {@link #withSymbols(String[])}) or * underlying symbols must be specified and no default is provided. * * <p>The given underlying symbols replace any existing underlying symbols. If the given underlying symbols are <code>null</code> * or empty, any existing underlying symbols are removed. * * @param inUnderlyingSymbols a <code>String</code> value containing underlying symbols separated by * {@link MarketDataRequestBuilder#SYMBOL_DELIMITER} to add to the request or <code>null</code> * @return a <code>MarketDataRequest</code> value * @throws IllegalArgumentException if the specified symbols result in an invalid request */ public MarketDataRequestBuilder withUnderlyingSymbols(String inUnderlyingSymbols) { if(inUnderlyingSymbols == null || inUnderlyingSymbols.length() == 0) { marketdata.setUnderlyingSymbols(EMPTY_SYMBOLS); } else { withUnderlyingSymbols(inUnderlyingSymbols.split(SYMBOL_DELIMITER)); } return this; } /** * Adds the given provider to the market data request. * * <p>The provider is not validated because the set of valid providers is * resolved at run-time. * * <p>This attribute is required and no default is provided except under circumstances * where the provider can be inferred by context. * * @param inProvider a <code>String</code> value containing the provider from which to request data or <code>null</code> * @return a <code>MarketDataRequest</code> value * @throws IllegalArgumentException if the specified provider results in an invalid request */ public MarketDataRequestBuilder withProvider(String inProvider) { marketdata.setProvider(inProvider); return this; } /** * Adds the given exchange to the market data request. * * <p>The exchange is not validated as the set of valid exchanges is dependent on the * provider and the provisioning within the domain of the services provided therein. * * <p>This attribute is optional and no default is provided. * * @param inExchange a <code>String</code> value * @return a <code>MarketDataRequest</code> value */ public MarketDataRequestBuilder withExchange(String inExchange) { marketdata.setExchange(inExchange); return this; } /** * Adds the given content to the market data request. * * <p>The given value must correspond to one or more valid {@link Content} values separated by * {@link MarketDataRequestBuilder#SYMBOL_DELIMITER}. Case is not considered. If <code>null</code> * or empty, the current content is removed. * * <p>This attribute is required. If unspecified, the default value is {@link Content#TOP_OF_BOOK}. * * @param inContent a <code>String</code> value * @return a <code>MarketDataRequest</code> value * @throws IllegalArgumentException if the specified content results in an invalid request */ public MarketDataRequestBuilder withContent(String inContent) { if(inContent == null || inContent.length() == 0) { marketdata.setContent(EMPTY_CONTENT); } else { withContent(inContent.split(SYMBOL_DELIMITER)); } return this; } /** * Adds the given content to the market data request. * * <p>The given content value must not be null. This attribute is required and no * default is provided. * * @param inContent a <code>Content[]</code> value * @return a <code>MarketDataRequest</code> value * @throws IllegalArgumentException if the specified content results in an invalid request */ public MarketDataRequestBuilder withContent(Content...inContent) { if(inContent == null || inContent.length == 0) { marketdata.setContent(EMPTY_CONTENT); } else { Collection<Content> contents = new ArrayList<Content>(); for(Content content : inContent) { contents.add(content); } marketdata.setContent(contents); } return this; } /** * Adds the given content to the market data request. * * <p>Either symbols or underlying symbols ({@link #withUnderlyingSymbols(String)} or * {@link #withUnderlyingSymbols(String[])}) must be specified and no default is provided. * * <p>The given content replaces the existing content. If the given content collection is <code>null</code> * or empty, any existing content is removed. * * @param inContent a <code>Collection<Content></code> value containing content to add to the request or <code>null</code> * @return a <code>MarketDataRequest</code> value */ public MarketDataRequestBuilder withContent(Collection<Content> inContent) { if(inContent == null || inContent.isEmpty()) { marketdata.setContent(EMPTY_CONTENT); } else { marketdata.setContent(inContent); } return this; } /** * Adds the given content to the market data request. * * <p>The given content value must not be null. This attribute is required and no * default is provided. * * @param inContent a <code>String[]</code> value * @return a <code>MarketDataRequest</code> value * @throws IllegalArgumentException if the specified content results in an invalid request */ public MarketDataRequestBuilder withContent(String...inContent) { if(inContent == null || inContent.length == 0) { marketdata.setContent(EMPTY_CONTENT); } else { List<Content> newContents = new ArrayList<Content>(); for(String contentString : inContent) { if(contentString == null) { throw new IllegalArgumentException(new I18NBoundMessage1P(INVALID_CONTENT, contentString).getText()); } try { // normally, validation is not done at time of attribute-set, but this is a special // case because the given strings must be translated to valid Contents newContents.add(Content.valueOf(contentString.toUpperCase())); } catch (IllegalArgumentException e) { throw new IllegalArgumentException(new I18NBoundMessage1P(INVALID_CONTENT, contentString).getText()); } } withContent(newContents.toArray(new Content[newContents.size()])); } return this; } /** * Adds the given asset class to the market data request. * * <p>This attribute is required. If unspecified, the default value is {@link AssetClass#EQUITY}. * If set to <code>null</code>, the current asset class is removed. * * @param inAssetClass an <code>AssetClass</code> value or <code>null</code> * @return a <code>MarketDataRequest</code> value */ public MarketDataRequestBuilder withAssetClass(AssetClass inAssetClass) { marketdata.setAssetClass(inAssetClass); return this; } /** * Adds the given asset class to the market data request. * * <p>This attribute is required. If * unspecified, the default value is {@link AssetClass#EQUITY}. If the given asset class * is <code>null</code> or empty, the current asset class is removed. * * @param inAssetClass a <code>String</code> value containing a string representation of * an {@link AssetClass} or <code>null</code> * @return a <code>MarketDataRequest</code> value * @throws IllegalArgumentException if the given <code>String</code> is not a valid asset class */ public MarketDataRequestBuilder withAssetClass(String inAssetClass) { if(inAssetClass == null || inAssetClass.isEmpty()) { marketdata.setAssetClass(null); return this; } try { // normally, validation is not done at time of attribute-set, but this is a special // case because the given string must be translated to a valid Asset marketdata.setAssetClass(AssetClass.valueOf(inAssetClass.toUpperCase().trim())); } catch (IllegalArgumentException e) { throw new IllegalArgumentException(new I18NBoundMessage1P(INVALID_ASSET_CLASS, inAssetClass).getText()); } return this; } /** * Adds the given parameter name and value to the market data request. * <p> * See marketdata providers documentation for supported parameters. * Parameters not supported by a marketdata provider will be ignored. * <p> * Both name and value must be non-null. Name and value will have any * whitespace removed. * * @param inName the parameter name * @param inValue the parameter value * @return a <code>MarketDataRequest</code> value */ public MarketDataRequestBuilder withParameter(String inName, String inValue) { if(inName == null || inValue == null) { throw new NullPointerException(); } marketdata.setParameter(inName.trim(), inValue.trim()); return this; } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return marketdata.toString(); } /** * Create a new MarketDataRequestBuilder instance. */ private MarketDataRequestBuilder() { marketdata = new MarketDataRequestBean(); } /** * contains the data to use to create the {@link MarketDataRequest} */ private final MarketDataRequestBean marketdata; /** * empty collection used to indicate that existing symbols or underlying symbols should be removed */ private static final Collection<String> EMPTY_SYMBOLS = Collections.emptyList(); /** * empty collection used to indicate that existing content should be removed */ private static final Collection<Content> EMPTY_CONTENT = EnumSet.noneOf(Content.class); }