/*
 * The Apache Software License, Version 1.1
 *
 *
 * Copyright (c) 1999,2001 The Apache Software Foundation.  All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowledgment may appear in the software itself,
 *    if and wherever such third-party acknowledgments normally appear.
 *
 * 4. The names "Xerces" and "Apache Software Foundation" must
 *    not be used to endorse or promote products derived from this
 *    software without prior written permission. For written
 *    permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache",
 *    nor may "Apache" appear in their name, without prior written
 *    permission of the Apache Software Foundation.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation and was
 * originally based on software copyright (c) 1999, iClick Inc.,
 * http://www.apache.org.  For more information on the Apache Software
 * Foundation, please see <http://www.apache.org/>.
 */

/* Modified by Jeremy J. Carroll HP
 *
 * Was originally org/apache/xerces/utils/URI.java in Xerces 1.4.4
 *
 */

package org.wymiwyg.commons.util;

import java.io.Serializable;
import java.util.StringTokenizer;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
//copied from jena src-distribution, because I need relativied (better/different than jdk1.4)
/**********************************************************************
* A class to represent a Uniform Resource Identifier (URI). This class
* is designed to handle the parsing of URIs and provide access to
* the various components (scheme, host, port, userinfo, path, query
* string and fragment) that may constitute a URI.
*

* Parsing of a URI specification is done according to the URI
* syntax described in RFC 2396
* <http://www.ietf.org/rfc/rfc2396.txt?number=2396>. Every URI consists
* of a scheme, followed by a colon (':'), followed by a scheme-specific
* part. For URIs that follow the "generic URI" syntax, the scheme-
* specific part begins with two slashes ("//") and may be followed
* by an authority segment (comprised of user information, host, and
* port), path segment, query segment and fragment. Note that RFC 2396
* no longer specifies the use of the parameters segment and excludes
* the "user:password" syntax as part of the authority segment. If
* "user:password" appears in a URI, the entire user/password string
* is stored as userinfo.
*

* For URIs that do not follow the "generic URI" syntax (e.g. mailto),
* the entire scheme-specific part is treated as the "path" portion
* of the URI.
*

* Note that, unlike the java.net.URL class, this class does not provide
* any built-in network access functionality nor does it provide any
* scheme-specific functionality (for example, it does not know a
* default port for a specific scheme). Rather, it only knows the
* grammar and basic set of operations that can be applied to a URI.
*
* @version  $Id: URI.java,v 1.2 2004/07/06 05:20:50 rebach Exp $
*
**********************************************************************/
public class URI implements Serializable {

	static Log logger = LogFactory.getLog(URI.class);

	/** reserved characters */
	private static final String RESERVED_CHARACTERS = ";/?:@&=+$,[]";

	/** URI punctuation mark characters - these, combined with
	    alphanumerics, constitute the "unreserved" characters */
	private static final String MARK_CHARACTERS = "-_.!~*'() ";

	/** scheme can be composed of alphanumerics and these characters */
	private static final String SCHEME_CHARACTERS = "+-.";

	/** userinfo can be composed of unreserved, escaped and these
	    characters */
	private static final String USERINFO_CHARACTERS = ";:&=+$,";

	/** Stores the scheme (usually the protocol) for this URI. */
	private String m_scheme = null;

	/** If specified, stores the userinfo for this URI; otherwise null */
	private String m_userinfo = null;

	/** If specified, stores the host for this URI; otherwise null */
	private String m_host = null;

	/** If specified, stores the port for this URI; otherwise null */
	private String m_port = null;
	private int n_port = -1;

	/** If specified, stores the path for this URI; otherwise null */
	private String m_path = null;
	/** Lazily assigned variable being: ??
     * { m_path +"a", dir, parent-dir, grand-parent-dir }
     * Each component is lazily assigned too.
	 */
	private String m_subPaths[] = null;

	/** If specified, stores the query string for this URI; otherwise
	    null.  */
	private String m_queryString = null;

	/** If specified, stores the fragment for this URI; otherwise null */
	private String m_fragment = null;

	private static boolean DEBUG = false;

	/**
	* Construct a new and uninitialized URI.
	*/
	public URI() {
	}

	/**
	 * Construct a new URI from another URI. All fields for this URI are
	 * set equal to the fields of the URI passed in.
	 *
	 * @param p_other the URI to copy (cannot be null)
	 */
	public URI(URI p_other) {
		initialize(p_other);
	}

	/**
	 * Construct a new URI from a URI specification string. If the
	 * specification follows the "generic URI" syntax, (two slashes
	 * following the first colon), the specification will be parsed
	 * accordingly - setting the scheme, userinfo, host,port, path, query
	 * string and fragment fields as necessary. If the specification does
	 * not follow the "generic URI" syntax, the specification is parsed
	 * into a scheme and scheme-specific part (stored as the path) only.
	 *
	 * @param p_uriSpec the URI specification string (cannot be null or
	 *                  empty)
	 *
	 * @exception MalformedURIException if p_uriSpec violates any syntax
	 *                                   rules
	 */
	public URI(String p_uriSpec) throws MalformedURIException {
		this((URI) null, p_uriSpec);
	}

	/**
	 * Construct a new URI from a base URI and a URI specification string.
	 * The URI specification string may be a relative URI.
	 *
	 * @param p_base the base URI (cannot be null if p_uriSpec is null or
	 *               empty)
	 * @param p_uriSpec the URI specification string (cannot be null or
	 *                  empty if p_base is null)
	 *
	 * @exception MalformedURIException if p_uriSpec violates any syntax
	 *                                  rules
	 */
	public URI(URI p_base, String p_uriSpec) throws MalformedURIException {
		initialize(p_base, p_uriSpec);
	}

	/**
	 * Construct a new URI that does not follow the generic URI syntax.
	 * Only the scheme and scheme-specific part (stored as the path) are
	 * initialized.
	 *
	 * @param p_scheme the URI scheme (cannot be null or empty)
	 * @param p_schemeSpecificPart the scheme-specific part (cannot be
	 *                             null or empty)
	 *
	 * @exception MalformedURIException if p_scheme violates any
	 *                                  syntax rules
	 */
	public URI(String p_scheme, String p_schemeSpecificPart)
		throws MalformedURIException {
		if (p_scheme == null || p_scheme.length() == 0) {
			throw new MalformedURIException("Cannot construct URI with null/empty scheme!");
		}
		if (p_schemeSpecificPart == null
			|| p_schemeSpecificPart.length() == 0) {
			throw new MalformedURIException("Cannot construct URI with null/empty scheme-specific part!");
		}
		setScheme(p_scheme);
		setPath(p_schemeSpecificPart);
	}

	/**
	 * Construct a new URI that follows the generic URI syntax from its
	 * component parts. Each component is validated for syntax and some
	 * basic semantic checks are performed as well.  See the individual
	 * setter methods for specifics.
	 *
	 * @param p_scheme the URI scheme (cannot be null or empty)
	 * @param p_host the hostname or IPv4 address for the URI
	 * @param p_path the URI path - if the path contains '?' or '#',
	 *               then the query string and/or fragment will be
	 *               set from the path; however, if the query and
	 *               fragment are specified both in the path and as
	 *               separate parameters, an exception is thrown
	 * @param p_queryString the URI query string (cannot be specified
	 *                      if path is null)
	 * @param p_fragment the URI fragment (cannot be specified if path
	 *                   is null)
	 *
	 * @exception MalformedURIException if any of the parameters violates
	 *                                  syntax rules or semantic rules
	 */
	public URI(
		String p_scheme,
		String p_host,
		String p_path,
		String p_queryString,
		String p_fragment)
		throws MalformedURIException {
		this(p_scheme, null, p_host, -1, p_path, p_queryString, p_fragment);
	}

	/**
	 * Construct a new URI that follows the generic URI syntax from its
	 * component parts. Each component is validated for syntax and some
	 * basic semantic checks are performed as well.  See the individual
	 * setter methods for specifics.
	 *
	 * @param p_scheme the URI scheme (cannot be null or empty)
	 * @param p_userinfo the URI userinfo (cannot be specified if host
	 *                   is null)
	 * @param p_host the hostname or IPv4 address for the URI
	 * @param p_port the URI port (may be -1 for "unspecified"; cannot
	 *               be specified if host is null)
	 * @param p_path the URI path - if the path contains '?' or '#',
	 *               then the query string and/or fragment will be
	 *               set from the path; however, if the query and
	 *               fragment are specified both in the path and as
	 *               separate parameters, an exception is thrown
	 * @param p_queryString the URI query string (cannot be specified
	 *                      if path is null)
	 * @param p_fragment the URI fragment (cannot be specified if path
	 *                   is null)
	 *
	 * @exception MalformedURIException if any of the parameters violates
	 *                                  syntax rules or semantic rules
	 */
	public URI(
		String p_scheme,
		String p_userinfo,
		String p_host,
		int p_port,
		String p_path,
		String p_queryString,
		String p_fragment)
		throws MalformedURIException {
		if (p_scheme == null || p_scheme.length() == 0) {
			throw new MalformedURIException("Scheme is required!");
		}

		if (p_host == null) {
			if (p_userinfo != null) {
				throw new MalformedURIException("Userinfo may not be specified if host is not specified!");
			}
			if (p_port != -1) {
				throw new MalformedURIException("Port may not be specified if host is not specified!");
			}
		}

		if (p_path != null) {
			if (p_path.indexOf('?') != -1 && p_queryString != null) {
				throw new MalformedURIException("Query string cannot be specified in path and query string!");
			}

			if (p_path.indexOf('#') != -1 && p_fragment != null) {
				throw new MalformedURIException("Fragment cannot be specified in both the path and fragment!");
			}
		}

		setScheme(p_scheme);
		setHost(p_host);
		setPort(p_port);
		m_port = "" + p_port;
		setUserinfo(p_userinfo);
		setPath(p_path);
		setQueryString(p_queryString);
		setFragment(p_fragment);
	}

	/**
	 * Initialize all fields of this URI from another URI.
	 *
	 * @param p_other the URI to copy (cannot be null)
	 */
	private void initialize(URI p_other) {
		m_scheme = p_other.getScheme();
		m_userinfo = p_other.getUserinfo();
		m_host = p_other.getHost();
		m_port = p_other.m_port;
		n_port = p_other.n_port;
		m_path = p_other.getPath();
		m_queryString = p_other.getQueryString();
		m_fragment = p_other.getFragment();
	}

	/**
	 * Initializes this URI from a base URI and a URI specification string.
	 * See RFC 2396 Section 4 and Appendix B for specifications on parsing
	 * the URI and Section 5 for specifications on resolving relative URIs
	 * and relative paths.
	 *
	 * @param p_base the base URI (may be null if p_uriSpec is an absolute
	 *               URI)
	 * @param p_uriSpec the URI spec string which may be an absolute or
	 *                  relative URI (can only be null/empty if p_base
	 *                  is not null)
	 *
	 * @exception MalformedURIException if p_base is null and p_uriSpec
	 *                                  is not an absolute URI or if
	 *                                  p_uriSpec violates syntax rules
	 */
	private void initialize(URI p_base, String p_uriSpec)
		throws MalformedURIException {
		if (p_base == null && (p_uriSpec == null || p_uriSpec.length() == 0)) {
			throw new RelativeURIException("Cannot initialize URI with empty parameters.");
		}

		// just make a copy of the base if spec is empty
		if (p_uriSpec == null || p_uriSpec.length() == 0) {
			initialize(p_base);
			return;
		}

		String uriSpec = p_uriSpec;
		int uriSpecLen = uriSpec.length();
		int index = 0;

		// Check for scheme, which must be before '/', '?' or '#'. Also handle
		// names with DOS drive letters ('D:'), so 1-character schemes are not
		// allowed.
		int colonIdx = uriSpec.indexOf(':');
		int slashIdx = uriSpec.indexOf('/');
		int queryIdx = uriSpec.indexOf('?');
		int fragmentIdx = uriSpec.indexOf('#');

		if ((colonIdx < 2)
			|| (colonIdx > slashIdx && slashIdx != -1)
			|| (colonIdx > queryIdx && queryIdx != -1)
			|| (colonIdx > fragmentIdx && fragmentIdx != -1)) {
			// We need to do the relative URI algorithm:

			// jjc: the spec says:
			// 'URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ]'
			// My understanding is that if there is only the fragment
			// then this is a relative URI.
			if (p_base == null // del jjc: && fragmentIdx != 0
			) {
				// Nothing to be relative against.
				throw new RelativeURIException("No scheme found in URI." + p_uriSpec );
			} else {
				if ((!p_base.isGenericURI()) && fragmentIdx != 0)
					// Can't be relative against opaque URI (except using the #frag).
					throw new MalformedURIException("Cannot apply relative URI to an opaque URI");
			}
		} else {
			initializeScheme(uriSpec);
			index = m_scheme.length() + 1;
		}

		// two slashes means generic URI syntax, so we get the authority
		if (((index + 1) < uriSpecLen)
			&& (uriSpec.substring(index).startsWith("//"))) {
			index += 2;
			int startPos = index;

			// get authority - everything up to path, query or fragment
			char testChar = '\0';
			while (index < uriSpecLen) {
				testChar = uriSpec.charAt(index);
				if (testChar == '/' || testChar == '?' || testChar == '#') {
					break;
				}
				index++;
			}

			// if we found authority, parse it out, otherwise we set the
			// host to empty string
			if (index > startPos) {
				initializeAuthority(uriSpec.substring(startPos, index));
			} else {
				m_host = "";
			}
		}

		initializePath(uriSpec.substring(index));

		// Resolve relative URI to base URI - see RFC 2396 Section 5.2
		// In some cases, it might make more sense to throw an exception
		// (when scheme is specified is the string spec and the base URI
		// is also specified, for example), but we're just following the
		// RFC specifications
		if (p_base != null) {

			// check to see if this is the current doc - RFC 2396 5.2 #2
			// note that this is slightly different from the RFC spec in that
			// we don't include the check for query string being null
			// - this handles cases where the urispec is just a query
			// string or a fragment (e.g. "?y" or "#s") -
			// see <http://www.ics.uci.edu/~fielding/url/test1.html> which
			// identified this as a bug in the RFC
			if (m_path.length() == 0 && m_scheme == null && m_host == null) {
				m_scheme = p_base.getScheme();
				m_userinfo = p_base.getUserinfo();
				m_host = p_base.getHost();
				m_port = p_base.m_port;
				n_port = p_base.getPort();
				m_path = p_base.getPath();

				if (m_queryString == null) {
					m_queryString = p_base.getQueryString();
				}
				return;
			}

			// check for scheme - RFC 2396 5.2 #3
			// if we found a scheme, it means absolute URI, so we're done
			if (m_scheme == null) {
				m_scheme = p_base.getScheme();
			} else {
				return;
			}

			// check for authority - RFC 2396 5.2 #4
			// if we found a host, then we've got a network path, so we're done
			if (m_host == null) {
				m_userinfo = p_base.getUserinfo();
				m_host = p_base.getHost();
				m_port = p_base.m_port;
				n_port = p_base.getPort();
			} else {
				return;
			}

			// check for absolute path - RFC 2396 5.2 #5
			if (m_path.length() > 0 && m_path.startsWith("/")) {
				return;
			}

			// if we get to this point, we need to resolve relative path
			// RFC 2396 5.2 #6
				String path = // jjc new String();
	"/"; // jjc ins
			String basePath = p_base.getPath();

			// 6a - get all but the last segment of the base URI path
			if (basePath != null) {
				int lastSlash = basePath.lastIndexOf('/');
				if (lastSlash != -1) {
					path = basePath.substring(0, lastSlash + 1);
				}
			}

			// 6b - append the relative URI path
			path = path.concat(m_path);

			// 6c - remove all "./" where "." is a complete path segment
			index = -1;
			while ((index = path.indexOf("/./")) != -1) {
				path =
					path.substring(0, index + 1).concat(
						path.substring(index + 3));
			}

			// 6d - remove "." if path ends with "." as a complete path segment
			if (path.endsWith("/.")) {
				path = path.substring(0, path.length() - 1);
			}

			// 6e - remove all "<segment>/../" where "<segment>" is a complete
			// path segment not equal to ".."
			index = 1;
			int segIndex = -1;

			while ((index = path.indexOf("/../", index)) > 0) {
				segIndex = path.lastIndexOf('/', index - 1);
				if (segIndex != -1
					&& !path.substring(segIndex + 1, index).equals("..")) {
					path =
						path.substring(0, segIndex).concat(
							path.substring(index + 3));
					index = segIndex;
				} else {
					index += 4;
				}
			}

			// 6f - remove ending "<segment>/.." where "<segment>" is a
			// complete path segment
			if (path.endsWith("/..")) {
				index = path.length() - 3;
				segIndex = path.lastIndexOf('/', index - 1);
				if (segIndex != -1
					&& !path.substring(segIndex + 1, index).equals("..")) {
					path = path.substring(0, segIndex + 1);
				}
			}

			m_path = path;
		}
	}

	/**
	 * Initialize the scheme for this URI from a URI string spec.
	 *
	 * @param p_uriSpec the URI specification (cannot be null)
	 *
	 * @exception MalformedURIException if URI does not have a conformant
	 *                                  scheme
	 */
	private void initializeScheme(String p_uriSpec)
		throws MalformedURIException {
		int uriSpecLen = p_uriSpec.length();
		int index = p_uriSpec.indexOf(':');

		if (index < 1)
			throw new MalformedURIException("No scheme found in URI.");

		if (index == uriSpecLen - 1)
			throw new MalformedURIException("A bare scheme name is not a URI.");

		setScheme(p_uriSpec.substring(0, index));
	}

	/**
	 * Initialize the authority (userinfo, host and port) for this
	 * URI from a URI string spec.
	 *
	 * @param p_uriSpec the URI specification (cannot be null)
	 *
	 * @exception MalformedURIException if p_uriSpec violates syntax rules
	 */
	private void initializeAuthority(String p_uriSpec)
		throws MalformedURIException {
		int index = 0;
		int start = 0;
		int end = p_uriSpec.length();
		char testChar = '\0';
		String userinfo = null;

		// userinfo is everything up @
		if (p_uriSpec.indexOf('@', start) != -1) {
			while (index < end) {
				testChar = p_uriSpec.charAt(index);
				if (testChar == '@') {
					break;
				}
				index++;
			}
			userinfo = p_uriSpec.substring(start, index);
			index++;
		}

		// host is everything up to ':'
		String host = null;
		start = index;
		while (index < end) {
			testChar = p_uriSpec.charAt(index);
			if (testChar == ':') {
				break;
			}
			index++;
		}
		host = p_uriSpec.substring(start, index);
		int port = -1;
		if (host.length() > 0) {
			// port
			if (testChar == ':') {
				index++;
				start = index;
				while (index < end) {
					index++;
				}
				String portStr = p_uriSpec.substring(start, index);
				if (portStr.length() > 0) {
					for (int i = 0; i < portStr.length(); i++) {
						if (!isDigit(portStr.charAt(i))) {
							throw new MalformedURIException(
								portStr
									+ " is invalid. Port should only contain digits!");
						}
					}
					try {
						port = Integer.parseInt(portStr);
						m_port = portStr;
					} catch (NumberFormatException nfe) {
						// can't happen
					}
				}
			}
		}
		setHost(host);
		setPort(port);
		setUserinfo(userinfo);
	}

	/**
	 * Initialize the path for this URI from a URI string spec.
	 *
	 * @param p_uriSpec the URI specification (cannot be null)
	 *
	 * @exception MalformedURIException if p_uriSpec violates syntax rules
	 */
	private void initializePath(String p_uriSpec)
		throws MalformedURIException {
		if (p_uriSpec == null) {
			throw new MalformedURIException("Cannot initialize path from null string!");
		}

		int index = 0;
		int start = 0;
		int end = p_uriSpec.length();
		char testChar = '\0';

		// path - everything up to query string or fragment
		while (index < end) {
			testChar = p_uriSpec.charAt(index);
			if (testChar == '?' || testChar == '#') {
				break;
			}
			// check for valid escape sequence
			if (testChar == '%') {
				if (index + 2 >= end
					|| !isHex(p_uriSpec.charAt(index + 1))
					|| !isHex(p_uriSpec.charAt(index + 2))) {
					throw new MalformedURIException("Path contains invalid escape sequence!");
				}
			} else if (
				!isReservedCharacter(testChar)
					&& !isUnreservedCharacter(testChar)) {
				throw new MalformedURIException(
					"Path contains invalid character: " + testChar);
			}
			index++;
		}
		m_path = p_uriSpec.substring(start, index);

		// query - starts with ? and up to fragment or end
		if (testChar == '?') {
			index++;
			start = index;
			while (index < end) {
				testChar = p_uriSpec.charAt(index);
				if (testChar == '#') {
					break;
				}
				if (testChar == '%') {
					if (index + 2 >= end
						|| !isHex(p_uriSpec.charAt(index + 1))
						|| !isHex(p_uriSpec.charAt(index + 2))) {
						throw new MalformedURIException("Query string contains invalid escape sequence!");
					}
				} else if (
					!isReservedCharacter(testChar)
						&& !isUnreservedCharacter(testChar)) {
					throw new MalformedURIException(
						"Query string contains invalid character:" + testChar);
				}
				index++;
			}
			m_queryString = p_uriSpec.substring(start, index);
		}

		// fragment - starts with #
		if (testChar == '#') {
			index++;
			start = index;
			while (index < end) {
				testChar = p_uriSpec.charAt(index);

				if (testChar == '%') {
					if (index + 2 >= end
						|| !isHex(p_uriSpec.charAt(index + 1))
						|| !isHex(p_uriSpec.charAt(index + 2))) {
						throw new MalformedURIException("Fragment contains invalid escape sequence!");
					}
				} else if (
					!isReservedCharacter(testChar)
						&& !isUnreservedCharacter(testChar)) {
					throw new MalformedURIException(
						"Fragment contains invalid character:" + testChar);
				}
				index++;
			}
			m_fragment = p_uriSpec.substring(start, index);
		}
	}

	/**
	 * Get the scheme for this URI.
	 *
	 * @return the scheme for this URI
	 */
	public String getScheme() {
		return m_scheme;
	}

	/**
	 * Get the scheme-specific part for this URI (everything following the
	 * scheme and the first colon). See RFC 2396 Section 5.2 for spec.
	 *
	 * @return the scheme-specific part for this URI
	 */
	public String getSchemeSpecificPart() {
		StringBuffer schemespec = new StringBuffer();

		if (m_userinfo != null || m_host != null || m_port != null) {
			schemespec.append("//");
		}

		if (m_userinfo != null) {
			schemespec.append(m_userinfo);
			schemespec.append('@');
		}

		if (m_host != null) {
			schemespec.append(m_host);
		}

		if (m_port != null) {
			schemespec.append(':');
			schemespec.append(m_port);
		}

		if (m_path != null) {
			schemespec.append((m_path));
		}

		if (m_queryString != null) {
			schemespec.append('?');
			schemespec.append(m_queryString);
		}

		if (m_fragment != null) {
			schemespec.append('#');
			schemespec.append(m_fragment);
		}

		return schemespec.toString();
	}

	/**
	 * Get the userinfo for this URI.
	 *
	 * @return the userinfo for this URI (null if not specified).
	 */
	public String getUserinfo() {
		return m_userinfo;
	}

	/**
	* Get the host for this URI.
	*
	* @return the host for this URI (null if not specified).
	*/
	public String getHost() {
		return m_host;
	}

	/**
	 * Get the port for this URI.
	 *
	 * @return the port for this URI (-1 if not specified).
	 */
	public int getPort() {
		return n_port;
	}

	/**
	 * Get the path for this URI (optionally with the query string and
	 * fragment).
	 *
	 * @param p_includeQueryString if true (and query string is not null),
	 *                             then a "?" followed by the query string
	 *                             will be appended
	 * @param p_includeFragment if true (and fragment is not null),
	 *                             then a "#" followed by the fragment
	 *                             will be appended
	 *
	 * @return the path for this URI possibly including the query string
	 *         and fragment
	 */
	public String getPath(
		boolean p_includeQueryString,
		boolean p_includeFragment) {
		StringBuffer pathString = new StringBuffer(m_path);

		if (p_includeQueryString && m_queryString != null) {
			pathString.append('?');
			pathString.append(m_queryString);
		}

		if (p_includeFragment && m_fragment != null) {
			pathString.append('#');
			pathString.append(m_fragment);
		}
		return pathString.toString();
	}

	/**
	 * Get the path for this URI. Note that the value returned is the path
	 * only and does not include the query string or fragment.
	 *
	 * @return the path for this URI.
	 */
	public String getPath() {
		return m_path;
	}

	/**
	 * Get the query string for this URI.
	 *
	 * @return the query string for this URI. Null is returned if there
	 *         was no "?" in the URI spec, empty string if there was a
	 *         "?" but no query string following it.
	 */
	public String getQueryString() {
		return m_queryString;
	}

	/**
	 * Get the fragment for this URI.
	 *
	 * @return the fragment for this URI. Null is returned if there
	 *         was no "#" in the URI spec, empty string if there was a
	 *         "#" but no fragment following it.
	 */
	public String getFragment() {
		return m_fragment;
	}

	/**
	 * Set the scheme for this URI. The scheme is converted to lowercase
	 * before it is set.
	 *
	 * @param p_scheme the scheme for this URI (cannot be null)
	 *
	 * @exception MalformedURIException if p_scheme is not a conformant
	 *                                  scheme name
	 */
	private void setScheme(String p_scheme) throws MalformedURIException {
		if (p_scheme == null) {
			throw new MalformedURIException("Cannot set scheme from null string!");
		}
		if (!isConformantSchemeName(p_scheme)) {
			throw new MalformedURIException("The scheme is not conformant.");
		}

		m_scheme = p_scheme; //.toLowerCase();
	}

	/**
	 * Set the userinfo for this URI. If a non-null value is passed in and
	 * the host value is null, then an exception is thrown.
	 *
	 * @param p_userinfo the userinfo for this URI
	 *
	 * @exception MalformedURIException if p_userinfo contains invalid
	 *                                  characters
	 */
	private void setUserinfo(String p_userinfo) throws MalformedURIException {
		if (p_userinfo == null) {
			m_userinfo = null;
		} else {
			if (m_host == null) {
				throw new MalformedURIException("Userinfo cannot be set when host is null!");
			}

			// userinfo can contain alphanumerics, mark characters, escaped
			// and ';',':','&','=','+','$',','
			int index = 0;
			int end = p_userinfo.length();
			char testChar = '\0';
			while (index < end) {
				testChar = p_userinfo.charAt(index);
				if (testChar == '%') {
					if (index + 2 >= end
						|| !isHex(p_userinfo.charAt(index + 1))
						|| !isHex(p_userinfo.charAt(index + 2))) {
						throw new MalformedURIException("Userinfo contains invalid escape sequence!");
					}
				} else if (
					!isUnreservedCharacter(testChar)
						&& USERINFO_CHARACTERS.indexOf(testChar) == -1) {
					throw new MalformedURIException(
						"Userinfo contains invalid character:" + testChar);
				}
				index++;
			}
		}
		m_userinfo = p_userinfo;
	}

	/**
	* Set the host for this URI. If null is passed in, the userinfo
	* field is also set to null and the port is set to -1.
	*
	* @param p_host the host for this URI
	*
	* @exception MalformedURIException if p_host is not a valid IP
	*                                  address or DNS hostname.
	*/
	private void setHost(String p_host) throws MalformedURIException {
		if (p_host == null || p_host.length() == 0) {
			m_host = p_host;
			m_userinfo = null;
			m_port = null;
			n_port = -1;
		} else if (!isWellFormedAddress(p_host)) {
			throw new MalformedURIException("Host is not a well formed address!");
		}
		m_host = p_host;
	}

	/**
	 * Set the port for this URI. -1 is used to indicate that the port is
	 * not specified, otherwise valid port numbers are  between 0 and 65535.
	 * If a valid port number is passed in and the host field is null,
	 * an exception is thrown.
	 *
	 * @param p_port the port number for this URI
	 *
	 * @exception MalformedURIException if p_port is not -1 and not a
	 *                                  valid port number
	 */
	private void setPort(int p_port) throws MalformedURIException {
		if (p_port >= 0 && p_port <= 65535) {
			if (m_host == null) {
				throw new MalformedURIException("Port cannot be set when host is null!");
			}
		} else if (p_port != -1) {
			throw new MalformedURIException("Invalid port number!");
		}
		n_port = p_port;

	}

	/**
	 * Set the path for this URI. If the supplied path is null, then the
	 * query string and fragment are set to null as well. If the supplied
	 * path includes a query string and/or fragment, these fields will be
	 * parsed and set as well. Note that, for URIs following the "generic
	 * URI" syntax, the path specified should start with a slash.
	 * For URIs that do not follow the generic URI syntax, this method
	 * sets the scheme-specific part.
	 *
	 * @param p_path the path for this URI (may be null)
	 *
	 * @exception MalformedURIException if p_path contains invalid
	 *                                  characters
	 */
	private void setPath(String p_path) throws MalformedURIException {
		if (p_path == null) {
			m_path = null;
			m_queryString = null;
			m_fragment = null;
		} else {
			initializePath(p_path);
		}
	}

	/**
	 * Append to the end of the path of this URI. If the current path does
	 * not end in a slash and the path to be appended does not begin with
	 * a slash, a slash will be appended to the current path before the
	 * new segment is added. Also, if the current path ends in a slash
	 * and the new segment begins with a slash, the extra slash will be
	 * removed before the new segment is appended.
	 *
	 * @param p_addToPath the new segment to be added to the current path
	 *
	 * @exception MalformedURIException if p_addToPath contains syntax
	 *                                  errors
	 */
	private void appendPath(String p_addToPath) throws MalformedURIException {
		if (p_addToPath == null || p_addToPath.length() == 0) {
			return;
		}

		if (!isURIString(p_addToPath)) {
			throw new MalformedURIException("Path contains invalid character!");
		}

		if (m_path == null || m_path.length() == 0) {
			if (p_addToPath.startsWith("/")) {
				m_path = p_addToPath;
			} else {
				m_path = "/" + p_addToPath;
			}
		} else if (m_path.endsWith("/")) {
			if (p_addToPath.startsWith("/")) {
				m_path = m_path.concat(p_addToPath.substring(1));
			} else {
				m_path = m_path.concat(p_addToPath);
			}
		} else {
			if (p_addToPath.startsWith("/")) {
				m_path = m_path.concat(p_addToPath);
			} else {
				m_path = m_path.concat("/" + p_addToPath);
			}
		}
	}

	/**
	 * Set the query string for this URI. A non-null value is valid only
	 * if this is an URI conforming to the generic URI syntax and
	 * the path value is not null.
	 *
	 * @param p_queryString the query string for this URI
	 *
	 * @exception MalformedURIException if p_queryString is not null and this
	 *                                  URI does not conform to the generic
	 *                                  URI syntax or if the path is null
	 */
	private void setQueryString(String p_queryString)
		throws MalformedURIException {
		if (p_queryString == null) {
			m_queryString = null;
		} else if (!isGenericURI()) {
			throw new MalformedURIException("Query string can only be set for a generic URI!");
		} else if (getPath() == null) {
			throw new MalformedURIException("Query string cannot be set when path is null!");
		} else if (!isURIString(p_queryString)) {
			throw new MalformedURIException("Query string contains invalid character!");
		} else {
			m_queryString = p_queryString;
		}
	}

	/**
	 * Set the fragment for this URI. A non-null value is valid only
	 * if this is a URI conforming to the generic URI syntax and
	 * the path value is not null.
	 *
	 * @param p_fragment the fragment for this URI
	 *
	 * @exception MalformedURIException if p_fragment is not null and this
	 *                                  URI does not conform to the generic
	 *                                  URI syntax or if the path is null
	 */
	public void setFragment(String p_fragment) throws MalformedURIException {
		if (p_fragment == null) {
			m_fragment = null;
		} else if (!isGenericURI()) {
			throw new MalformedURIException("Fragment can only be set for a generic URI!");
		} else if (getPath() == null) {
			throw new MalformedURIException("Fragment cannot be set when path is null!");
		} else if (!isURIString(p_fragment)) {
			throw new MalformedURIException("Fragment contains invalid character!");
		} else {
			m_fragment = p_fragment;
		}
	}

	/**
	 * Determines if the passed-in Object is equivalent to this URI.
	 *
	 * @param p_test the Object to test for equality.
	 *
	 * @return true if p_test is a URI with all values equal to this
	 *         URI, false otherwise
	 */
	public boolean equals(Object p_test) {
		if (p_test instanceof URI) {
			URI testURI = (URI) p_test;
			if (((m_scheme == null && testURI.m_scheme == null)
				|| (m_scheme != null
					&& testURI.m_scheme != null
					&& m_scheme.equals(testURI.m_scheme)))
				&& ((m_userinfo == null && testURI.m_userinfo == null)
					|| (m_userinfo != null
						&& testURI.m_userinfo != null
						&& m_userinfo.equals(testURI.m_userinfo)))
				&& ((m_host == null && testURI.m_host == null)
					|| (m_host != null
						&& testURI.m_host != null
						&& m_host.equals(testURI.m_host)))
				&& ((m_port == null && testURI.m_port == null)
					|| (m_port != null
						&& testURI.m_port != null
						&& m_port.equals(testURI.m_port)))
				&& ((m_path == null && testURI.m_path == null)
					|| (m_path != null
						&& testURI.m_path != null
						&& m_path.equals(testURI.m_path)))
				&& ((m_queryString == null && testURI.m_queryString == null)
					|| (m_queryString != null
						&& testURI.m_queryString != null
						&& m_queryString.equals(testURI.m_queryString)))
				&& ((m_fragment == null && testURI.m_fragment == null)
					|| (m_fragment != null
						&& testURI.m_fragment != null
						&& m_fragment.equals(testURI.m_fragment)))) {
				return true;
			}
		}
		return false;
	}

    /**
        produce a human-consumable string for the URI
    */
	public String toString()
        { return getURIString(); }

    /**
     * Get the URI as a string specification. See RFC 2396 Section 5.2.
     *
     * @return the URI string specification
     */
    public String getURIString() {
		StringBuffer uriSpecString = new StringBuffer();

		if (m_scheme != null) {
			uriSpecString.append(m_scheme);
			uriSpecString.append(':');
		}
		uriSpecString.append(getSchemeSpecificPart());
		return uriSpecString.toString();
	}

    public int hashCode() {
        return toString().hashCode();
    }

	/**
	 * Get the indicator as to whether this URI uses the "generic URI"
	 * syntax.
	 *
	 * @return true if this URI uses the "generic URI" syntax, false
	 *         otherwise
	 */
	public boolean isGenericURI() {
		// presence of the host (whether valid or empty) means
		// double-slashes which means generic uri
		return (m_host != null);
	}

	/**
	 * Determine whether a scheme conforms to the rules for a scheme name.
	 * A scheme is conformant if it starts with an alphanumeric, and
	 * contains only alphanumerics, '+','-' and '.'.
	 *
	 * @return true if the scheme is conformant, false otherwise
	 */
	public static boolean isConformantSchemeName(String p_scheme) {
		if (p_scheme == null || p_scheme.length() == 0) {
			return false;
		}

		if (!isAlpha(p_scheme.charAt(0))) {
			return false;
		}

		char testChar;
		for (int i = 1; i < p_scheme.length(); i++) {
			testChar = p_scheme.charAt(i);
			if (!isAlphanum(testChar)
				&& SCHEME_CHARACTERS.indexOf(testChar) == -1) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Determine whether a string is syntactically capable of representing
	 * a valid IPv4 address or the domain name of a network host. A valid
	 * IPv4 address consists of four decimal digit groups separated by a
	 * '.'. A hostname consists of domain labels (each of which must
	 * begin and end with an alphanumeric but may contain '-') separated
	 & by a '.'. See RFC 2396 Section 3.2.2.
	 *
	 * @return true if the string is a syntactically valid IPv4 address
	 *              or hostname
	 */
	public static boolean isWellFormedAddress(String p_address) {
		if (p_address == null) {
			return false;
		}

		String address = p_address;
		int addrLength = address.length();
		if (addrLength == 0 || addrLength > 255) {
			return false;
		}

		if (address.startsWith(".") || address.startsWith("-")) {
			return false;
		}

		// rightmost domain label starting with digit indicates IP address
		// since top level domain label can only start with an alpha
		// see RFC 2396 Section 3.2.2
		int index = address.lastIndexOf('.');
		if (address.endsWith(".")) {
			index = address.substring(0, index).lastIndexOf('.');
		}

		if (index + 1 < addrLength && isDigit(p_address.charAt(index + 1))) {
			char testChar;
			int numDots = 0;

			// make sure that 1) we see only digits and dot separators, 2) that
			// any dot separator is preceded and followed by a digit and
			// 3) that we find 3 dots
			for (int i = 0; i < addrLength; i++) {
				testChar = address.charAt(i);
				if (testChar == '.') {
					if (!isDigit(address.charAt(i - 1))
						|| (i + 1 < addrLength
							&& !isDigit(address.charAt(i + 1)))) {
						return false;
					}
					numDots++;
				} else if (!isDigit(testChar)) {
					return false;
				}
			}
			if (numDots != 3) {
				return false;
			}
		} else {
			// domain labels can contain alphanumerics and '-"
			// but must start and end with an alphanumeric
			char testChar;

			for (int i = 0; i < addrLength; i++) {
				testChar = address.charAt(i);
				if (testChar == '.') {
					if (!isAlphanum(address.charAt(i - 1))) {
						return false;
					}
					if (i + 1 < addrLength
						&& !isAlphanum(address.charAt(i + 1))) {
						return false;
					}
				} else if (!isAlphanum(testChar) && testChar != '-') {
					return false;
				}
			}
		}
		return true;
	}

	/**
	 * Determine whether a char is a digit.
	 *
	 * @return true if the char is betweeen '0' and '9', false otherwise
	 */
	private static boolean isDigit(char p_char) {
		return p_char >= '0' && p_char <= '9';
	}

	/**
	 * Determine whether a character is a hexadecimal character.
	 *
	 * @return true if the char is betweeen '0' and '9', 'a' and 'f'
	 *         or 'A' and 'F', false otherwise
	 */
	private static boolean isHex(char p_char) {
		return (
			isDigit(p_char)
				|| (p_char >= 'a' && p_char <= 'f')
				|| (p_char >= 'A' && p_char <= 'F'));
	}

	/**
	 * Determine whether a char is an alphabetic character: a-z or A-Z
	 *
	 * @return true if the char is alphabetic, false otherwise
	 */
	private static boolean isAlpha(char p_char) {
		return (
			(p_char >= 'a' && p_char <= 'z')
				|| (p_char >= 'A' && p_char <= 'Z'));
	}

	/**
	 * Determine whether a char is an alphanumeric: 0-9, a-z or A-Z
	 *
	 * @return true if the char is alphanumeric, false otherwise
	 */
	private static boolean isAlphanum(char p_char) {
		return (isAlpha(p_char) || isDigit(p_char));
	}

	/**
	 * Determine whether a character is a reserved character:
	 * ';', '/', '?', ':', '@', '&', '=', '+', '$' or ','
	 *
	 * @return true if the string contains any reserved characters
	 */
	private static boolean isReservedCharacter(char p_char) {
		return RESERVED_CHARACTERS.indexOf(p_char) != -1;
	}

	/**
	 * Determine whether a char is an unreserved character.
	 *
	 * @return true if the char is unreserved, false otherwise
	 */
	private static boolean isUnreservedCharacter(char p_char) {
		return (!isReservedCharacter(p_char)) && "#%[]".indexOf(p_char) == -1;
		//   return (isAlphanum(p_char) ||
		//           MARK_CHARACTERS.indexOf(p_char) != -1);
	}

	private boolean haveCheckedNFC = false;
	private boolean isNFC;

	public boolean isNormalFormC() {
		if (!haveCheckedNFC) {
			isNFC = CharacterModel.isNormalFormC(toString());
			haveCheckedNFC = true;
		}
		return isNFC;
	}
	/**
	 * Determine whether a given string contains only URI characters (also
	 * called "uric" in RFC 2396). uric consist of all reserved
	 * characters, unreserved characters and escaped characters.
	 *
	 * @return true if the string is comprised of uric, false otherwise
	 */
	private static boolean isURIString(String p_uric) {
		if (p_uric == null) {
			return false;
		}
		int end = p_uric.length();
		char testChar = '\0';
		for (int i = 0; i < end; i++) {
			testChar = p_uric.charAt(i);
			if (testChar == '%') {
				if (i + 2 >= end
					|| !isHex(p_uric.charAt(i + 1))
					|| !isHex(p_uric.charAt(i + 2))) {
					return false;
				} else {
					i += 2;
					continue;
				}
			}
			if (isReservedCharacter(testChar)
				|| isUnreservedCharacter(testChar)) {
				continue;
			} else {
				return false;
			}
		}
		return true;
	}

	/* RELATIVIZE */

	static final public int SAMEDOCUMENT = 1;
	static final public int NETWORK = 2;
	static final public int ABSOLUTE = 4;
	static final public int RELATIVE = 8;
	static final public int PARENT = 16;
	static final public int GRANDPARENT = 32;

	private boolean equal(String s1, String s2) {
		return s1 == null ? s2 == null : s1.equals(s2);
	}
    static private int prefs[][] = {
        { RELATIVE, RELATIVE|PARENT|GRANDPARENT },
        { PARENT, PARENT|GRANDPARENT },
        { GRANDPARENT, GRANDPARENT }
    };
    static String exact[] = { ".", "..", "../.." };
    static String sub[] = { "", "../", "../../" };
	public String relativize(String abs, int flags) throws MalformedURIException {
		URI r;
			r = new URI(abs);
       // logger.info("<"+Util.substituteStandardEntities(abs)+">");
       // logger.info("<"+Util.substituteStandardEntities(r.m_path)+">");
		if (r.isGenericURI()) {
			boolean net = equal(r.m_scheme, m_scheme);
			boolean absl =
				net
					&& equal(r.m_host, m_host)
					&& equal(m_userinfo, r.m_userinfo)
					&& equal(m_port, r.m_port);
			boolean same =
				absl
					&& equal(m_path, r.m_path)
					&& equal(m_queryString, r.m_queryString);

			String rslt = r.m_fragment == null ? "" : ("#" + r.m_fragment);

			if (same && (flags & SAMEDOCUMENT) != 0)
				return rslt;
			if (r.m_queryString != null) {
				rslt = "?" + r.m_queryString + rslt;
			}
            if ( absl ) {
                if ( m_subPaths == null ) {
                    m_subPaths = new String[]{
                        m_path==null?null:(m_path + "a"),
                         null, null, null };
                }
                if ( m_subPaths[0] != null )
                for (int i=0; i<3; i++) {
                    if ( (flags & prefs[i][1]) == 0 )
                        break;
                    if ( m_subPaths[i+1] == null )
                       m_subPaths[i+1] = getLastSlash(m_subPaths[i]);
                    if ( m_subPaths[i+1].length() == 0 )
                       break;
                    if ( (flags & prefs[i][0]) == 0 )
                       continue;
                    if ( !r.m_path.startsWith(m_subPaths[i+1]) )
                       continue;
                    // A relative path can be constructed.
                    int lg = m_subPaths[i+1].length();
                    if (lg == r.m_path.length()) {
                        return exact[i] + rslt;
                    }
                    rslt = sub[i] + r.m_path.substring(lg) + rslt;

              //      logger.info("<"+Util.substituteStandardEntities(rslt)+">["+i+"]");
                    return rslt;
                }
            }
            rslt = r.m_path + rslt;
            if ( absl && (flags & ABSOLUTE ) != 0 ) {
                return rslt;
            }
            if ( net && ( flags & NETWORK ) != 0 ) {
                return "//" +
                ( r.m_userinfo == null ? "" : ( r.m_userinfo + "@") ) +
                r.m_host +
                ( r.m_port == null ? "" : ( ":" + r.m_port) ) +
                rslt;

            }
		}
		return abs;
	}
    static private String getLastSlash(String s) {
        int ix = s.lastIndexOf('/',s.length()-2);
        return s.substring(0,ix+1);
    }
    static public String flags2str(int f) {
    StringBuffer oldValue = new StringBuffer(64);
    if ( (f&SAMEDOCUMENT)!=0 )
       oldValue.append( "same-document, " );
    if ( (f&NETWORK)!=0 )
       oldValue.append( "network, ");
    if ( (f&ABSOLUTE)!=0 )
       oldValue.append("absolute, ");
    if ( (f&RELATIVE)!=0 )
       oldValue.append("relative, ");
    if ((f&PARENT)!=0)
       oldValue.append("parent, ");
    if ((f&GRANDPARENT)!=0)
       oldValue.append("grandparent, ");
    if (oldValue.length() > 0)
       oldValue.setLength(oldValue.length()-2);
       return oldValue.toString();
    }
    public static int str2flags(String pv){
    StringTokenizer tkn = new StringTokenizer(pv,", ");
    int rslt = 0;
    while ( tkn.hasMoreElements() ) {
        String flag = tkn.nextToken();
        if ( flag.equals("same-document") )
           rslt |= SAMEDOCUMENT;
        else if ( flag.equals("network") )
           rslt |= NETWORK;
        else if ( flag.equals("absolute") )
           rslt |= ABSOLUTE;
        else if ( flag.equals("relative") )
           rslt |= RELATIVE;
        else if ( flag.equals("parent") )
           rslt |= PARENT;
        else if ( flag.equals("grandparent") )
           rslt |= GRANDPARENT;
        else

        logger.warn(
            "Incorrect property value for relativeURIs: " + flag
            );
    }
    return rslt;
    }
}


/**
 * Copyright 2013-2014 Guoqiang Chen, Shanghai, China. All rights reserved.
 *
 * Email: subchen@gmail.com
 * URL: http://subchen.github.io/
 *
 * 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 jetbrick.text.codec;

import java.io.UnsupportedEncodingException;
import java.util.Arrays;

/**
 * The fastest Base64 encoder/decoder implementations.
 * Base64 is defined in RFC 2045.
 *

 * Encoding supports two modes - with or without line separator.
 * When line separator flag is on, result will have lines with
 * max size of 76 chars, as per spec.
 *

 * When decoding, input must be valid, without illegal characters.
 * If input contains lines, they must be 76 chars long. Lines must
 * end with CRLF ("\r\n"), as per spec.
 */
public class Base64 {
    private static final char[] CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray();
    private static final int[] INV = new int[256];
    private static final String ENCODING = "UTF-8";

    static {
        Arrays.fill(INV, -1);
        for (int i = 0, iS = CHARS.length; i < iS; i++) {
            INV[CHARS[i]] = i;
        }
        INV['='] = 0;
    }

    // ---------------------------------------------------------------- char

    /**
     * Encodes a raw byte array into a BASE64 char[].
     * @param lineSeparator optional CRLF after 76 chars, unless EOF.
     */
    public static char[] encodeToChar(byte[] arr, boolean lineSeparator) {
        int len = arr != null ? arr.length : 0;
        if (len == 0) {
            return new char[0];
        }

        int evenlen = (len / 3) * 3;
        int cnt = ((len - 1) / 3 + 1) << 2;
        int destLen = cnt + (lineSeparator ? (cnt - 1) / 76 << 1 : 0);
        char[] dest = new char[destLen];

        for (int s = 0, d = 0, cc = 0; s < evenlen;) {
            int i = (arr[s++] & 0xff) << 16 | (arr[s++] & 0xff) << 8 | (arr[s++] & 0xff);

            dest[d++] = CHARS[(i >>> 18) & 0x3f];
            dest[d++] = CHARS[(i >>> 12) & 0x3f];
            dest[d++] = CHARS[(i >>> 6) & 0x3f];
            dest[d++] = CHARS[i & 0x3f];

            if (lineSeparator && (++cc == 19) && (d < (destLen - 2))) {
                dest[d++] = '\r';
                dest[d++] = '\n';
                cc = 0;
            }
        }

        int left = len - evenlen; // 0 - 2.
        if (left > 0) {
            int i = ((arr[evenlen] & 0xff) << 10) | (left == 2 ? ((arr[len - 1] & 0xff) << 2) : 0);

            dest[destLen - 4] = CHARS[i >> 12];
            dest[destLen - 3] = CHARS[(i >>> 6) & 0x3f];
            dest[destLen - 2] = left == 2 ? CHARS[i & 0x3f] : '=';
            dest[destLen - 1] = '=';
        }
        return dest;
    }

    /**
     * Decodes a BASE64 encoded char array.
     */
    public byte[] decode(char[] arr) {
        int length = arr.length;
        if (length == 0) {
            return new byte[0];
        }

        int sndx = 0, endx = length - 1;
        int pad = arr[endx] == '=' ? (arr[endx - 1] == '=' ? 2 : 1) : 0;
        int cnt = endx - sndx + 1;
        int sepCnt = length > 76 ? (arr[76] == '\r' ? cnt / 78 : 0) << 1 : 0;
        int len = ((cnt - sepCnt) * 6 >> 3) - pad;
        byte[] dest = new byte[len];

        int d = 0;
        for (int cc = 0, eLen = (len / 3) * 3; d < eLen;) {
            int i = INV[arr[sndx++]] << 18 | INV[arr[sndx++]] << 12 | INV[arr[sndx++]] << 6 | INV[arr[sndx++]];

            dest[d++] = (byte) (i >> 16);
            dest[d++] = (byte) (i >> 8);
            dest[d++] = (byte) i;

            if (sepCnt > 0 && ++cc == 19) {
                sndx += 2;
                cc = 0;
            }
        }

        if (d < len) {
            int i = 0;
            for (int j = 0; sndx <= endx - pad; j++) {
                i |= INV[arr[sndx++]] << (18 - j * 6);
            }
            for (int r = 16; d < len; r -= 8) {
                dest[d++] = (byte) (i >> r);
            }
        }

        return dest;
    }

    // ---------------------------------------------------------------- byte

    public static byte[] encodeToByte(String s) {
        try {
            return encodeToByte(s.getBytes(ENCODING), false);
        } catch (UnsupportedEncodingException ignore) {
            return null;
        }
    }

    public static byte[] encodeToByte(String s, boolean lineSep) {
        try {
            return encodeToByte(s.getBytes(ENCODING), lineSep);
        } catch (UnsupportedEncodingException ignore) {
            return null;
        }
    }

    public static byte[] encodeToByte(byte[] arr) {
        return encodeToByte(arr, false);
    }

    /**
     * Encodes a raw byte array into a BASE64 char[].
     * @param lineSep optional CRLF after 76 chars, unless EOF.
     */
    public static byte[] encodeToByte(byte[] arr, boolean lineSep) {
        int len = arr != null ? arr.length : 0;
        if (len == 0) {
            return new byte[0];
        }

        int evenlen = (len / 3) * 3;
        int cnt = ((len - 1) / 3 + 1) << 2;
        int destlen = cnt + (lineSep ? (cnt - 1) / 76 << 1 : 0);
        byte[] dest = new byte[destlen];

        for (int s = 0, d = 0, cc = 0; s < evenlen;) {
            int i = (arr[s++] & 0xff) << 16 | (arr[s++] & 0xff) << 8 | (arr[s++] & 0xff);

            dest[d++] = (byte) CHARS[(i >>> 18) & 0x3f];
            dest[d++] = (byte) CHARS[(i >>> 12) & 0x3f];
            dest[d++] = (byte) CHARS[(i >>> 6) & 0x3f];
            dest[d++] = (byte) CHARS[i & 0x3f];

            if (lineSep && ++cc == 19 && d < destlen - 2) {
                dest[d++] = '\r';
                dest[d++] = '\n';
                cc = 0;
            }
        }

        int left = len - evenlen;
        if (left > 0) {
            int i = ((arr[evenlen] & 0xff) << 10) | (left == 2 ? ((arr[len - 1] & 0xff) << 2) : 0);

            dest[destlen - 4] = (byte) CHARS[i >> 12];
            dest[destlen - 3] = (byte) CHARS[(i >>> 6) & 0x3f];
            dest[destlen - 2] = left == 2 ? (byte) CHARS[i & 0x3f] : (byte) '=';
            dest[destlen - 1] = '=';
        }
        return dest;
    }

    public static String decodeToString(byte[] arr) {
        try {
            return new String(decode(arr), ENCODING);
        } catch (UnsupportedEncodingException ignore) {
            return null;
        }
    }

    /**
     * Decodes BASE64 encoded byte array.
     */
    public static byte[] decode(byte[] arr) {
        int length = arr.length;
        if (length == 0) {
            return new byte[0];
        }

        int sndx = 0, endx = length - 1;
        int pad = arr[endx] == '=' ? (arr[endx - 1] == '=' ? 2 : 1) : 0;
        int cnt = endx - sndx + 1;
        int sepCnt = length > 76 ? (arr[76] == '\r' ? cnt / 78 : 0) << 1 : 0;
        int len = ((cnt - sepCnt) * 6 >> 3) - pad;
        byte[] dest = new byte[len];

        int d = 0;
        for (int cc = 0, eLen = (len / 3) * 3; d < eLen;) {
            int i = INV[arr[sndx++]] << 18 | INV[arr[sndx++]] << 12 | INV[arr[sndx++]] << 6 | INV[arr[sndx++]];

            dest[d++] = (byte) (i >> 16);
            dest[d++] = (byte) (i >> 8);
            dest[d++] = (byte) i;

            if (sepCnt > 0 && ++cc == 19) {
                sndx += 2;
                cc = 0;
            }
        }

        if (d < len) {
            int i = 0;
            for (int j = 0; sndx <= endx - pad; j++) {
                i |= INV[arr[sndx++]] << (18 - j * 6);
            }
            for (int r = 16; d < len; r -= 8) {
                dest[d++] = (byte) (i >> r);
            }
        }

        return dest;
    }

    // ---------------------------------------------------------------- string

    public static String encodeToString(String s) {
        try {
            return new String(encodeToChar(s.getBytes(ENCODING), false));
        } catch (UnsupportedEncodingException ignore) {
            return null;
        }
    }

    public static String encodeToString(String s, boolean lineSep) {
        try {
            return new String(encodeToChar(s.getBytes(ENCODING), lineSep));
        } catch (UnsupportedEncodingException ignore) {
            return null;
        }
    }

    public static String encodeToString(byte[] arr) {
        return new String(encodeToChar(arr, false));
    }

    /**
     * Encodes a raw byte array into a BASE64 String.
     */
    public static String encodeToString(byte[] arr, boolean lineSep) {
        return new String(encodeToChar(arr, lineSep));
    }

    public static String decodeToString(String s) {
        try {
            return new String(decode(s), ENCODING);
        } catch (UnsupportedEncodingException ignore) {
            return null;
        }
    }

    /**
     * Decodes a BASE64 encoded string.
     */
    public static byte[] decode(String s) {
        int length = s.length();
        if (length == 0) {
            return new byte[0];
        }

        int sndx = 0, endx = length - 1;
        int pad = s.charAt(endx) == '=' ? (s.charAt(endx - 1) == '=' ? 2 : 1) : 0;
        int cnt = endx - sndx + 1;
        int sepCnt = length > 76 ? (s.charAt(76) == '\r' ? cnt / 78 : 0) << 1 : 0;
        int len = ((cnt - sepCnt) * 6 >> 3) - pad;
        byte[] dest = new byte[len];

        int d = 0;
        for (int cc = 0, eLen = (len / 3) * 3; d < eLen;) {
            int i = INV[s.charAt(sndx++)] << 18 | INV[s.charAt(sndx++)] << 12 | INV[s.charAt(sndx++)] << 6 | INV[s.charAt(sndx++)];

            dest[d++] = (byte) (i >> 16);
            dest[d++] = (byte) (i >> 8);
            dest[d++] = (byte) i;

            if (sepCnt > 0 && ++cc == 19) {
                sndx += 2;
                cc = 0;
            }
        }

        if (d < len) {
            int i = 0;
            for (int j = 0; sndx <= endx - pad; j++) {
                i |= INV[s.charAt(sndx++)] << (18 - j * 6);
            }
            for (int r = 16; d < len; r -= 8) {
                dest[d++] = (byte) (i >> r);
            }
        }

        return dest;
    }
}


/**
 * Copyright 2013-2014 Guoqiang Chen, Shanghai, China. All rights reserved.
 *
 * Email: subchen@gmail.com
 * URL: http://subchen.github.io/
 *
 * 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 jetbrick.dao.orm.utils;

import java.sql.*;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jetbrick.beans.ClassUtils;
import jetbrick.collections.iterators.ArrayIterator;
import jetbrick.lang.StringUtils;
import jetbrick.reflect.BeanMap;

public class PreparedStatementCreator {
    private static final Pattern namedParameterPattern = Pattern.compile("\\:([a-zA-Z0-9_]+)");

    @SuppressWarnings("unchecked")
    public static PreparedStatement createPreparedStatement(Connection conn, String sql, Object... parameters) throws SQLException {
        if (parameters == null) {
            return createByIterator(conn, sql, null);
        }

        if (parameters.length == 1) {
            Object value = parameters[0];
            Class<?> clazz = value.getClass();
            if (ClassUtils.isAssignable(clazz, Map.class)) {
                return createByMap(conn, sql, (Map<String, ?>) value);
            } else if (ClassUtils.isAssignable(clazz, Collection.class)) {
                return createByIterator(conn, sql, new ArrayIterator(parameters));
            } else if (clazz.isPrimitive() || clazz.getName().startsWith("java.")) {
                return createByIterator(conn, sql, new ArrayIterator(parameters));
            } else {
                Map<String, Object> beanMap = new BeanMap(value);
                return createByMap(conn, sql, beanMap);
            }
        } else {
            return createByIterator(conn, sql, new ArrayIterator(parameters));
        }
    }

    /**
     * Support ? as parameter
     */
    protected static PreparedStatement createByIterator(Connection conn, String sql, Iterator<?> parameters) throws SQLException {
        PreparedStatement ps = conn.prepareStatement(sql);
        if (parameters != null) {
            int index = 1;
            while (parameters.hasNext()) {
                Object parameter = parameters.next();
                if (parameter == null) {
                    ps.setObject(index, null);
                } else {
                    ps.setObject(index, parameter);
                }
                index++;
            }
        }
        return ps;
    }

    /**
     * Support :name as parameter, and Array or Collection type
     */
    protected static PreparedStatement createByMap(Connection conn, String sql, Map<String, ?> parameters) throws SQLException {
        StringBuffer sb = new StringBuffer();
        List<Object> params = new ArrayList<Object>();

        Matcher m = namedParameterPattern.matcher(sql);
        while (m.find()) {
            String key = m.group(1);
            Object value = parameters.get(key);
            if (value == null) {
                params.add(null);
                m.appendReplacement(sb, "?");
            } else if (value instanceof Object[]) {
                Object[] array = (Object[]) value;
                if (array.length == 0) {
                    params.add(null);
                    m.appendReplacement(sb, "?");
                } else {
                    for (Object one : array) {
                        params.add(one);
                    }
                    m.appendReplacement(sb, StringUtils.repeat("?", ",", array.length));
                }
            } else if (value instanceof Collection) {
                Collection<?> collection = (Collection<?>) value;
                if (collection.size() == 0) {
                    params.add(null);
                    m.appendReplacement(sb, "?");
                } else {
                    for (Object one : collection) {
                        params.add(one);
                    }
                    m.appendReplacement(sb, StringUtils.repeat("?", ",", collection.size()));
                }
            } else {
                params.add(value);
                m.appendReplacement(sb, "?");
            }
        }
        m.appendTail(sb);

        return createByIterator(conn, sb.toString(), params.iterator());
    }

}


/*
 * MuffinManager.java
 *
 * Created on January 30, 2001, 12:14 AM

 * Copyright © 2012 jbundle.org. All rights reserved.
 */
package org.jbundle.util.muffinmanager;

import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.ActionEvent;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;

import javax.jnlp.BasicService;
import javax.jnlp.ClipboardService;
import javax.jnlp.FileContents;
import javax.jnlp.FileOpenService;
import javax.jnlp.PersistenceService;
import javax.jnlp.ServiceManager;
import javax.jnlp.UnavailableServiceException;
import javax.swing.Action;
import javax.swing.JComponent;
import javax.swing.text.JTextComponent;
import javax.swing.text.TextAction;

/**
 * MuffinManager - This code handles the muffins for the java web start program.
 * @author  Administrator
 * @version 1.0.0
 */
public class MuffinManager extends Object
{
    /**
     * The basic jnlp services.
     */
    protected BasicService m_bs = null;
    /**
     * The jnlp persistence service.
     */
    protected PersistenceService m_ps = null;
    /**
     * The codebase to prefix muffins.
     */
    protected String m_strCodeBase = null;
    /**
     * The default encoding for muffins.
     */
    public static final String ENCODING = "UTF-8";

    /**
     * Creates new MuffinManager.
     */
    public MuffinManager()
    {
        super();
    }
    /**
     * Creates new MuffinManager .
     * @param applet The parent object (ignored).
     */
    public MuffinManager(Object applet)
    {
        this();
        this.init(applet);
    }
    /**
     * Creates new MuffinManager.
     * @param applet The parent object (ignored).
     */
    public void init(Object applet)
    {
        try {
            m_ps = (PersistenceService)ServiceManager.lookup("javax.jnlp.PersistenceService");
            m_bs = (BasicService)ServiceManager.lookup("javax.jnlp.BasicService");
            m_strCodeBase = m_bs.getCodeBase().toString();
        } catch (UnavailableServiceException e) {
            m_ps = null;
            m_bs = null;
        }
    }
    /**
     * Is the muffin manager service available?
     * @return true if available.
     */
    public boolean isServiceAvailable()
    {
        return (m_ps != null);
    }
    /**
     * Get the current value for this muffin.
     * @param strParam The key for this muffin parameter.
     * @return The value for this muffin (null if none).
     */
    public String getMuffin(String strParam)
    {
        try   {
            URL url = new URL(m_strCodeBase + strParam);
            FileContents fc = m_ps.get(url);
            if (fc == null)
                return null;
            // read in the contents of a muffin
            byte[] buf = new byte[(int)fc.getLength()];
            InputStream is = fc.getInputStream();
            int pos = 0;
            while((pos = is.read(buf, pos, buf.length - pos)) > 0) {
                // just loop
            }
            is.close();
            String strValue = new String(buf, ENCODING);
            return strValue;
        } catch (Exception ex)  {
            // Return null for any exception
        }
        return null;
    }
    /**
     * Set the current value for this muffin.
     * @param strParam The key for this muffin parameter.
     * @param strValue The value for this muffin.
     */
    public void setMuffin(String strParam, String strValue)
    {
        FileContents fc = null;
        URL url = null;
        try   {
            url = new URL(m_strCodeBase + strParam);
        } catch (Exception ex)  {
            return;
        }
        try   {
            fc = m_ps.get(url);
            fc.getMaxLength(); // This will throw an exception if there is no muffin yet.
        } catch (Exception ex)  {
            fc = null;
        }
        try   {
            if (fc == null)
            {
                m_ps.create(url, 100);
                fc = m_ps.get(url);
            }          // don't append
            if (strValue != null)
            {
                OutputStream os = fc.getOutputStream(false);
                byte[] buf = strValue.getBytes(ENCODING);
                os.write(buf);
                os.close();
                m_ps.setTag(url, PersistenceService.DIRTY);
            }
            else
                m_ps.delete(url);
        } catch (Exception ex)  {
            ex.printStackTrace(); // Return null for any exception
        }
    }
    /**
     * Get the Java WebStart codebase.
     * @return The codebase.
     */
    public URL getCodeBase()
    {
        if (m_bs != null)
            return m_bs.getCodeBase();
        return null;
    }
    /**
     * Display this URL in a web browser.
     * @param url The URL to display.
     * @return True if successfully displayed.
     */
    public boolean showTheDocument(URL url)
    {
        if (m_bs != null)
            return m_bs.showDocument(url);
        return false;
    }
    private ClipboardService cs = null;

    public static final int CLIPBOARD_ENABLED = 1 << 0;
    public static final int CLIPBOARD_DISABLED = 1 << 1;

    protected int clipboardReadStatus = 0;
    protected int clipboardWriteStatus = 0;

    /**
     * Get data from the system clipboard.
     * @return
     */
    public Transferable getClipboardContents()
    {
        if ((clipboardReadStatus & CLIPBOARD_DISABLED) == CLIPBOARD_DISABLED)
            return null;    // Rejected it last time, don't ask again
        clipboardReadStatus = CLIPBOARD_DISABLED;
        if (cs == null)
        {
            try {
                cs = (ClipboardService)ServiceManager.lookup("javax.jnlp.ClipboardService");
            } catch (UnavailableServiceException e) {
                cs = null;
            }
        }

        if (cs != null) {
            // get the contents of the system clipboard and print them
            Transferable tr = cs.getContents();
            if (tr != null)
                clipboardReadStatus = CLIPBOARD_ENABLED;
            return tr;
        }
        return null;
    }
    /**
     * Set the global clipboard contents.
     * @param data
     */
    public boolean setClipboardContents(Transferable data)
    {
        if (data == null)
            return false;
        if ((clipboardWriteStatus & CLIPBOARD_DISABLED) == CLIPBOARD_DISABLED)
            return false;    // Rejected it last time, don't ask again
        clipboardWriteStatus = CLIPBOARD_ENABLED;
        if (cs == null)
        {
            try {
                cs = (ClipboardService)ServiceManager.lookup("javax.jnlp.ClipboardService");
            } catch (UnavailableServiceException e) {
                cs = null;
            }
        }

        if (cs != null)
        { // set the system clipboard contents to a string selection
            try {
                cs.setContents(data);
                clipboardWriteStatus = CLIPBOARD_ENABLED;
                return true;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return false;
    }

    /**
     * Replace the clipboard cut/cut/paste command with this command.
     * @param component
     * @param actionName
     * @return
     */
    public boolean replaceClipboardAction(JComponent component, String actionName)
    {
        Action action = (Action)component.getActionMap().get(actionName);
        if (action != null)
            component.getActionMap().put(actionName, new LinkedClipboardAction(actionName, action));
        return (action != null);

    }
    /**
     * Do cut/copy/paste from system clipboard, or if not possible, do it from local clipboard.
     * @author don
     *
     */
    class LinkedClipboardAction extends TextAction   // Action?
    {
        private static final long serialVersionUID = 1L;
        Action linkedAction;
        public LinkedClipboardAction(String name, Action linkedAction)
        {
            super(name);
            this.linkedAction = linkedAction;
        }
        @Override
        public void actionPerformed(ActionEvent e) {
            JTextComponent target = getTextComponent(e);
            String name = (String)this.getValue(Action.NAME);
            if (target != null) {
                {
                    if ("paste".equals(name))
                    {
                        Transferable contents = getClipboardContents();
                        if (contents != null) {
                            try {
                                String s = (String)contents.getTransferData(DataFlavor.stringFlavor);
                                if (s != null)
                                {
                                    target.replaceSelection(s);
                                    return;
                                }
                            } catch (UnsupportedFlavorException e1) {
                                e1.printStackTrace();
                            } catch (IOException e1) {
                                e1.printStackTrace();
                            }
                        }
                    }
                }

                linkedAction.actionPerformed(e);

                {
                    if (("cut".equals(name)) || ("copy".equals(name)))
                    {
                        String data = target.getSelectedText();
                        StringSelection ss = new StringSelection(data);
                        if (ss != null)
                            setClipboardContents(ss);
                    }
                }
            }
        }
    }

    FileOpenService fos = null;;
    /**
     * Open this file.
     * @return
     */
    public InputStream openFileStream(String pathHint, String[] extensions)
    {
        if (fos == null)
        {
            try {
                fos = (FileOpenService)ServiceManager.lookup("javax.jnlp.FileOpenService");
            } catch (UnavailableServiceException e) {
                fos = null;
            }
        }

        if (fos != null) {
            try {
                // ask user to select a file through this service
                FileContents fc = fos.openFileDialog(pathHint, extensions);
                return fc.getInputStream();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }

}


/**
 * ServiceManager.java
 *
 * Created on 26. 2. 2015, 22:59:16 by burgetr
 */
package org.fit.layout.api;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;

import org.fit.layout.gui.BrowserPlugin;

/**
 * This class provides static methods for managing the global services.
 *
 * @author burgetr
 */
public class ServiceManager
{
    private static List<BrowserPlugin> browserPlugins;
    private static Map<String, BoxTreeProvider> boxProviders;
    private static Map<String, AreaTreeProvider> areaProviders;
    private static Map<String, LogicalTreeProvider> logicalProviders;
    private static Map<String, AreaTreeOperator> operators;
    private static Map<String, PageStorage> pageStorages;

    /** All the parametrized services */
    private static Map<String, ParametrizedOperation> parametrizedServices;
    /** All the script objects (may coexist with other service types) */
    private static Map<String, ScriptObject> scriptObjects;

    static {
        scriptObjects = new HashMap<String, ScriptObject>();
        parametrizedServices = new HashMap<String, ParametrizedOperation>();
        //load services of standard types
        browserPlugins = loadBrowserPlugins();
        boxProviders = loadServicesByType(BoxTreeProvider.class);
        areaProviders = loadServicesByType(AreaTreeProvider.class);
        logicalProviders = loadServicesByType(LogicalTreeProvider.class);
        operators = loadServicesByType(AreaTreeOperator.class);
        pageStorages = loadServicesByType(PageStorage.class);
        //load the remaining script objects - this should be the last step
        loadScriptObjects();
    }

    /**
     * Discovers all the BrowserPlugin service implementations.
     * @return A list of all browser plugins.
     */
    public static List<BrowserPlugin> findBrowserPlugins()
    {
        return browserPlugins;
    }

    /**
     * Discovers all the BoxTreeProvider service implementations.
     * @return A map that assigns the service {@code id} to the appropriate implementation.
     */
    public static Map<String, BoxTreeProvider> findBoxTreeProviders()
    {
        return boxProviders;
    }

    /**
     * Discovers all the AreaTreeProvider service implementations.
     * @return A map that assigns the service {@code id} to the appropriate implementation.
     */
    public static Map<String, AreaTreeProvider> findAreaTreeProviders()
    {
        return areaProviders;
    }

    /**
     * Discovers all the LogicalTreeProvider service implementations.
     * @return A map that assigns the service {@code id} to the appropriate implementation.
     */
    public static Map<String, LogicalTreeProvider> findLogicalTreeProviders()
    {
        return logicalProviders;
    }

    /**
     * Discovers all the AreaTreeOperator service implementations.
     * @return A map that assigns the service {@code id} to the appropriate implementation.
     */
    public static Map<String, AreaTreeOperator> findAreaTreeOperators()
    {
        return operators;
    }

    /**
     * Discovers all the ScriptObject service implementations.
     * @return A map that assigns the service {@code id} to the appropriate implementation.
     */
    public static Map<String, ScriptObject> findScriptObjects()
    {
        return scriptObjects;
    }

    /**
     * Discovers all the PageStorage service implementations.
     * @return A map that assigns the service {@code id} to the appropriate implementation.
     */
    public static Map<String, PageStorage> findPageStorages()
    {
        return pageStorages;
    }

    /**
     * Discovers the registered services of the given class.
     * @param clazz the class of the required services
     * @return A map that maps the services to their identifiers
     */
    public static <T extends Service> Map<String, T> loadServicesByType(Class<T> clazz)
    {
        ServiceLoader<T> loader = ServiceLoader.load(clazz);
        Iterator<T> it = loader.iterator();
        Map<String, T> ret = new HashMap<String, T>();
        while (it.hasNext())
        {
            T op = it.next();
            ret.put(op.getId(), op);
            if (op instanceof ParametrizedOperation)
                addParametrizedService(op.getId(), (ParametrizedOperation) op);
            if (op instanceof ScriptObject)
                addScriptObject(((ScriptObject) op).getVarName(), (ScriptObject) op);
        }
        return ret;
    }

    private static List<BrowserPlugin> loadBrowserPlugins()
    {
        ServiceLoader<BrowserPlugin> loader = ServiceLoader.load(BrowserPlugin.class);
        Iterator<BrowserPlugin> it = loader.iterator();
        List<BrowserPlugin> ret = new ArrayList<BrowserPlugin>();
        while (it.hasNext())
        {
            BrowserPlugin plugin = it.next();
            ret.add(plugin);
            if (plugin instanceof ScriptObject)
                addScriptObject(((ScriptObject) plugin).getVarName(), (ScriptObject) plugin);
        }
        return ret;
    }

    private static Map<String, ScriptObject> loadScriptObjects()
    {
        ServiceLoader<ScriptObject> loader = ServiceLoader.load(ScriptObject.class);
        Iterator<ScriptObject> it = loader.iterator();
        while (it.hasNext())
        {
            ScriptObject op = it.next();
            addScriptObject(op.getVarName(), op);
        }
        return scriptObjects;
    }

    //=============================================================================================

    /**
     * Sets the operation parametres based on a map of values.
     * @param op The operation whose parametres should be set
     * @param params A map that assigns values to parameter names
     */
    public static void setServiceParams(ParametrizedOperation op, Map<String, Object> params)
    {
        if (params != null)
        {
            for (Map.Entry<String, Object> entry : params.entrySet())
            {
                op.setParam(entry.getKey(), entry.getValue());
            }
        }
    }

    /**
     * Obtains the values of all the parametres of the given operation.
     * @param op The operation whose parametres should be set
     * @return A map that assigns values to parameter names
     */
    public static Map<String, Object> getServiceParams(ParametrizedOperation op)
    {
        Map<String, Object> ret = new HashMap<String, Object>();
        for (Parameter param : op.getParams())
        {
            ret.put(param.getName(), op.getParam(param.getName()));
        }
        return ret;
    }

    /**
     * Finds a parametrized service based on its ID.
     * @param id the service ID.
     * @return the parametrized operation object or {@code null} when the service does not exist.
     */
    public static ParametrizedOperation findParmetrizedService(String id)
    {
        if (parametrizedServices == null)
            return null;
        else
            return parametrizedServices.get(id);
    }

    /**
     * Adds a new parametrized operation to the list of all parametrized operations.
     * @param id
     * @param op
     */
    private static void addParametrizedService(String id, ParametrizedOperation op)
    {
        parametrizedServices.put(id, op);
    }

    private static void addScriptObject(String id, ScriptObject op)
    {
        if (!scriptObjects.containsKey(id))
            scriptObjects.put(id, op);
    }

    /**
     * Finds a service in a collection of services based on its class.
     * @param services the collection of services to scan
     * @param clazz the required class of the service
     * @return the fisrt service in the collection that is instance of the given class or {@code null} when
     * no such servise is present in the collection.
     */
    public static <T> T findByClass(Collection<?> services, Class<T> clazz)
    {
        for (Object serv : services)
        {
            if (clazz.isInstance(serv))
                return clazz.cast(serv);
        }
        return null;
    }

}


package com.w3asel.cubesensors.api.v1;

import java.io.IOException;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.scribe.builder.ServiceBuilder;
import org.scribe.model.OAuthRequest;
import org.scribe.model.Response;
import org.scribe.model.SignatureType;
import org.scribe.model.Token;
import org.scribe.model.Verb;
import org.scribe.oauth.OAuthService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.w3asel.cubesensors.CubeSensorsProperties;
import com.w3asel.cubesensors.api.v1.json.ErrorResponse;
import com.w3asel.cubesensors.api.v1.json.JsonCurrentResponse;
import com.w3asel.cubesensors.api.v1.json.JsonDevice;
import com.w3asel.cubesensors.api.v1.json.JsonDevice.ExtraMapping;
import com.w3asel.cubesensors.api.v1.json.JsonDeviceResponse;
import com.w3asel.cubesensors.api.v1.json.JsonDevicesResponse;
import com.w3asel.cubesensors.api.v1.json.JsonSpanResponse;
import com.w3asel.cubesensors.api.v1.json.StateParser;
import com.w3asel.cubesensors.auth.CubeSensorsAuthApi;

/**
 * This class provides methods that map to each of the queries made available by the CubeSensors API.
 *
 * @author Joe
 */
public class CubeSensorsApiV1 {
	private static final Logger LOGGER = LoggerFactory.getLogger(CubeSensorsApiV1.class);

	private static final String RESOURCES_ROOT = "http://api.cubesensors.com/v1/";
	private static final String DEVICES_PATH = "devices/";
	private static final String MEDIA_TYPE_APPLICATION_JSON = "application/json";
	private static final String HTTP_HEADER_ACCEPT = "Accept";

	/** mapper for parsing json */
	private static final ObjectMapper MAPPER = new ObjectMapper();

	/** service to handle signing API queries */
	private static OAuthService service = new ServiceBuilder().provider(CubeSensorsAuthApi.class).apiKey(CubeSensorsProperties.getAppKey())
			.apiSecret(CubeSensorsProperties.getAppSecret()).signatureType(SignatureType.QueryString).build();

	/**
	 * Rebuilds the {@code service} object with debug turned on (defaulted to {@code System.out})
	 */
	public static void debug() {
		service = new ServiceBuilder().debug().provider(CubeSensorsAuthApi.class).apiKey(CubeSensorsProperties.getAppKey())
				.apiSecret(CubeSensorsProperties.getAppSecret()).signatureType(SignatureType.QueryString).build();
	}

	private final Token accessToken;

	/**
	 * @param accessToken
	 *            the access token to be used when querying the API
	 */
	public CubeSensorsApiV1(final Token accessToken) {
		this.accessToken = accessToken;
	}

	/**
	 * Parses the results of a query and handles any errors.
	 */
	private <T> T parseQuery(final String response, final Class<T> responseClass) {
		T queryResponse;
		try {
			/*
			 * Possible exceptions:
			 *
			 * IOException - if the underlying input source has problems during parsing
			 *
			 * JsonParseException - if parser has problems parsing content
			 *
			 * JsonMappingException - if the parser does not have any more content to map (note: Json "null" value is considered content; enf-of-stream not)
			 */
			queryResponse = MAPPER.readValue(response, responseClass);
		} catch (JsonParseException | JsonMappingException e) {
			try {
				final ErrorResponse error = MAPPER.readValue(response, ErrorResponse.class);

				LOGGER.error("Query returned an error: {}", error);
				return null;
			} catch (final IOException e1) {
				LOGGER.error("Failed to read error response.", e1);
			}
			LOGGER.error("Error reading response.", e);
			return null;
		} catch (final IOException e) {
			LOGGER.error("Error reading response.", e);
			return null;
		}

		return queryResponse;
	}

	/** Converts the {@link JsonDevice} object into a {@link Device} and logs any missing/extra fields to debug. */
	private Device extractDevice(final JsonDevice device) {
		final Map<ExtraMapping, String> extras = new EnumMap<>(ExtraMapping.class);
		final Set<String> keys = new HashSet<>(device.extra.keySet());
		for (final ExtraMapping extra : ExtraMapping.values()) {
			extras.put(extra, device.extra.get(extra.name()));

			if (keys.contains(extra.name())) {
				keys.remove(extra.name());
			} else {
				LOGGER.debug("\"extra\" missing key \"{}\": {}", extra.name(), device.extra.toString());
			}
		}
		for (final String key : keys) {
			LOGGER.debug("Unexpected key in \"extra\": {}", key);
		}

		return new Device(device, extras);
	}

	/**
	 * @return the list of accessible devices
	 * @throws CubeSensorsException
	 *             when a request fails
	 **/
	public List<Device> getDevices() {
		final String queryUrl = RESOURCES_ROOT + DEVICES_PATH;
		LOGGER.trace("Querying: {}", queryUrl);

		final OAuthRequest request = new OAuthRequest(Verb.GET, queryUrl);
		request.getHeaders().put(HTTP_HEADER_ACCEPT, MEDIA_TYPE_APPLICATION_JSON);
		service.signRequest(accessToken, request);
		final Response response = request.send();
		LOGGER.trace("Response: {}", response.getBody());

		if (!response.isSuccessful()) {
			throw new CubeSensorsException(response.getBody());
		}

		final JsonDevicesResponse queryResponse = parseQuery(response.getBody(), JsonDevicesResponse.class);
		if (queryResponse == null) {
			return new ArrayList<>();
		}

		LOGGER.debug("Retrieved {} devices.", queryResponse.devices.size());

		final List<Device> devices = new ArrayList<Device>();
		for (final JsonDevice device : queryResponse.devices) {
			devices.add(extractDevice(device));
		}

		return devices;
	}

	/**
	 * @param uid
	 *            the cube id to query for
	 * @return the description of a device
	 * @throws CubeSensorsException
	 *             when a request fails
	 */
	public Device getDevice(final String uid) {
		final String queryUrl = RESOURCES_ROOT + DEVICES_PATH + uid;
		LOGGER.trace("Querying: {}", queryUrl);

		final OAuthRequest request = new OAuthRequest(Verb.GET, queryUrl);
		request.getHeaders().put(HTTP_HEADER_ACCEPT, MEDIA_TYPE_APPLICATION_JSON);
		service.signRequest(accessToken, request);
		final Response response = request.send();
		LOGGER.trace("Response: {}", response.getBody());

		if (!response.isSuccessful()) {
			throw new CubeSensorsException(response.getBody());
		}

		final JsonDeviceResponse queryResponse = parseQuery(response.getBody(), JsonDeviceResponse.class);
		if (queryResponse == null) {
			return null;
		}

		LOGGER.debug("Retrieved device {}.", queryResponse.device.uid);

		return extractDevice(queryResponse.device);
	}

	/**
	 * @param uid
	 *            the cube id to query for
	 * @return the current state of the cube
	 * @throws CubeSensorsException
	 *             when a request fails
	 */
	public State getCurrent(final String uid) {
		final String queryUrl = RESOURCES_ROOT + DEVICES_PATH + uid + "/current";
		LOGGER.trace("Querying: {}", queryUrl);

		final OAuthRequest request = new OAuthRequest(Verb.GET, queryUrl);
		request.getHeaders().put(HTTP_HEADER_ACCEPT, MEDIA_TYPE_APPLICATION_JSON);
		service.signRequest(accessToken, request);
		final Response response = request.send();
		LOGGER.trace("Response: {}", response.getBody());

		if (!response.isSuccessful()) {
			throw new CubeSensorsException(response.getBody());
		}

		final JsonCurrentResponse queryResponse = parseQuery(response.getBody(), JsonCurrentResponse.class);
		if (queryResponse == null) {
			return null;
		}

		final List<State> states = StateParser.parseState(queryResponse.fieldList, queryResponse.results);
		// can happen if the cube hasn't reported in recently enough
		if (states.isEmpty()) {
			return null;
		}
		return states.get(0);
	}

	/**
	 * Queries for a list of states over the time span specified. Leaving a field {@code null} will default to the API defaults.
	 *
	 * @param uid
	 *            the UID of the device to request data for
	 * @param start
	 *            the query start time
	 * @param end
	 *            the query end time
	 * @param resolution
	 *            the resolution in minutes
	 * @return a list of all states returned by the API
	 * @throws CubeSensorsException
	 *             when a request fails
	 */
	public List<State> getSpan(final String uid, final ZonedDateTime start, final ZonedDateTime end, final Integer resolution) {
		final String queryUrl = RESOURCES_ROOT + DEVICES_PATH + uid + "/span";
		LOGGER.trace("Querying: {}", queryUrl);

		final OAuthRequest request = new OAuthRequest(Verb.GET, queryUrl);
		request.getHeaders().put(HTTP_HEADER_ACCEPT, MEDIA_TYPE_APPLICATION_JSON);

		if (start != null) {
			final ZonedDateTime startUtc = start.withZoneSameInstant(ZoneId.of("Z")).truncatedTo(ChronoUnit.SECONDS);
			request.addQuerystringParameter("start", startUtc.toString());
			LOGGER.trace("Adding querystring parameter {}={}", "start", startUtc);
		}
		if (end != null) {
			final ZonedDateTime endUtc = end.withZoneSameInstant(ZoneId.of("Z")).truncatedTo(ChronoUnit.SECONDS);
			request.addQuerystringParameter("end", endUtc.toString());
			LOGGER.trace("Adding querystring parameter {}={}", "end", endUtc);
		}
		if (resolution != null) {
			request.addQuerystringParameter("resolution", resolution.toString());
			LOGGER.trace("Adding querystring parameter {}={}", "resolution", resolution);
		}

		service.signRequest(accessToken, request);
		final Response response = request.send();
		LOGGER.trace("Response: {}", response.getBody());

		if (!response.isSuccessful()) {
			throw new CubeSensorsException(response.getBody());
		}

		final JsonSpanResponse queryResponse = parseQuery(response.getBody(), JsonSpanResponse.class);
		if (queryResponse == null) {
			return new ArrayList<>();
		}

		final List<State> states = StateParser.parseState(queryResponse.fieldList, queryResponse.results);

		LOGGER.debug("Retrieved {} states.", states.size());

		return states;
	}
}