All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sangupta.jerry.util.UrlManipulator Maven / Gradle / Ivy

There is a newer version: 3.0.1
Show newest version
/**
 *
 * jerry - Common Java Functionality
 * Copyright (c) 2012-2015, Sandeep Gupta
 * 
 * http://sangupta.com/projects/jerry
 * 
 * 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.
 * 
 */
 
package com.sangupta.jerry.util;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Utility class to manipulate a new Url or an existing url. 
 * The objects of this class should NOT be re-used.
 * Currently, does not support multi-value keys in the query string.
 * 
 * @author sangupta
 *
 */
public class UrlManipulator {
	
	/**
	 * The scheme for pure HTTP non-SSL
	 */
	public static final String HTTP_SCHEME = "http";
	
	/**
	 * Holds the scheme or the protocol
	 */
	private String scheme = HTTP_SCHEME;
	
	/**
	 * Holds the host part - the domain
	 */
	private String host;
	
	/**
	 * Holds the port
	 */
	private int port = 80;
	
	/**
	 * Holds the path
	 */
	private String path;
	
	/**
	 * Holds the query parameters
	 */
	private final Map queryParams = new HashMap();

	/**
	 * Holds the fragment or page anchor
	 */
	private String fragment;
	
	/**
	 * Construct a new URL for the given host and path. A default scheme of HTTP
	 * and default port of 80 is assumed.
	 * 
	 * @param host
	 *            the domain or host to use
	 * 
	 * @param path
	 *            the server path to use
	 * 
	 */
	public UrlManipulator(String host, String path) {
		this.host = host;
		this.path = path;
	}
	
	/**
	 * Construct a new URL for the given host, port and path. A default scheme
	 * of HTTP is assumed.
	 * 
	 * @param host
	 *            the domain or host to use
	 * 
	 * @param port
	 *            the port number to use
	 * 
	 * @param path
	 *            the server path to use
	 */
	public UrlManipulator(String host, int port, String path) {
		this.host = host;
		this.port = port;
		this.path = path;
	}
	
	/**
	 * Construct a new URL for the given scheme, host and path. A default port
	 * value of 80 is assumed.
	 * 
	 * @param scheme
	 *            the scheme or protocol to use
	 * 
	 * @param host
	 *            the domain or host to use
	 * 
	 * @param path
	 *            the server path to use
	 */
	public UrlManipulator(String scheme, String host, String path) {
		this.scheme = scheme;
		this.host = host;
		this.path = path;
	}
	
	/**
	 * Construct a new URL for the given scheme, host, port and path.
	 * 
	 * @param scheme
	 *            the scheme or protocol to use
	 * 
	 * @param host
	 *            the domain or host to use
	 * 
	 * @param port
	 *            the port number to use
	 * 
	 * @param path
	 *            the server path to use
	 */
	public UrlManipulator(String scheme, String host, int port, String path) {
		this.scheme = scheme;
		this.host = host;
		this.port = port;
		this.path = path;
	}
	
	/**
	 * Decompose a URL to allow for easier manipulation. If the URL does not
	 * consists of a scheme (http/https/ftp/etc), a default scheme of HTTP is
	 * considered. No sanity around extracted parts of the URL is made.
	 * 
	 * @param url
	 *            the url to be decomposed
	 *            
	 * @throws IllegalArgumentException
	 *             if the URL supplied is null
	 */
	public UrlManipulator(String url) {
		this(url, true);
	}
	
	/**
	 * Decompose a URL to allow for easier manipulation. If the URL does not
	 * consists of a scheme (http/https/ftp/etc), a default scheme of HTTP is
	 * considered.
	 * 
	 * @param url
	 *            the url to be decomposed
	 * 
	 * @param sanitizeParts
	 *            if set to true, each part extracted from the url
	 *            is sanitized for values that are allowed. For example, a
	 *            scheme can only have characters, a port can only be an
	 *            integer. The flag should be turned off for better performance
	 *            of the method if it is known that the URL is valid before
	 *            hand.
	 * 
	 * @throws IllegalArgumentException
	 *             if the URL supplied is null, empty or contains
	 *             only white spaces. The exception can also be throws if
	 *             sanitization of parts fails.
	 */
	public UrlManipulator(String url, boolean sanitizeParts) {
		if(AssertUtils.isBlank(url)) {
			throw new IllegalArgumentException("URL to be manipulated cannot be null");
		}
		
		// the location from which scanning is to happen
		int scanStart = 0;
		
		// extract the scheme
		int schemeEnd = url.indexOf("://", scanStart);
		if(schemeEnd == -1) {
			this.scheme = HTTP_SCHEME;
		} else {
			this.scheme = url.substring(0, schemeEnd).toLowerCase();
			scanStart = schemeEnd + 3;
			if(sanitizeParts) {
				checkScheme();
			}
		}
		
		// the base domain
		int domainPortEnd = url.indexOf('/', scanStart);
		if(domainPortEnd == -1) {
			// the entire piece is domain and port
			extractDomainAndPort(url, scanStart, url.length());
			
			// no query param and port can be present
			return;
		}
		
		extractDomainAndPort(url, scanStart, domainPortEnd);
		scanStart = ++domainPortEnd;
		
		// now extract the base path
		final int fragmentStart = url.indexOf('#', scanStart);
		final int queryStart = url.indexOf('?', scanStart);
		
		if(queryStart == -1 && fragmentStart == -1) {
			// we are done extracting everything from this url
			// check for path
			this.path = url.substring(domainPortEnd);
			return;
		}
		
		int basePathStart = queryStart;
		if(basePathStart == -1 || (fragmentStart > 0 && fragmentStart < queryStart)) {
			basePathStart = fragmentStart;
		}
		
		this.path = url.substring(domainPortEnd, basePathStart);
		
		// now extract the query parameters
		if(queryStart > 0) {
			if(queryStart < fragmentStart) {
				extractQueryParams(url, queryStart + 1, fragmentStart);
			} else {
				extractQueryParams(url, queryStart + 1, url.length());
			}
		}
		
		// finally extract the fragment
		if(fragmentStart == -1) {
			return;
		}
		
		if(fragmentStart > queryStart) {
			// fragment is present at the end
			this.fragment = url.substring(fragmentStart + 1);
		} else {
			this.fragment = url.substring(fragmentStart + 1, queryStart);
		}
	}
	
	/**
	 * The most common method before disposing off this object would be to
	 * obtain the final constructed url. This is the method for the same, to
	 * obtain the final representation as a String object.
	 * 
	 * @return the final URL representation for details in this manipulator
	 * 
	 */
	public String constructURL() {
		StringBuilder builder = new StringBuilder();
		
		builder.append(this.scheme);
		builder.append("://");
		builder.append(this.host);
		if(this.port != 80) {
			builder.append(':');
			builder.append(String.valueOf(this.port));
		}
		
		// the path
		boolean pathAppended = false;
		
		if(AssertUtils.isNotEmpty(this.path)) {
			if(this.path.charAt(0) != '/') {
				builder.append('/');
			}
			
			builder.append(this.path);
			pathAppended = true;
		}
		
		// the query parameters
		if(AssertUtils.isNotEmpty(this.queryParams)) {
			if(!pathAppended) {
				builder.append('/');
				pathAppended = true;
			}

			builder.append('?');
			boolean first = true;
			final List keys = new ArrayList(this.queryParams.keySet());
			Collections.sort(keys);
			
			String value;
			for(String key : keys) {
				if(!first) {
					builder.append('&');
				}
				
				first = false;
				
				builder.append(key);
				builder.append('=');
				
				value = this.queryParams.get(key);
				if(AssertUtils.isNotEmpty(value)) {
					builder.append(value);
				}
			}
		}
		
		// the fragment
		if(this.fragment != null) {
			if(!pathAppended) {
				builder.append('/');
			}
			
			builder.append('#');
			builder.append(this.fragment);
		}
		
		return builder.toString();
	}
	
	// Java basic overriding methods follow
	
	/**
	 * Return a {@link String} representation of the URL that can be currently
	 * constructed using this manipulator.
	 * 
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return this.constructURL();
	}
	
	// Methods for manipulation start here
	
	/**
	 * Set the new scheme for this URL.
	 * 
	 * @param newScheme
	 *            the new scheme
	 * 
	 * @throws IllegalArgumentException
	 *             if the scheme is null or empty.
	 * 
	 */
	public void setScheme(String newScheme) {
		if(AssertUtils.isEmpty(newScheme)) {
			throw new IllegalArgumentException("Scheme provided cannot be null/empty");
		}
		
		this.scheme = newScheme.toLowerCase();
	}
	
	/**
	 * Set the new host for this URL.
	 * 
	 * @param host
	 *            the new host
	 * 
	 * @throws IllegalArgumentException
	 *             if the host is null or empty.
	 */
	public void setHost(String host) {
		if(AssertUtils.isBlank(host)) {
			throw new IllegalArgumentException("Host provided cannot be null/empty");
		}
		
		this.host = host.toLowerCase();
	}
	
	/**
	 * Set the new port for this URL.
	 * 
	 * @param port
	 *            the value of the port
	 * 
	 * @throws IllegalArgumentException
	 *             if the value of the port is negative
	 * 
	 */
	public void setPort(int port) {
		if(port < 0) {
			throw new IllegalArgumentException("The provided port cannot be negative");
		}
		
		this.port = port;
	}
	
	/**
	 * Set the new path. A null value indicates that we want to
	 * reset to root path.
	 * 
	 * @param path
	 *            the new path for this URL.
	 */
	public void setPath(String path) {
		if(path == null) {
			this.path = null;
			return;
		}
		
		if(path.charAt(0) == '/') {
			this.path = path.substring(1);
			return;
		}
		
		this.path = path;
	}
	
	/**
	 * Set the new fragment. A value of null or empty string, or a
	 * string full of white-spaces, indicate that we don't need a fragment.
	 * 
	 * @param fragment
	 *            the fragment to set
	 */
	public void setFragment(String fragment) {
		if(fragment == null) {
			this.fragment = null;
			return;
		}
		
		fragment = fragment.trim();
		if(fragment.length() == 0) {
			this.fragment = null;
			return;
		}
		
		if(fragment.charAt(0) == '#') {
			this.fragment = fragment.substring(1);
			return;
		}
		
		this.fragment = fragment;
	}
	
	/**
	 * Clear all query parameters that may be present in this URL construct.
	 * 
	 */
	public void clearAllQueryParams() {
		this.queryParams.clear();
	}
	
	/**
	 * Add the given key-value pair as query parameter. The value will be
	 * percent encoded.
	 * 
	 * @param key
	 *            the request parameter name
	 *            
	 * @param value
	 *            the parameter value
	 * 
	 * @throws IllegalArgumentException
	 *             if the key is empty/null
	 */
	public void setQueryParam(String key, String value) {
		this.setQueryParam(key, value, true);
	}
	
	/**
	 * Add the given key-value pair as query parameter, optionally encoding the
	 * value if desired. Encoding used is percent-encoding.
	 * 
	 * @param key
	 *            the request parameter name
	 *            
	 * @param value
	 *            the parameter value
	 *            
	 * @param encodeValue
	 *            whether the given value should be percent-encoded or not
	 * 
	 * @throws IllegalArgumentException
	 *             if the key is empty/null
	 */
	public void setQueryParam(String key, String value, boolean encodeValue) {
		if(AssertUtils.isEmpty(key)) {
			throw new IllegalArgumentException("Key cannot be null/empty");
		}
		
		if(AssertUtils.isEmpty(value)) {
			this.queryParams.put(key, null);
			return;
		}
		
		if(!encodeValue) {
			this.queryParams.put(key, value);
			return;
		}
		
		this.queryParams.put(key, UriUtils.encodeURIComponent(value));
	}
	
	/**
	 * Remove the query parameter with the given name.
	 * 
	 * @param key
	 *            the request parameter name
	 *            
	 */
	public void removeQueryParam(String key) {
		this.queryParams.remove(key);
	}
	
	// Simple accessors methods on the internal values start here
	
	/**
	 * Return the current scheme
	 * 
	 * @return the current scheme
	 */
	public String getScheme() {
		return this.scheme;
	}
	
	/**
	 * Return the value of the current host.
	 * 
	 * @return the current host
	 */
	public String getHost() {
		return this.host;
	}
	
	/**
	 * Return the value of the current port
	 * 
	 * @return the current port
	 */
	public int getPort() {
		return this.port;
	}
	
	/**
	 * Return the value of the current path
	 * 
	 * @return the current server path
	 */
	public String getPath() {
		return this.path;
	}
	
	/**
	 * Return the value of the current fragment
	 * 
	 * @return the current fragment
	 */
	public String getFragment() {
		return this.fragment;
	}
	
	/**
	 * Return the number of query parameters
	 * 
	 * @return the current query parameters
	 */
	public int getNumQueryParams() {
		if(this.queryParams == null) {
			return 0;
		}
		
		return this.queryParams.size();
	}
	
	// Supporting internal methods start
	
	/**
	 * Extract the domain name and port from the given URL, where domain and
	 * port start and end indexes are provided.
	 * 
	 * @param url
	 *            the url from which values are extracted
	 * 
	 * @param start
	 *            the start index
	 * 
	 * @param end
	 *            the end index
	 */
	private void extractDomainAndPort(String url, int start, int end) {
		int hasPort = url.indexOf(':', start);
		if(hasPort != -1 && hasPort < end) {
			// port exists
			this.host = url.substring(start, hasPort).toLowerCase();
			
			String portValue = url.substring(hasPort + 1, end);
			
			try {
				this.port = Integer.parseInt(portValue);
			} catch(NumberFormatException e) {
				throw new IllegalArgumentException("Port value cannot be parsed to an integer value");
			}
		} else {
			this.host = url.substring(start, end).toLowerCase();
			this.port = 80;
		}
	}
	
	/**
	 * Extract all the query parameters from this URL. Parse them and extract
	 * the key value pairs from this segment of the url.
	 * 
	 * @param url
	 *            the url from which query paramters need to be extracted
	 * 
	 * @param start
	 *            the starting index for query parameters
	 * 
	 * @param end
	 *            the ending index for query paramets
	 * 
	 */
	private void extractQueryParams(String url, int start, int end) {
		String segment = url.substring(start, end);
		
		String[] tokens = segment.split("&");
		for(String token : tokens) {
			String[] pair = token.split("=");
			if(pair.length != 2) {
				throw new IllegalArgumentException("Query parameter is not a valid pair: " + token);
			}
			
			this.queryParams.put(pair[0], pair[1]);
		}
	}

	/**
	 * Method that checks the scheme to be a valid value. The valid
	 * values in a scheme are an alphabetic character or a digit.
	 * 
	 */
	private void checkScheme() {
		if(this.scheme == null || this.scheme.trim().length() == 0) {
			throw new IllegalArgumentException("Scheme cannot be null/empty");
		}
		
		char[] chars = this.scheme.toCharArray();
		for(char c : chars) {
			if(!(isDigit(c) || isAlphabet(c))) {
				throw new IllegalArgumentException("Invalid characters in scheme: " + this.scheme);
			}
		}
	}

	private static boolean isDigit(char c) {
		if(('0' <= c ) && (c <= '9')) {
			return true;
		}
		
		return false;
	}

	/**
	 * Check if the given character is an alphabet in English or not
	 * 
	 * @param c
	 *            the character to test
	 * 
	 * @return true if char is alphabet, false
	 *         otherwise
	 */
	private static boolean isAlphabet(char c) {
		if(('a' <= c ) && (c <= 'z')) {
			return true;
		}

		if(('A' <= c ) && (c <= 'Z')) {
			return true;
		}

		return false;
	}
	
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy