/*******************************************************************************
 * Copyright (c) 2005 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.bpel.ui.util;

import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;

/**
 * Utility class for calculating default namespaces based on namespace templates from the preference store. 
 */
public class NamespaceUtils
{
    protected static char NS_SEPARATOR = '/'; 
    protected static char PACKAGE_SEPARATOR = '.'; 
    protected static String HEX_PREFIX = "%"; //$NON-NLS-1$
    protected static String CHARSET_UTF8 = "UTF-8"; //$NON-NLS-1$
    
    // unsafe characters
    public static final String SPACE = " "; //$NON-NLS-1$
    public static final String DOUBLE_QUOTE = "\""; //$NON-NLS-1$
    public static final String LESS_THAN = "<"; //$NON-NLS-1$
    public static final String GREATER_THAN = ">"; //$NON-NLS-1$
    public static final String POUND = "#"; //$NON-NLS-1$
    public static final String PERCENT = "%"; //$NON-NLS-1$
    public static final String LEFT_CURLY = "{"; //$NON-NLS-1$
    public static final String RIGHT_CURLY = "}"; //$NON-NLS-1$
    public static final String PIPE = "|"; //$NON-NLS-1$
    public static final String BACKSLASH = "\\"; //$NON-NLS-1$
    public static final String CARET = "^"; //$NON-NLS-1$
    public static final String TILDE = "~"; //$NON-NLS-1$
    public static final String LEFT_SQUARE = "["; //$NON-NLS-1$
    public static final String RIGHT_SQUARE = "]"; //$NON-NLS-1$
    public static final String GRAVE = "`"; //$NON-NLS-1$
    
    // escaped characters
    public static final String ESCAPED_SPACE = "%20"; //$NON-NLS-1$
    public static final String ESCAPED_DOUBLE_QUOTE = "%22"; //$NON-NLS-1$
    public static final String ESCAPED_LESS_THAN = "%3C"; //$NON-NLS-1$
    public static final String ESCAPED_GREATER_THAN = "%3E"; //$NON-NLS-1$
    public static final String ESCAPED_POUND = "%23"; //$NON-NLS-1$
    public static final String ESCAPED_PERCENT = "%25"; //$NON-NLS-1$
    public static final String ESCAPED_LEFT_CURLY = "%7B"; //$NON-NLS-1$
    public static final String ESCAPED_RIGHT_CURLY = "%7D"; //$NON-NLS-1$
    public static final String ESCAPED_PIPE = "%7C"; //$NON-NLS-1$
    public static final String ESCAPED_BACKSLASH = "%5C"; //$NON-NLS-1$
    public static final String ESCAPED_CARET = "%5E"; //$NON-NLS-1$
    public static final String ESCAPED_TILDE = "%7E"; //$NON-NLS-1$
    public static final String ESCAPED_LEFT_SQUARE = "%5B"; //$NON-NLS-1$
    public static final String ESCAPED_RIGHT_SQUARE = "%5D"; //$NON-NLS-1$
    public static final String ESCAPED_GRAVE = "%60"; //$NON-NLS-1$
    
    protected static HashMap fCharToEscaped = new HashMap(15);
    static
    {
        fCharToEscaped.put(SPACE, ESCAPED_SPACE);
        fCharToEscaped.put(DOUBLE_QUOTE, ESCAPED_DOUBLE_QUOTE);
        fCharToEscaped.put(LESS_THAN, ESCAPED_LESS_THAN);
        fCharToEscaped.put(GREATER_THAN, ESCAPED_GREATER_THAN);
        fCharToEscaped.put(POUND, ESCAPED_POUND);
        fCharToEscaped.put(PERCENT, ESCAPED_PERCENT);
        fCharToEscaped.put(LEFT_CURLY, ESCAPED_LEFT_CURLY);
        fCharToEscaped.put(RIGHT_CURLY, ESCAPED_RIGHT_CURLY);
        fCharToEscaped.put(PIPE, ESCAPED_PIPE);
        fCharToEscaped.put(BACKSLASH, ESCAPED_BACKSLASH);
        fCharToEscaped.put(CARET, ESCAPED_CARET);
        fCharToEscaped.put(TILDE, ESCAPED_TILDE);
        fCharToEscaped.put(LEFT_SQUARE, ESCAPED_LEFT_SQUARE);
        fCharToEscaped.put(RIGHT_SQUARE, ESCAPED_RIGHT_SQUARE);
        fCharToEscaped.put(GRAVE, ESCAPED_GRAVE);
    }
        
    /**
     * Converts a UTF-8 escaped string back into a unicode string.  This method undoes the
     * operation performed in convertNamespaceToUri.
     * 
     * @param uri
     * @return - a unicode representation of the given UTF-8 escaped string.
     * @see convertNamespaceToUri
     */
    public static String convertUriToNamespace(String uri)
    {
        if (uri==null || uri.length()<1)
            return null;
        
        String namespace = uri;
        
        int prevIndex = 0; // index of the previously found %, plus 1
        int contiguousIndex = 0; // index of next contiguous hex number
        Byte hexByte = null;
        ArrayList contiguousBytes = null;
        String hexChunk = null;
        
        for (int i=uri.indexOf(HEX_PREFIX); i>=0; i=uri.indexOf(HEX_PREFIX, prevIndex))
        {
            // get hex byte
            hexByte = getHexByte(uri, i);
            contiguousBytes = new ArrayList();
            contiguousIndex = i;
            hexChunk = new String();
            
            prevIndex = contiguousIndex+1; // +1 for %
            
            // get contiguous bytes
            while (hexByte!=null)
            {
                contiguousBytes.add(hexByte);
                
                hexChunk += uri.substring(contiguousIndex, contiguousIndex+3);
                prevIndex = contiguousIndex+1; // +1 for %
                                
                hexByte = getHexByte(uri, contiguousIndex+3); // 2 for current hex num, 1 for %
                
                contiguousIndex += 3;
            }
            prevIndex = prevIndex>=uri.length()?uri.length()-1:prevIndex;
            
            if (contiguousBytes.size()<=0)
                continue;
            
            // convert hex string to bytes
            byte[] cbytes = new byte[contiguousBytes.size()];
            for (int j=0; j<cbytes.length; j++)
                cbytes[j] = ((Byte)contiguousBytes.get(j)).byteValue();
            
            try
            {
                // convert bytes to unicode
                String unicode = new String(cbytes, CHARSET_UTF8);
                
                // now find the hex string in the namespace and replace it with the proper unicode
                int beginIndex = namespace.indexOf(hexChunk);
                int endIndex = beginIndex+hexChunk.length();
                int lastIndex = namespace.length();
                endIndex = endIndex>lastIndex?lastIndex:endIndex;
                
                namespace = namespace.substring(0, beginIndex) + unicode + namespace.substring(endIndex);
            }
            catch(UnsupportedEncodingException e)
            {
                // this should never happen -- log anyway
            	// JM: Commented out:
                //ModelUtilPlugin.logWarning(e, e.getMessage());
            }
        }   
        
        return namespace;
    }
    
    /**
     * Returns the Byte value of the hex string at position index in string uri.
     * 
     * @param uri
     * @param index
     * @return null if characters at position index aren't of the form %HH
     */
    protected static Byte getHexByte(String uri, int index)
    {
        if (uri==null || uri.length()<1)
            return null;
        
        if (index>=uri.length())
            return null;
        
        // first character must be %
        if (!HEX_PREFIX.equals(String.valueOf(uri.charAt(index))))
            return null;
        
        // hex string is two digits
        int lastIndex = uri.length();
        int endIndex = index+3;
        
        // ensure endIndex doesn't extend the length of the string
        if (endIndex>lastIndex)
            endIndex = lastIndex;
        
        String hexString = uri.substring(index+1, endIndex);
        byte hexByte = 0;
        
        try
        {
            hexByte = Integer.valueOf(hexString, 16).byteValue();
        }
        catch (NumberFormatException e)
        {
            // not a hex number, return
            return null;
        }
        
        return new Byte(hexByte);
    }

    /**
     * Convert a string intended to be a target namespace, i.e. any string typed with xsd:anyURI.  According to the
     * XSD spec, any string typed with xsd:anyURI cannot contain any characters outside the US-ASCII character set.
     * These characters must be escaped in the UTF-8 hex notation, %HH.  This method also escapes "unwise" characters
     * by calling escapeUnsafeCharacters first.
     * 
     * Note that models saving to an XMLResource don't need this method since the XMLResource saves/loads using a
     * Xerces parser which automatically does this.  Models using the SSE parser for saving/loading need this as the
     * SSE parser doesn't do this automatically.
     * 
     * @param namespace
     * @return - UTF-8 character escaped string of any characters outside the US-ASCII character set.
     * @see convertUriToNamespace
     * @see escapeUnsafeCharacters
     */
    public static String convertNamespaceToUri(String namespace)
    {
        return convertNamespaceToUri(namespace, true);
    }
    
    /**
     * Convert a string intended to be a target namespace, i.e. any string typed with xsd:anyURI.  According to the
     * XSD spec, any string typed with xsd:anyURI cannot contain any characters outside the US-ASCII character set.
     * These characters must be escaped in the UTF-8 hex notation, %HH.  This method also escapes "unwise" characters
     * (except for '%' if escapePercent is set to false) by calling escapeUnsafeCharacters first.  Setting escapePercent
     * to false means you can call this method on previously converted strings without the '%' from '%HH' being converted
     * again.
     * 
     * Note that models saving to an XMLResource don't need this method since the XMLResource saves/loads using a
     * Xerces parser which automatically does this.  Models using the SSE parser for saving/loading need this as the
     * SSE parser doesn't do this automatically.
     * 
     * @param namespace
     * @param escapePercent - escapes '%' to it's hex value if true, ignores it otherwise.
     * @return - UTF-8 character escaped string of any characters outside the US-ASCII character set.
     * @see convertUriToNamespace
     * @see escapeUnsafeCharacters
     */
    public static String convertNamespaceToUri(String namespace, boolean escapePercent)
    {
        if (namespace==null || namespace.length()<1)
            return null;
        
        StringBuffer buffer = new StringBuffer();
        
        // first escape "unsafe" characters 
        namespace = escapeUnsafeCharacters(namespace, escapePercent);
        
        char[] nsChars = namespace.toCharArray();
        byte[] utf8 = null;
        
        try
        {
	        for (int i=0; i<nsChars.length; i++)
	        {
	            // Convert all characters outside the US-ASCII range (>127) to
	            // UTF-8 bytes as dictated by the anyURI definition in the
	            // XSD spec.
	            if (nsChars[i]>127)
	            {
	                utf8 = String.valueOf(nsChars[i]).getBytes(CHARSET_UTF8);
	                for (int j=0; j<utf8.length; j++)
	                {
	                    buffer.append(HEX_PREFIX);
	                    
	                    // 2-digit hex value
	                    buffer.append(Integer.toString(( utf8[j] & 0xff ), 16).toUpperCase());
	                }
	            }
	            else
	            {
	                buffer.append(nsChars[i]);
	            }
	        }
        }
        catch (UnsupportedEncodingException e)
        {
            // this should never happen -- log anyway
        	// JM commented out:
            //ModelUtilPlugin.logWarning(e, e.getMessage());
        }
        
        return buffer.toString();
    }
    
    /**
     * Escapes the "delim" and "unwise" characters as specified by rfc2396.  Also escapes the tilde (~) as this
     * also seems to cause problems with the XSD validator.  The characters are escaped using the UTF-8 hex 
     * notation, %HH.
     * 
     * To do undo this operation, call convertUriToNamespace
     * 
     * @param anyURI
     * @return
     * @see this is equivalent to escapeUnsafeCharacters(anyURI, true)
     */
    public static String escapeUnsafeCharacters(String anyURI)
    {
        return escapeUnsafeCharacters(anyURI, true);
    }

    /**
     * Escapes the "delim" and "unwise" characters as specified by rfc2396.  Also escapes the tilde (~) as this
     * also seems to cause problems with the XSD validator.  The characters are escaped using the UTF-8 hex 
     * notation, %HH.
     * 
     * To do undo this operation, call convertUriToNamespace
     * 
     * @param anyURI
     * @param escapePercent - escapes '%' to it's hex value if true, ignores it otherwise.
     * @return
     */
    public static String escapeUnsafeCharacters(String anyURI, boolean escapePercent)
    {
        if (anyURI==null)
            return null;

        // must escape % first since our escapes have % in them.
        if (escapePercent)
            anyURI = anyURI.replaceAll("\\"+PERCENT, (String)fCharToEscaped.get(PERCENT)); //$NON-NLS-1$
        
        String key = null;
        
        // escape all other characters except %
        for (Iterator i = fCharToEscaped.keySet().iterator(); i.hasNext();)
        {
            key = (String)i.next();
            if (key.equals(PERCENT))
                continue;
            
            anyURI = anyURI.replaceAll("\\"+key, (String)fCharToEscaped.get(key)); //$NON-NLS-1$
        }

        return anyURI;
    }

    /**
     * Returns the "delim" and "unwise" characters as specified by rfc2396 if found.  Also returns the tilde (~) as this
     * also seems to cause problems with the XSD validator.  Returns null if no unsafe characters found. 
     *
     * @param anyURI
     * @param spaceUnsafe - if true, treats a space as an "unwise" character.
     * @return null if no unsafe characters found,
     * 		   the unsafe character string otherwise
     */
    public static String containsUnsafeCharacter(String anyURI, boolean spaceUnsafe)
    {
        if (anyURI==null)
            return null;
        
        String key = null;
        
        for (Iterator i = fCharToEscaped.keySet().iterator(); i.hasNext();)
        {
            key = (String)i.next();
            
            if (!spaceUnsafe && SPACE.equals(key))
                continue;
            
            if (anyURI.indexOf(key)>=0)
                return key;
        }
        
        // no unsafe characters
        return null;
    }
}
