package com.tesora.dve.common; /* * #%L * Tesora Inc. * Database Virtualization Engine * %% * Copyright (C) 2011 - 2014 Tesora Inc. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, version 3, * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * #L% */ import java.net.URI; import java.net.URISyntaxException; import java.util.Properties; import org.apache.commons.lang.StringUtils; import com.tesora.dve.exceptions.PEException; public class PEUrl { private static String URL_PARAMETER_SEPARATOR = "?"; public static String stripUrlParameters(final String url) { return StringUtils.substringBeforeLast(url.trim(), URL_PARAMETER_SEPARATOR); } private String protocol; private String subProtocol; private String host; private int port; private String path; private String query; private Properties queryOptions = new Properties(); private boolean hasPort = false; /** * Create an uninitialized instance of PEUrl. */ public PEUrl() { } /** * Create an instance of PEUrl and initialize with values from Properties * object. If the Properties contains a prefix."url" key, PEUrl will be * initialized with that; otherwise it will look for prefix."host", * prefix."port", prefix."type" and prefix."dbname" * * This doesn't currently handle passing query options via a properties file * * @param Properties * props * @param String * prefix * @throws PEException * if the ".url" is malformed or the props doesn't contain * enough info to make a valid URL. */ public PEUrl(Properties props, String prefix) throws PEException { this(props, prefix, false); } /** * Create an instance of PEUrl and initialize with values from Properties * object. If the Properties contains a prefix."url" key, PEUrl will be * initialized with that; otherwise it will look for prefix."host", * prefix."port", prefix."type" and prefix."dbname". If setDefaults is true, * then defaults will be filled in for host (localhost), port (3306) and * type (mysql). * * This doesn't currently handle passing query options via a properties file * * @param Properties * props * @param String * prefix * @param boolean useDefaults * @throws PEException * if the ".url" is malformed. */ public PEUrl(Properties props, String prefix, boolean useDefaults) throws PEException { initializeFromProps(props, prefix, useDefaults); if (!isInitialized()) { throw new PEException("Error creating URL object from Properties: " + props.toString()); } } /** * Initialize with the information contained in <code>urlString</code> * * @param urlString * - a String representing a valid URL * @throws PEException * - if <code>urlString</code> is not a properly formed URL */ public static PEUrl fromUrlString(String urlString) throws PEException { PEUrl url = new PEUrl(); url.parseURL(urlString); if (!url.isInitialized()) throw new PEException("Error creating URL object from string: " + urlString); return url; } public static PEUrl isValidUrlWithPort(String urlString) throws PEException { // This is a test to ensure the url is well formed PEUrl pUrl = fromUrlString(urlString); // PEUrl will have done some checking - but we want to ensure we // have a valid port if (!pUrl.hasPort()) throw new PEException("URL '" + urlString + "' does not contain a valid port"); return pUrl; } /** * Initialize with the information contained in <code>connectString</code> * * @param connectString * - a String representing parameters for a URL in * "connect string" format. e.g. * "host=localhost;port=3306;dbname=dve_catalog" * @throws PEException * - if <code>connectString</code> is malformed - connectString * must contain at least a "host" token * */ public static PEUrl fromConnectString(String connectString) throws PEException { PEUrl url = new PEUrl(); // TODO this "parser" will not handle embedded delimeters String delims = "[=;]"; String[] tokens = connectString.split(delims); if (tokens.length % 2 != 0) throw new PEException("Connect String does not contain the correct number of elements"); Properties props = new Properties(); for (int i = 0; i < tokens.length; i = i + 2) { props.setProperty(tokens[i], tokens[i + 1]); } url.initializeFromProps(props, null /* prefix */, false /* useDefaults */); if (!url.isInitialized()) throw new PEException("Error creating URL object from connect string: " + connectString); return url; } /** * Is this PEUrl instance properly initialized. Minimally, this means that * the protocol, subProtocol and host are set. (set via PEUrl(urlString) * constructor or calling setXXX methods * * @return boolean indicating if the instance is properly initialized */ boolean isInitialized() { return (protocol != null && subProtocol != null && host != null && !host.isEmpty()); } /** * Will populate an empty PEUrl instance with the default URL parameters for * a MySQL JDBC connection. Calling example: * <code>PEUrl url = new PEUrl().createMysqlDefaultURL()</code> * * @return PEUrl instance representing "jdbc:mysql://localhost:3306" */ public PEUrl createMysqlDefaultURL() { setProtocol(PEConstants.MYSQL_PROTOCOL); setSubProtocol(PEConstants.MYSQL_SUBPROTOCOL); setHost(PEConstants.MYSQL_HOST); setPort(PEConstants.MYSQL_PORT); return this; } /** * Gets a queryOption from the option properties based on a key value. * * @param optionKey * - the key for the option to return * @return String - value of option for key <code>optionKey</code> */ public String getOption(String optionKey) { return queryOptions.getProperty(optionKey); } /** * Returns a string representation of the (i.e. a URL) of the current state * of this PEUrl instance. Instance must be properly initialized - meaning * that at least the protocol, subProtocol and host attributes must be set. * * @return String representation of URL * @throws PEException * - if instance isn't properly initialized. */ public String getURL() throws PEException { if (!isInitialized()) throw new PEException("Cannot call getURL method when PEUrl object is not initialized"); StringBuffer sb = new StringBuffer(); sb.append(protocol).append(':').append(subProtocol).append("://").append(getAuthority()); if (path != null) { if (!path.isEmpty() && !path.startsWith("/")) { sb.append('/'); } sb.append(path); } if (query != null) sb.append('?').append(query); return sb.toString(); } public String getProtocol() { return protocol; } /** * Set the protocol to <code>protocol</code> * * @param protocol * @return PEUrl this */ public PEUrl setProtocol(String protocol) { this.protocol = protocol; return this; } public String getSubProtocol() { return subProtocol; } /** * Set the sub protocol to <code>subProtocol</code> * * @param subProtocol * @return PEUrl this */ public PEUrl setSubProtocol(String subProtocol) { this.subProtocol = subProtocol; return this; } public String getAuthority() { return host + (hasPort ? ":" + port : ""); } /** * Set the authority to <code>authority</code> This set the host and port as * appropriate. e.g. calling <code>setAuthority("localhost:6800")</code> * will set host to "localhost" and port to 6800 * * @param authority * @return PEUrl this */ public void setAuthority(String authority) { String[] authorityA = authority.split(":"); if (authorityA.length == 2) { hasPort = true; port = Integer.parseInt(authorityA[1]); } host = authorityA[0]; } public String getHost() { return host; } /** * Set the host to <code>host</code>. * * @param host * @return PEUrl this */ public PEUrl setHost(String host) { this.host = host; return this; } public int getPort() { return port; } /** * Set the port to <code>port</code>. * * @param int port * @return PEUrl this */ public PEUrl setPort(int port) { hasPort = true; this.port = port; return this; } public boolean hasPort() { return hasPort; } /** * Set the port to <code>port</code>. * * @param String * port * @return PEUrl this */ public PEUrl setPort(String port) { setPort(Integer.parseInt(port)); return this; } public String getPath() { return path; } /** * Set the path to <code>path</code>. * * @param String * path * @return PEUrl this */ public PEUrl setPath(String path) { this.path = path; return this; } public String getQuery() { return query; } /** * Set the query options to <code>query</code>. Query options are of the * form "opt1Key=optValue1&opt2Key=optValue2" * * @param String * query * @return PEUrl this */ public PEUrl setQuery(String query) throws PEException { this.query = query; queryOptions = parseURLQuery(query); return this; } public PEUrl clearQuery() { this.query = null; queryOptions.clear(); return this; } /** * Return all the query options as <code>Properties</code> * * @return Properties - the query options */ public Properties getQueryOptions() { return queryOptions; } /** * Set one query option for the URL. * * @param key * for Option * @param value * for Option * @return PEUrl this */ public PEUrl setQueryOption(String key, String value) { queryOptions.setProperty(key, value); calcQueryString(); return this; } public PEUrl setQueryOptions(Properties props) { for (String key : props.stringPropertyNames()) { setQueryOption(key, props.getProperty(key)); } return this; } private void initializeFromProps(Properties props, String prefix, boolean useDefaults) throws PEException { // TODO handle query options String normPrefix = PEStringUtils.normalizePrefix(prefix); if (props.getProperty(normPrefix + "url") != null) { parseURL(props.getProperty(normPrefix + "url")); } else { if (useDefaults) createMysqlDefaultURL(); else { setProtocol(PEConstants.MYSQL_PROTOCOL); setSubProtocol(PEConstants.MYSQL_SUBPROTOCOL); } if (props.getProperty(normPrefix + "host") != null) setHost(props.getProperty(normPrefix + "host")); if (props.getProperty(normPrefix + "port") != null) setPort(props.getProperty(normPrefix + "port")); if (props.getProperty(normPrefix + "dbname") != null) setPath(props.getProperty(normPrefix + "dbname")); } } // [protocol:subprotocol:][//authority][path][?query][#fragment] private PEUrl parseURL(String urlString) throws PEException { try { URI parser = new URI(urlString.trim()); final String protocol = parser.getScheme(); parser = URI.create(parser.getSchemeSpecificPart()); final String subProtocol = parser.getScheme(); final String authority = parser.getAuthority(); if ((protocol == null) || (subProtocol == null)) { throw new PEException("Malformed URL '" + urlString + "' - incomplete protocol"); } else if (authority == null) { throw new PEException("Malformed URL '" + urlString + "' - invalid authority"); } this.setProtocol(protocol); this.setSubProtocol(subProtocol); final String host = parser.getHost(); if (host != null) { this.setHost(host); } final int port = parser.getPort(); if (port != -1) { this.setPort(port); } final String query = parser.getQuery(); if (query != null) { this.setQuery(query); } final String path = parser.getPath(); if ((path != null) && !path.isEmpty()) { this.setPath(path.startsWith("/") ? path.substring(1) : path); } return this; } catch (final URISyntaxException | IllegalArgumentException e) { throw new PEException("Malformed URL '" + urlString + "'", e); } } private Properties parseURLQuery(String urlQuery) throws PEException { Properties urlQProps = new Properties(); String[] params = urlQuery.split("&"); for (String param : params) { String[] queryElems = param.split("="); if (queryElems.length != 2) throw new PEException("Invalid options specified on URL"); urlQProps.setProperty(queryElems[0], queryElems[1]); } return urlQProps; } private void calcQueryString() { query = ""; for (String key : queryOptions.stringPropertyNames()) { if (!query.isEmpty()) query += "&"; query += key + "=" + queryOptions.getProperty(key); } } @Override public String toString() { try { return getURL(); } catch (Exception e) { // ignore } return super.toString(); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((host == null) ? 0 : host.hashCode()); result = prime * result + ((path == null) ? 0 : path.hashCode()); result = prime * result + port; result = prime * result + ((protocol == null) ? 0 : protocol.hashCode()); result = prime * result + ((query == null) ? 0 : query.hashCode()); result = prime * result + ((queryOptions == null) ? 0 : queryOptions.hashCode()); result = prime * result + ((subProtocol == null) ? 0 : subProtocol.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; PEUrl other = (PEUrl) obj; if (host == null) { if (other.host != null) return false; } else if (!host.equals(other.host)) return false; if (path == null) { if (other.path != null) return false; } else if (!path.equals(other.path)) return false; if (port != other.port) return false; if (protocol == null) { if (other.protocol != null) return false; } else if (!protocol.equals(other.protocol)) return false; if (query == null) { if (other.query != null) return false; } else if (!query.equals(other.query)) return false; if (queryOptions == null) { if (other.queryOptions != null) return false; } else if (!queryOptions.equals(other.queryOptions)) return false; if (subProtocol == null) { if (other.subProtocol != null) return false; } else if (!subProtocol.equals(other.subProtocol)) return false; return true; } }