/**
* VMware Continuent Tungsten Replicator
* Copyright (C) 2015 VMware, Inc. All rights reserved.
*
* Licensed 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.
*
* Initial developer(s): Edward Archibald
* Contributor(s): Robert Hodges
*/
package com.continuent.tungsten.common.cluster.resource;
import java.sql.SQLException;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.UUID;
import org.apache.log4j.Logger;
/**
* Implements a simple parser for SQLRouter URLs. It identifies and strips out
* t-router properties
*
* @author <a href="mailto:edward.archibald@continuent.com">Edward Archibald</a>
* @version 1.0
*/
public class RouterURL implements Cloneable
{
private static final String URL_OPTIONS_DELIMITERS = "&=?";
private static Logger logger = Logger.getLogger(RouterURL.class);
private static final String URL_ELMT_JDBC = "jdbc";
private static final String URL_ELMT_TROUTER = "t-router";
public static final String URL_FULL_HEADER = URL_ELMT_JDBC
+ ':'
+ URL_ELMT_TROUTER
+ "://";
// Keys for specific, internal use, connection properties
public static final String KEY_MAX_APPLIED_LATENCY = "maxAppliedLatency";
public static final String KEY_QOS = "qos";
public static final String KEY_SESSION_ID = "sessionId";
public static final String KEY_AFFINITY = "affinity";
public static final String KEY_USER = "user";
public static final String KEY_PASSWORD = "password";
/** How the password will be displayed when hiding it */
private static final String OBFUSCATED_PASSWORD = "<obfuscated>";
// various predefined session ids
public static final String SESSIONID_CONNECTION = "CONNECTION";
public static final String SESSIONID_DATABASE = "DATABASE";
public static final String SESSIONID_USER = "USER";
public static final String SESSIONID_PROVIDED_IN_DBNAME = "PROVIDED_IN_DBNAME";
/** Special tag to be replaced by a database name dynamically */
public static final String DBNAME_TOKEN = "${DBNAME}";
// Parsed URL data
private String dataServiceName = "UNDEFINED";
private String dbname = "";
private QualityOfService qos = QualityOfService.RW_STRICT;
public static double MAX_APPLIED_LATENCY_UNDEF = -1;
private double maxAppliedLatency = MAX_APPLIED_LATENCY_UNDEF;
private String sessionId = null;
private String affinity = null;
private boolean autoSession = false;
/**
* These properties hold only the non-RouterURL settings, ie. the JDBC
* driver specific ones
*/
private Properties props = new Properties();
// Parsing information.
// TUC-1065: don't store the original URL, this would double the information
/**
* After parsing, this will store the last position character position of
* the URL base ([jdbc:t-router://<service>/<db>]<opts>)
*/
private int urlBaseEndIndex = 0;
/**
* Creates a parsed URL object.<br>
* Valid driver URLs are: jdbc:t-router://service/<database-name>
* [?][qos={RW_STRICT|RW_RELAXED|RO_STRICT|RO_RELAXED}]
* [?|&][otheroptions=value...]<br>
* The default qos (Quality of service) is RW_STRICT unless specified or
* unless overridden by the service configuration.<br>
* Properties passed via the URL supersede the ones in the properties. Last
* property defined in the URL wins.
*
* @param url SQL router URL
* @param info Properties for URL
* @throws SQLException if the URL cannot be parsed or misses the data
* service name or database name
*/
public RouterURL(String url, Properties info) throws SQLException
{
parseUrl(url, info);
}
/**
* Parses the driver URL and extracts the properties.
*
* @param url the URL to parse
* @param info any existing properties already loaded in a
* <code>Properties</code> object
* @throws SQLException if the URL is not correctly formed or if the data
* service name or database name are missing
*/
private void parseUrl(String url, Properties info) throws SQLException
{
// TUC-1065: don't store the original URL, this would double the
// information
// Record the parsing position
this.urlBaseEndIndex = 0;
// Add input properties if supplied.
if (info != null)
props.putAll(info);
// Skip jdbc protocol.
if (!"jdbc".equalsIgnoreCase(nextUrlBaseToken(url)))
{
throw new SQLException("URL lacks 'jdbc' protocol: " + url);
}
// Skip sub-protocol.
if (!"t-router".equalsIgnoreCase(nextUrlBaseToken(url)))
{
throw new SQLException("URL lacks 't-router' sub-protocol: " + url);
}
// Get the service name.
dataServiceName = nextUrlBaseToken(url);
if (dataServiceName == null)
{
throw new SQLException("Missing data service name in URL: " + url);
}
// Get the database name.
dbname = nextUrlBaseToken(url);
if (dbname == null)
{
dbname = "";
}
parseURLOptions(url.substring(urlBaseEndIndex));
if (logger.isDebugEnabled())
{
logger.debug("Parsed t-router URL: " + toString());
}
}
public void parseURLOptions(String substring) throws SQLException
{
urlOptionsToProperties(substring, props);
transferRouterPropertiesToMemberVariables();
}
/**
* Given a string of URL options (eg. affinity=blah&maxAppliedLatency=2),
* extracts each option and add them to the given Properties parameter.
*
* @param urlOptions string to parse
* @param p output properties to which options will be added, overwriting
* them if already in
* @throws SQLException in case of parsing error
*/
public static void urlOptionsToProperties(String urlOptions, Properties p)
throws SQLException
{
String key;
StringTokenizer st = new StringTokenizer(urlOptions,
URL_OPTIONS_DELIMITERS);
while (st.hasMoreTokens())
{
key = st.nextToken();
if (!st.hasMoreTokens())
{
throw new SQLException("Invalid empty value for property '"
+ key + "' in URL: " + urlOptions);
}
String value = st.nextToken();
p.setProperty(key, value);
}
}
/**
* Iterates through all properties and removes the ones that are router-
* specific. Sets the member variables with these values
*
* @throws SQLException upon illegal values
*/
protected void transferRouterPropertiesToMemberVariables()
throws SQLException
{
transferPropertiesToRouterURLMemberVariables(props);
}
public void transferPropertiesToRouterURLMemberVariables(Properties propsArg)
throws SQLException
{
// If QOS is among the properties, set it explicitly and remove it from
// there
String qosValue = (String) propsArg.remove(KEY_QOS);
if (qosValue != null)
{
try
{
qos = QualityOfService
.valueOf(QualityOfService.class, qosValue);
}
catch (IllegalArgumentException i)
{
StringBuilder msg = new StringBuilder();
msg.append("Invalid value '").append(qosValue)
.append("' passed for the quality of service.")
.append(" Valid values are: ");
for (QualityOfService q : QualityOfService.values())
{
msg.append(q.toString()).append(' ');
}
throw new SQLException(msg.toString());
}
}
// Same for max latency...
String maxAppliedLatencyValue = (String) propsArg
.remove(KEY_MAX_APPLIED_LATENCY);
if (maxAppliedLatencyValue != null)
{
try
{
this.maxAppliedLatency = Double
.parseDouble(maxAppliedLatencyValue);
}
catch (NumberFormatException nfe)
{
logger.warn("URL option maxAppliedLatency value "
+ maxAppliedLatencyValue
+ " could not be parsed correctly - defaulting to -1 (undef)");
this.maxAppliedLatency = MAX_APPLIED_LATENCY_UNDEF;
}
}
// ...for affinity...
String affinityInProps = (String) propsArg.remove(KEY_AFFINITY);
if (affinityInProps != null)
{
this.affinity = affinityInProps;
}
// ...and for session ID
String propsSessionId = (String) propsArg.remove(KEY_SESSION_ID);
if (propsSessionId != null)
{
boolean wasAutoSession = isAutoSession();
autoSession = false;
if (propsSessionId.equals(SESSIONID_CONNECTION)
||
// smartScale Fall back when no sessionId is given
propsSessionId.equals(SESSIONID_PROVIDED_IN_DBNAME))
{
autoSession = true;
// generate a session id only if not already generated
// previously
if (!wasAutoSession)
{
sessionId = UUID.randomUUID().toString();
}
}
else if (propsSessionId.equals(SESSIONID_DATABASE))
{
autoSession = true;
if (dbname != null)
{
sessionId = dbname;
}
else
{
throw new SQLException(
"You must supply a database name to use the "
+ "DATABASE based sessionId");
}
}
else if (propsSessionId.equals(SESSIONID_USER))
{
autoSession = true;
String user = getProperty(KEY_USER);
if (user != null)
{
sessionId = user;
}
else
{
throw new SQLException(
"You must supply a user name for the URL property "
+ "'user' to use the USER based sessionId");
}
}
else
{
sessionId = propsSessionId;
}
}
}
/**
* Extracts the next lexical token from the provided URL base.
*
* @param url The URL being parsed
* @param pos The current position in the URL string.
* @return The next string until one of the following character is found:
* '?' ';' ':' '/', or null if no token (ie. empty token) was found
*/
private String nextUrlBaseToken(String url)
{
StringBuffer token = new StringBuffer();
while (urlBaseEndIndex < url.length())
{
char ch = url.charAt(urlBaseEndIndex++);
if (ch == ':' || ch == ';' || ch == '?')
{
break;
}
if (ch == '/')
{
if (urlBaseEndIndex < url.length()
&& url.charAt(urlBaseEndIndex) == '/')
{
urlBaseEndIndex++;
continue;
}
else
{
break;
}
}
token.append(ch);
}
if (token.length() == 0)
return null;
else
return token.toString();
}
public void setProperties(String propsStr) throws SQLException
{
String key;
StringTokenizer st = new StringTokenizer(propsStr,
URL_OPTIONS_DELIMITERS);
while (st.hasMoreTokens())
{
key = st.nextToken();
if (!st.hasMoreTokens())
{
throw new SQLException("Invalid empty value for property '"
+ key + "' in properties string: " + propsStr);
}
String value = st.nextToken();
props.setProperty(key, value);
}
transferRouterPropertiesToMemberVariables();
}
/**
* Returns the dataServiceName value.
*
* @return Returns the dataServiceName.
*/
public String getDataServiceName()
{
return dataServiceName;
}
/**
* Changes the database name. This triggers a replacement of the database
* name in the URL string as well
*
* @param dbname The dbname to set.
*/
public void setDbname(String dbname)
{
this.dbname = dbname;
}
public double getMaxAppliedLatency()
{
return maxAppliedLatency;
}
public void setMaxAppliedLatency(double maxAppliedLatencyPrm)
{
maxAppliedLatency = maxAppliedLatencyPrm;
}
/**
* Upon catalog change, it can be required to change the session ID
*
* @param newSessionId
*/
public void setSessionId(String newSessionId)
{
sessionId = newSessionId;
}
public String getSessionId()
{
return sessionId;
}
public String getService()
{
return dataServiceName;
}
public String getDbname()
{
return dbname;
}
public Properties getProps()
{
return props;
}
public QualityOfService getQos()
{
return qos;
}
public void setQoS(QualityOfService qosParam)
{
qos = qosParam;
}
public String getAffinity()
{
return affinity;
}
public void setAffinity(String affinityParam)
{
affinity = affinityParam;
}
/**
* Provides the property with the given key from the parsed and passed URL
* props. If the property is not set, returns null
*
* @param key the property key to retrieve
* @return the given property or null if no such property exists
*/
public String getProperty(String key)
{
return props.getProperty(key);
}
public Properties getObfuscatedPasswordPropsCopy()
{
Properties obfuscatedPasswordProps = (Properties) props.clone();
obfuscatedPasswordProps.setProperty(KEY_PASSWORD, OBFUSCATED_PASSWORD);
return obfuscatedPasswordProps;
}
public String toString()
{
Properties obfuscatedPasswordProps = getObfuscatedPasswordPropsCopy();
StringBuilder sb = new StringBuilder(URL_FULL_HEADER);
sb.append(dataServiceName).append('/').append(dbname).append(" QoS=")
.append(qos).append(" sessionId=").append(sessionId)
.append(" maxAppliedLatency=").append(maxAppliedLatency)
.append(" affinity=").append(affinity)
.append(" jdbc driver options=")
.append(obfuscatedPasswordProps);
return sb.toString();
}
public boolean isAutoSession()
{
return autoSession;
}
@Override
public boolean equals(Object obj)
{
if (obj == null || !(obj instanceof RouterURL))
{
return false;
}
RouterURL compareTo = (RouterURL) obj;
// data service name can't be null, or that would have thrown an error
// in the constructor
if (!dataServiceName.equals(compareTo.dataServiceName))
{
return false;
}
// database name can't be null, or that would have thrown an error
// in the constructor
if (!dbname.equals(compareTo.dbname))
{
return false;
}
// props can't be null, they are initialized at construction time
return props.equals(compareTo.props);
}
@Override
public Object clone() throws CloneNotSupportedException
{
RouterURL clone = null;
try
{
clone = new RouterURL(URL_FULL_HEADER + dataServiceName + '/'
+ dbname, props);
}
catch (SQLException sqle)
{
throw new CloneNotSupportedException(
"Failed to create clone because of "
+ sqle.getLocalizedMessage());
}
clone.maxAppliedLatency = this.maxAppliedLatency;
clone.affinity = this.affinity;
clone.autoSession = this.autoSession;
clone.urlBaseEndIndex = this.urlBaseEndIndex;
clone.qos = this.qos;
clone.sessionId = this.sessionId;
return clone;
}
}