/*
 * Copyright 2015 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 *
 *      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 org.eclipse.webdav.internal.kernel;

import java.util.Enumeration;
import java.util.Hashtable;
import org.eclipse.webdav.IContext;
import org.eclipse.webdav.internal.kernel.utils.EnumerationConverter;
import org.eclipse.webdav.internal.kernel.utils.MergedEnumeration;

/**
 * The Context class is essentially a collection of key-value
 * pairings, with defaults.  Implements the Context interface
 * which provides convenience methods for most HTTP and WebDAV message
 * header fields.
 *
 * To be done:
 *
 * 1) Types that take and return dates have not been changed.  We need to write
 * date formats to cope with the different strings that we may be sent, see
 *the HTTP spec for details.
 * 2) Identify the fields where multiple values are permissible, and change the
 *params to be arrays of values.
 */
public class Context implements IContext {

    protected Hashtable properties = new Hashtable(5);
    protected IContext defaults = null;

    protected final class ContextKey {

        protected String key;

        public ContextKey(String key) {
            super();
            this.key = key;
        }

        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (!(obj instanceof ContextKey))
                return false;
            return ((ContextKey) obj).key.equalsIgnoreCase(key);
        }

        public int hashCode() {
            return key.toLowerCase().hashCode();
        }

        public String toString() {
            return key;
        }
    }

    public final class ContextKeyToStringEnum extends EnumerationConverter {
        ContextKeyToStringEnum(Enumeration sourceEnum) {
            super(sourceEnum);
        }

        public Object nextElement() {
            return ((ContextKey) sourceEnum.nextElement()).key;
        }
    }

    // Field names used in HTTP message headers.

    // from HTTP/1.1
    public final String ACCEPT = "Accept"; //$NON-NLS-1$
    public final String ACCEPT_CHARSET = "Accept-Charset"; //$NON-NLS-1$
    public final String ACCEPT_ENCODING = "Accept-Encoding"; //$NON-NLS-1$
    public final String ACCEPT_LANGUAGE = "Accept-Language"; //$NON-NLS-1$
    public final String ACCEPT_RANGES = "Accept-Ranges"; //$NON-NLS-1$
    public final String AGE = "Age"; //$NON-NLS-1$
    public final String ALLOW = "Allow"; //$NON-NLS-1$
    public final String AUTHORIZATION = "Authorization"; //$NON-NLS-1$
    public final String CACHE_CONTROL = "Cache-Control"; //$NON-NLS-1$
    public final String CONNECTION = "Connection"; //$NON-NLS-1$
    public final String CONTENT_BASE = "Content-Base"; //$NON-NLS-1$
    public final String CONTENT_ENCODING = "Content-Encoding"; //$NON-NLS-1$
    public final String CONTENT_LANGUAGE = "Content-Language"; //$NON-NLS-1$
    public final String CONTENT_LENGTH = "Content-Length"; //$NON-NLS-1$
    public final String CONTENT_LOCATION = "Content-Location"; //$NON-NLS-1$
    public final String CONTENT_MD5 = "Content-MD5"; //$NON-NLS-1$
    public final String CONTENT_RANGE = "Content-Range"; //$NON-NLS-1$
    public final String CONTENT_TYPE = "Content-Type"; //$NON-NLS-1$
    public final String DATE = "Date"; //$NON-NLS-1$
    public final String ETAG = "ETag"; //$NON-NLS-1$
    public final String EXPIRES = "Expires"; //$NON-NLS-1$
    public final String FROM = "From"; //$NON-NLS-1$
    public final String HOST = "Host"; //$NON-NLS-1$
    public final String IF_MODIFIED_SINCE = "If-Modified-Since"; //$NON-NLS-1$
    public final String IF_MATCH = "If-Match"; //$NON-NLS-1$
    public final String IF_NONE_MATCH = "If-None-Match"; //$NON-NLS-1$
    public final String IF_RANGE = "If-Range"; //$NON-NLS-1$
    public final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since"; //$NON-NLS-1$
    public final String LAST_MODIFIED = "Last-Modified"; //$NON-NLS-1$
    public final String LOCATION = "Location"; //$NON-NLS-1$
    public final String MAX_FORWARDS = "Max-Forwards"; //$NON-NLS-1$
    public final String PRAGMA = "Pragma"; //$NON-NLS-1$
    public final String PROXY_AUTHENTICATE = "Proxy-Authenticate"; //$NON-NLS-1$
    public final String PROXY_AUTHORIZATION = "Proxy-Authorization"; //$NON-NLS-1$
    public final String PUBLIC = "Public"; //$NON-NLS-1$
    public final String RANGE = "Range"; //$NON-NLS-1$
    public final String REFERER = "Referer"; //$NON-NLS-1$
    public final String RETRY_AFTER = "Retry-After"; //$NON-NLS-1$
    public final String SERVER = "Server"; //$NON-NLS-1$
    public final String TRANSFER_ENCODING = "Transfer-Encoding"; //$NON-NLS-1$
    public final String UPGRADE = "Upgrade"; //$NON-NLS-1$
    public final String USER_AGENT = "User-Agent"; //$NON-NLS-1$
    public final String VARY = "Vary"; //$NON-NLS-1$
    public final String VIA = "Via"; //$NON-NLS-1$
    public final String WARNING = "Warning"; //$NON-NLS-1$
    public final String WWW_AUTHENTICATE = "WWW-Authenticate"; //$NON-NLS-1$

    // from WebDAV
    public final String DAV = "DAV"; //$NON-NLS-1$
    public final String DEPTH = "Depth"; //$NON-NLS-1$
    public final String DESTINATION = "Destination"; //$NON-NLS-1$
    public final String IF = "If"; //$NON-NLS-1$
    public final String LOCK_TOKEN = "Lock-Token"; //$NON-NLS-1$
    public final String OVERWRITE = "Overwrite"; //$NON-NLS-1$
    public final String STATUS_URI = "Status-URI"; //$NON-NLS-1$
    public final String TIMEOUT = "Timeout"; //$NON-NLS-1$

    // from Advanced Collections
    public final String ALL_BINDINGS = "All-Bindings"; //$NON-NLS-1$
    public final String REF_TARGET = "Ref-Target"; //$NON-NLS-1$
    public final String RES_TYPE = "Resource-Type"; //$NON-NLS-1$
    public final String PASSTHROUGH = "Passthrough"; //$NON-NLS-1$
    public final String ORDERED = "Ordered"; //$NON-NLS-1$
    public final String POSITION = "Position"; //$NON-NLS-1$

    // from Delta-V
    public final String LABEL = "Label"; //$NON-NLS-1$

    public Context() {
        super();
    }

    /**
     * Constructor for the class. Set the property defaults to be
     * the given value.
     *
     * @param defaults the default property values for the context
     */
    public Context(IContext defaults) {
        super();
        this.defaults = defaults;
    }

    /**
     * Copy all the default values into the receiver.
     */
    public void collapse() {
        if (defaults != null) {
            Enumeration keysEnum = defaults.keys();
            while (keysEnum.hasMoreElements()) {
                String key = (String) keysEnum.nextElement();
                put(key, get(key));
            }
            defaults = null;
        }
    }

    /**
     * Return the value for the given key.
     *
     * @param key the key to look up
     * @return the value for the key or null if none.
     */
    public String get(String key) {
        String value = (String) properties.get(new ContextKey(key));
        if (value == null && defaults != null)
            return defaults.get(key);
        return value;
    }

    /**
     * Gets the media types that are acceptable for a response.
     * Return the string value for the ACCEPT key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for ACCEPT
     * @see Context#getAccept()
     */
    public String getAccept() {
        return get(ACCEPT);
    }

    /**
     * Gets the character sets that are acceptable for a response.
     * Return the string value for the ACCEPT_CHARSET key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for ACCEPT_CHARSET
     * @see Context#getAcceptCharset()
     */
    public String getAcceptCharset() {
        return get(ACCEPT_CHARSET);
    }

    /**
     * Get what content-encoding values are acceptable for a response.
     * Return the string value for the ACCEPT_ENCODING key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for ACCEPT_ENCODING
     * @see Context#getAcceptEncoding()
     */
    public String getAcceptEncoding() {
        return get(ACCEPT_ENCODING);
    }

    /**
     * Get the natural languages that are acceptable for a response.
     * Return the string value for the ACCEPT_LANGUAGE key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for ACCEPT_LANGUAGE
     * @see Context#getAcceptLanguage()
     */
    public String getAcceptLanguage() {
        return get(ACCEPT_LANGUAGE);
    }

    /**
     * Get the range requests acceptable to a server.
     * Return the string value for the ACCEPT_RANGES key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for ACCEPT_RANGES
     * @see Context#getAcceptRanges()
     */
    public String getAcceptRanges() {
        return get(ACCEPT_RANGES);
    }

    /**
     * Get the sender's estimate of the time since the response was generated.
     * Return the int value for the AGE key. Return -1 if the value is not set.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for AGE
     * @see Context#getAge()
     */
    public int getAge() {
        String ageString = get(AGE);
        return (ageString == null) ? -1 : Integer.parseInt(ageString);
    }

    /**
     * Return the string value for the ALL_BINDINGS key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for ALL_BINDINGS
     * @see Context#getAllBindings()
     */
    public String getAllBindings() {
        return get(ALL_BINDINGS);
    }

    /**
     * Gets the methods allowed on a resource
     * Return the string value for the ALLOW key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for ALLOW
     * @see Context#getAllow()
     */
    public String getAllow() {
        return get(ALLOW);
    }

    /**
     * Get the user's credentials for the realm of the resource being requested.
     * Return the string value for the AUTHORIZATION key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for AUTHORIZATION
     * @see Context#getAuthorization()
     */
    public String getAuthorization() {
        return get(AUTHORIZATION);
    }

    /**
     * Get the cache control directives that must be obeyed.
     * Return the string value for the CACHE_CONTROL key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for CACHE_CONTROL
     * @see Context#getCacheControl()
     */
    public String getCacheControl() {
        return get(CACHE_CONTROL);
    }

    /**
     * Get sender connection options.
     * Return the string value for the CONNECTION key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for CONNECTION
     * @see Context#getConnection()
     */
    public String getConnection() {
        return get(CONNECTION);
    }

    /**
     * Return the String value for the CONTENT_BASE key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for CONTENT_BASE
     * @see Context#getContentBase()
     */
    public String getContentBase() {
        return get(CONTENT_BASE);
    }

    /**
     * Get what additional content encodings have been applied to the entity body.
     * Return the string value for the CONTENT_ENCODING key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for CONTENT_ENCODING
     * @see Context#getContentEncoding()
     */
    public String getContentEncoding() {
        return get(CONTENT_ENCODING);
    }

    /**
     * Gets the natural language of the intended audience for the entity body.
     * Return the string value for the CONTENT_LANGUAGE key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for CONTENT_LANGUAGE
     * @see Context#getContentLanguage()
     */
    public String getContentLanguage() {
        return get(CONTENT_LANGUAGE);
    }

    /**
     * Get the content length in bytes of the entity body.
     * Return the value for the CONTENT_LENGTH key.
     * Returns -1 if the Content-Length has not been set.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for CONTENT_LENGTH
     * @see Context#getContentLength()
     */
    public long getContentLength() {
        String lengthString = get(CONTENT_LENGTH);
        return (lengthString == null) ? -1 : Long.parseLong(lengthString);
    }

    /**
     * Return the String value for the CONTENT_LOCATION key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for CONTENT_LOCATION
     * @see Context#getContentLocation()
     */
    public String getContentLocation() {
        return get(CONTENT_LOCATION);
    }

    /**
     * Return the string value for the CONTENT_MD5 key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for CONTENT_MD5
     * @see Context#getContentMD5()
     */
    public String getContentMD5() {
        return get(CONTENT_MD5);
    }

    /**
     * Return the string value for the CONTENT_RANGE key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for CONTENT_RANGE
     * @see Context#getContentRange()
     */
    public String getContentRange() {
        return get(CONTENT_RANGE);
    }

    /**
     * Get the MIME type for the response contents.
     * Return the string value for the CONTENT_TYPE key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for CONTENT_TYPE
     * @see Context#getContentType()
     */
    public String getContentType() {
        return get(CONTENT_TYPE);
    }

    /**
     * The date the request was made.
     * Return the string value for the DATE key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for DATE
     * @see Context#getDate()
     */
    public String getDate() {
        // TBD parse and return as a Java date
        return get(DATE);
    }

    /**
     * Get the DAV level supported by the server.
     * Return the string value for the DAV key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for DAV
     * @see Context#getDAV()
     */
    public String getDAV() {
        return get(DAV);
    }

    /**
     * Return the string value for the DEPTH key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for DEPTH
     * @see Context#getDepth()
     */
    public String getDepth() {
        return get(DEPTH);
    }

    /**
     * Get the destination URI for a copy or move operation.
     * Return the URI value for the DESTINATION key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for DESTINATION
     * @see Context#getDestination()
     */
    public String getDestination() {
        return get(DESTINATION);
    }

    /**
     * Get the entity tag for the associated entity.
     * Return the string value for the ETAG key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for ETAG
     * @see Context#getETag()
     */
    public String getETag() {
        return get(ETAG);
    }

    /**
     * Get the date/time after which the response should be considered stale.
     * Return the string value for the EXPIRES key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for EXPIRES
     * @see Context#getExpires()
     */
    public String getExpires() {
        // TDB : Parse and return as a date
        return get(EXPIRES);
    }

    /**
     * Return the string value for the FROM key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for FROM
     * @see Context#getFrom()
     */
    public String getFrom() {
        return get(FROM);
    }

    /**
     * Get the Internet host and port of the resource being requested.
     * Return the string value for the HOST key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for HOST
     * @see Context#HOST
     */
    public String getHost() {
        return get(HOST);
    }

    /**
     * Return the string value for the IF key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for IF
     * @see Context#getIfKey()
     */
    public String getIfKey() {
        return get(IF);
    }

    /**
     * Return the string value for the IF_MATCH key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for IF_MATCH
     * @see Context#getIfMatch()
     */
    public String getIfMatch() {
        return get(IF_MATCH);
    }

    /**
     * Return the string value for the IF_MODIFIED_SINCE key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for IF_MODIFIED_SINCE
     * @see Context#getIfModifiedSince()
     */
    public String getIfModifiedSince() {
        return get(IF_MODIFIED_SINCE);
    }

    /**
     * Return the string value for the IF_NONE_MATCH key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for IF_NONE_MATCH
     * @see Context#getIfNoneMatch()
     */
    public String getIfNoneMatch() {
        return get(IF_NONE_MATCH);
    }

    /**
     * Return the string value for the IF_RANGE key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for IF_RANGE
     * @see Context#getIfRange()
     */
    public String getIfRange() {
        return get(IF_RANGE);
    }

    /**
     * Return the string value for the IF_UNMODIFIED_SINC key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for IF_UNMODIFIED_SINCE
     * @see Context#getIfUnmodifiedSince()
     */
    public String getIfUnmodifiedSince() {
        return get(IF_UNMODIFIED_SINCE);
    }

    /**
     * Get the message label selector.
     * Return the string value for the LABEL key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for LABEL
     * @see Context#getLabel()
     */
    public String getLabel() {
        return get(LABEL);
    }

    /**
     * Get when the resource was last modified.
     * Return the string value for the LAST_MODIFIED key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for LAST_MODIFIED
     * @see Context#getLastModified()
     */
    public String getLastModified() {
        // TDB: return as date
        return get(LAST_MODIFIED);
    }

    /**
     * Get the redirect location.
     * Return the URI value for the LOCATION key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for LOCATION
     * @see Context#getLocation()
     */
    public String getLocation() {
        return get(LOCATION);
    }

    /**
     * Gets the lock token for the resource, or null if it is not set.
     * Return the lock token value for the LOCK_TOKEN key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for LOCK_TOKEN
     * @see Context#getLockToken()
     */
    public String getLockToken() {
        return get(LOCK_TOKEN);
    }

    /**
     * Return the integer value for the MAX_FORWARDS key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for MAX_FORWARDS
     * @see Context#getMaxForwards()
     */
    public int getMaxForwards() {
        String s = get(MAX_FORWARDS);
        return s == null ? -1 : Integer.parseInt(s);
    }

    /**
     * Return the string value for the ORDERED key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for ORDERED
     * @see Context#getOrdered()
     */
    public String getOrdered() {
        return get(ORDERED);
    }

    /**
     * Get the flag that indicates if copy or move should overwrite
     * an existing destination. Return the boolean value for the
     * OVERWRITE key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for OVERWRITE
     * @see Context#OVERWRITE
     */
    public boolean getOverwrite() {
        String overwriteString = get(OVERWRITE);
        return overwriteString == null ? false : overwriteString.equalsIgnoreCase("T"); //$NON-NLS-1$
    }

    /**
     * Return the boolean value for the PASSTHROUGH key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for PASSTHROUGH
     * @see Context#getPassthrough()
     */
    public boolean getPassthrough() {
        String s = get(PASSTHROUGH);
        return s == null ? false : s.equalsIgnoreCase("T"); //$NON-NLS-1$
    }

    /**
     * Return the string value for the POSITION key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for POSITION
     * @see Context#getPosition()
     */
    public String getPosition() {
        return get(POSITION);
    }

    /**
     * Return the string value for the PRAGMA key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for PRAGMA
     * @see Context#getPragma()
     */
    public String getPragma() {
        return get(PRAGMA);
    }

    /**
     * Get any precondition that must be true in order for method
     * execution to be successful. A precondition corresponds to the
     * WebDAV "If" header.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for PRECONDITION
     * @see Context#getPrecondition()
     */
    public String getPrecondition() {
        return get(IF);
    }

    /**
     * Return the string value for the PROXY_AUTHENTICATE key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for PROXY_AUTHENTICATE
     * @see Context#getProxyAuthenticate()
     */
    public String getProxyAuthenticate() {
        return get(PROXY_AUTHENTICATE);
    }

    /**
     * Return the string value for the PROXY_AUTHORIZATION key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for PROXY_AUTHORIZATION
     * @see Context#getProxyAuthorization()
     */
    public String getProxyAuthorization() {
        return get(PROXY_AUTHORIZATION);
    }

    /**
     * Return the string value for the PUBLIC_KEY key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for PUBLIC_KEY
     * @see Context#getPublicKey()
     */
    public String getPublicKey() {
        return get(PUBLIC);
    }

    /**
     * Return the string value for the RANGE key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for RANGE
     * @see Context#getRange()
     */
    public String getRange() {
        return get(RANGE);
    }

    /**
     * Get the URI of the resource from which the request was obtained.
     * Return the String value for the REFERER key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for REFERER
     * @see Context#getReferer()
     */
    public String getReferer() {
        return get(REFERER);
    }

    /**
     * Return the URI value for the REF_TARGET key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for REF_TARGET
     * @see Context#getRefTarget()
     */
    public String getRefTarget() {
        return get(REF_TARGET);
    }

    /**
     * Return the string value for the RESOURCE_TYPE  key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for RESOURCE_TYPE
     * @see Context#getResourceType()
     */
    public String getResourceType() {
        return get(RES_TYPE);
    }

    /**
     * Return the string value for the RETRY_AFTER key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for RETRY_AFTER
     * @see Context#getRetryAfter()
     */
    public String getRetryAfter() {
        return get(RETRY_AFTER);
    }

    /**
     * Get information about the software used by the origin server
     * to handle the request. Return the string value for the SERVER
     * key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for SERVER
     * @see Context#getServer()
     */
    public String getServer() {
        return get(SERVER);
    }

    /**
     * Get the String of the resource whose method is in process.
     * Return the URI value for the STATUS_URI key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for STATUS_URI
     * @see Context#getStatusURI()
     */
    public String getStatusURI() {
        return get(STATUS_URI);
    }

    /**
     * Get the lock timeout value. The value -1 means that the
     * value was not set, the value -2 means that the value was "Infinity".
     * Return the integer value for the TIMEOUT key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for TIMEOUT
     * @see Context#getTimeout()
     */
    public int getTimeout() {
        String timeoutString = get(TIMEOUT);
        if (timeoutString == null)
            return -1;
        if (timeoutString.equalsIgnoreCase(DEPTH_INFINITY))
            return -2;
        if (timeoutString.regionMatches(true, 1, "Second-", 1, 7)) //$NON-NLS-1$
            return Integer.parseInt(timeoutString.substring(7));
        // ignore all other cases, and use infinite timeout
        return -2;
    }

    /**
     * Return the string value for the TRANSFER_ENCODING key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for TRANSFER_ENCODING
     * @see Context#getTransferEncoding()
     */
    public String getTransferEncoding() {
        return get(TRANSFER_ENCODING);
    }

    /**
     * Return the string value for the UPGRADE key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for UPGRADE
     * @see Context#getUpgrade()
     */
    public String getUpgrade() {
        return get(UPGRADE);
    }

    /**
     * Get information about the user agent originating the request.
     * Return the string value for the USER_AGENT key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for USER_AGENT
     * @see Context#getUserAgent()
     */
    public String getUserAgent() {
        return get(USER_AGENT);
    }

    /**
     * Return the string value for the VARY key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for VARY
     * @see Context#getVary()
     */
    public String getVary() {
        return get(VARY);
    }

    /**
     * Return the string value for the VIA key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for VIA
     * @see Context#getVia()
     */
    public String getVia() {
        return get(VIA);
    }

    /**
     * Return the string value for the WARNING key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for WARNING
     * @see Context#getWarning()
     */
    public String getWarning() {
        return get(WARNING);
    }

    /**
     * Return the string value for the WWW_AUTHENTICATE key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for WWW_AUTHENTICATE
     * @see Context#getWWWAuthenticate()
     */
    public String getWWWAuthenticate() {
        return get(WWW_AUTHENTICATE);
    }

    /**
     * Return an enumeration over the context's keys. (recursively computes the
     * keys based on keys defaults as well)
     *
     * @return an enumeration over the context keys
     */
    public Enumeration keys() {

        if (defaults == null)
            return new ContextKeyToStringEnum(properties.keys());

        Enumeration allKeys = new MergedEnumeration(new ContextKeyToStringEnum(properties.keys()), defaults.keys());

        Hashtable keysSet = new Hashtable();
        while (allKeys.hasMoreElements())
            keysSet.put(allKeys.nextElement(), "ignored"); //$NON-NLS-1$

        return keysSet.keys();
    }

    /**
     * Put the given key-value pair into the context.
     *
     * @param key the key
     * @param value its associated value
     */
    public void put(String key, String value) {
        ContextKey ckey = new ContextKey(key);
        if ((value == null) || (value.length() == 0))
            properties.remove(ckey);
        else
            properties.put(ckey, value);
    }

    /**
     * Remove the given key from the context
     *
     * @param key the key to remove
     */
    public void removeKey(String key) {
        properties.remove(new ContextKey(key));
    }

    /**
     * Set what media types are acceptable for a response.
     * Set the string value for the ACCEPT key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for ACCEPT
     * @see Context#setAccept(String)
     */
    public void setAccept(String value) {
        put(ACCEPT, value);
    }

    /**
     * Sets which character sets are acceptable for a response.
     * Set the string value for the ACCEPT_CHARSET key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for ACCEPT_CHARSET
     * @see Context#setAcceptCharset(String)
     */
    public void setAcceptCharset(String value) {
        put(ACCEPT_CHARSET, value);
    }

    /**
     * Set the content-encoding values which are acceptable for a response.
     * Set the string value for the ACCEPT_ENCODING key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for ACCEPT_ENCODING
     * @see Context#setAcceptEncoding(String)
     */
    public void setAcceptEncoding(String value) {
        put(ACCEPT_ENCODING, value);
    }

    /**
     * Get the natural languages that are acceptable for a response.
     * Return the string value for the ACCEPT_LANGUAGE key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @return the value for ACCEPT_LANGUAGE
     * @see Context#setAcceptLanguage()
     */
    public String setAcceptLanguage() {
        return get(ACCEPT_LANGUAGE);
    }

    /**
     * Set which natural languages are acceptable for a response.
     * Set the string value for the ACCEPT_LANGUAGE key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for ACCEPT_LANGUAGE
     * @see Context#setAcceptLanguage(String)
     */
    public void setAcceptLanguage(String value) {
        put(ACCEPT_LANGUAGE, value);
    }

    /**
     * Set the range requests acceptable to a server.
     * Set the string value for the ACCEPT_RANGES key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for ACCEPT_RANGES
     * @see Context#setAcceptRanges(String)
     */
    public void setAcceptRanges(String value) {
        put(ACCEPT_RANGES, value);
    }

    /**
     * Set the sender's estimate of the time since the response was generated.
     * Set the int value for the AGE key. Set the value to -1 to remove the key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param seconds the value for AGE
     * @see Context#setAge(int)
     */
    public void setAge(int seconds) {
        put(AGE, (seconds == -1) ? "" : Integer.toString(seconds)); //$NON-NLS-1$
    }

    /**
     * Set the string value for the ALL_BINDINGS key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param s the value for ALL_BINDINGS
     * @see Context#setAllBindings(String)
     */
    public void setAllBindings(String s) {
        put(ALL_BINDINGS, s);
    }

    /**
     * Sets methods allowed on a resource
     * Set the string value for the ALLOW key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for ALLOW
     * @see Context#setAllow(String)
     */
    public void setAllow(String value) {
        put(ALLOW, value);
    }

    /**
     * Set the user's credentials for the realm of the resource being requested.
     * Set the string value for the AUTHORIZATION key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for AUTHORIZATION
     * @see Context#setAuthorization(String)
     */
    public void setAuthorization(String value) {
        put(AUTHORIZATION, value);
    }

    /**
     * Set the cache control directives that must be obeyed.
     * Set the string value for the CACHE_CONTROL key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for CACHE_CONTROL
     * @see Context#setCacheControl(String)
     */
    public void setCacheControl(String value) {
        put(CACHE_CONTROL, value);
    }

    /**
     * Set sender connection options.
     * Set the string value for the CONNECTION key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for CONNECTION
     * @see Context#setConnection(String)
     */
    public void setConnection(String value) {
        put(CONNECTION, value);
    }

    /**
     * Set the String URL value for the CONTENT_BASE key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param url the value for CONTENT_BASE
     * @see Context#setContentBase(String)
     */
    public void setContentBase(String url) {
        put(CONTENT_BASE, url);
    }

    /**
     * Sets the additional content encodings that have been applied to the entity body.
     * Set the string value for the CONTENT_ENCODING key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for CONTENT_ENCODING
     * @see Context#setContentEncoding(String)
     */
    public void setContentEncoding(String value) {
        put(CONTENT_ENCODING, value);
    }

    /**
     * Sets the natural language of the intended audience for the entity body.
     * Set the string value for the CONTENT_LANGUAGE key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for CONTENT_LANGUAGE
     * @see Context#setContentLanguage(String)
     */
    public void setContentLanguage(String value) {
        put(CONTENT_LANGUAGE, value);
    }

    /**
     * Set the content length in bytes of the entity body.
     * Set the value for the CONTENT_LENGTH key.
     * Pass the value -1 to remove the key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for CONTENT_LENGTH
     * @see Context#setContentLength(long)
     */
    public void setContentLength(long value) {
        put(CONTENT_LENGTH, (value == -1) ? "" : Long.toString(value)); //$NON-NLS-1$
    }

    /**
     * Set the String URI value for the CONTENT_LOCATION key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for CONTENT_LOCATION
     * @see Context#setContentLocation(String)
     */
    public void setContentLocation(String value) {
        put(CONTENT_LOCATION, value);
    }

    /**
     * Set the string value for the CONTENT_MD5 key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for CONTENT_MD5
     * @see Context#setContentMD5(String)
     */
    public void setContentMD5(String value) {
        put(CONTENT_MD5, value);
    }

    /**
     * Set the string value for the CONTENT_RANGE key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for CONTENT_RANGE
     * @see Context#setContentRange(String)
     */
    public void setContentRange(String value) {
        put(CONTENT_RANGE, value);
    }

    /**
     * Set the MIME type for the response contents.
     * Set the string value for the CONTENT_TYPE key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for CONTENT_TYPE
     * @see Context#setContentType(String)
     */
    public void setContentType(String value) {
        put(CONTENT_TYPE, value);
    }

    /**
     * Set the date the request was made.
     * Set the string value for the DATE key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for DATE
     * @see Context#setDate(String)
     */
    public void setDate(String value) {
        put(DATE, value);
    }

    /**
     * Set the DAV level supported by the server.
     * Set the string value for the DAV key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for DAV
     * @see Context#setDAV(String)
     */
    public void setDAV(String value) {
        put(DAV, value);
    }

    /**
     * Set the string value for the DEPTH key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param s the value for DEPTH
     * @see Context#setDepth(String)
     */
    public void setDepth(String s) {
        put(DEPTH, s);
    }

    /**
     * Set the destination URI for a copy or move operation.
     * Set the URI value for the DESTINATION key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for DESTINATION
     * @see Context#setDestination(String)
     */
    public void setDestination(String value) {
        put(DESTINATION, value);
    }

    /**
     * Set the entity tag for the associated entity.
     * Set the string value for the ETAG key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for ETAG
     * @see Context#setETag(String)
     */
    public void setETag(String value) {
        put(ETAG, value);
    }

    /**
     * Set the date/time after which the response should be considered stale.
     * Set the string value for the EXPIRES key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for EXPIRES
     * @see Context#setExpires(String)
     */
    public void setExpires(String value) {
        // TDB accept a date
        put(EXPIRES, value);
    }

    /**
     * Set the string value for the FROM key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param s the value for FROM
     * @see Context#FROM
     */
    public void setFrom(String s) {
        put(FROM, s);
    }

    /**
     * Set the Internet host and port of the resource being requested.
     * Set the string value for the HOST key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for HOST
     * @see Context#setHost(String)
     */
    public void setHost(String value) {
        put(HOST, value);
    }

    /**
     * Set the string value for the IF key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param s the value for IF
     * @see Context#setIfKey(String)
     */
    public void setIfKey(String s) {
        put(IF, s);
    }

    /**
     * Set the string value for the IF_MATCH key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param s the value for IF_MATCH
     * @see Context#setIfMatch(String)
     */
    public void setIfMatch(String s) {
        put(IF_MATCH, s);
    }

    /**
     * Set the string value for the IF_MODIFIED_SINCE key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param s the value for IF_MODIFIED_SINCE
     * @see Context#setIfModifiedSince(String)
     */
    public void setIfModifiedSince(String s) {
        put(IF_MODIFIED_SINCE, s);
    }

    /**
     * Set the string value for the IF_NONE_MATCH key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param s the value for IF_NONE_MATCH
     * @see Context#setIfNoneMatch(String)
     */
    public void setIfNoneMatch(String s) {
        put(IF_NONE_MATCH, s);
    }

    /**
     * Set the string value for the IF_RANGE key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param s the value for IF_RANGE
     * @see Context#setIfRange(String)
     */
    public void setIfRange(String s) {
        put(IF_RANGE, s);
    }

    /**
     * Set the string value for the IF_UNMODIFIED_SINCE key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param s the value for IF_UNMODIFIED_SINCE
     * @see Context#setIfUnmodifiedSince(String)
     */
    public void setIfUnmodifiedSince(String s) {
        put(IF_UNMODIFIED_SINCE, s);
    }

    /**
     * Set the method's label selector.
     * Set the string value for the LABEL key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for LABEL
     * @see Context#setLabel(String)
     */
    public void setLabel(String value) {
        put(LABEL, value);
    }

    /**
     * Set when the resource was last modified.
     * Set the string value for the LAST_MODIFIED key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for LAST_MODIFIED
     * @see Context#setLastModified(String)
     */
    public void setLastModified(String value) {
        // TDB set as a date
        put(LAST_MODIFIED, value);
    }

    /**
     * Sets the redirect location.
     * Set the URI value for the LOCATION key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for LOCATION
     * @see Context#LOCATION
     */
    public void setLocation(String value) {
        put(LOCATION, value);
    }

    /**
     * Set the lock token for the resource.
     * Set the lock token value for the LOCK_TOKEN key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param token the value for LOCK_TOKEN
     * @see Context#setLockToken(String)
     */
    public void setLockToken(String token) {
        put(LOCK_TOKEN, token);
    }

    /**
     * Set the integer value for the MAX_FORWARDS key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param i the value for MAX_FORWARDS
     * @see Context#setMaxForwards(int)
     */
    public void setMaxForwards(int i) {
        put(MAX_FORWARDS, Integer.toString(i));
    }

    /**
     * Set the string value for the ORDERED key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for ORDERED
     * @see Context#setOrdered(String)
     */
    public void setOrdered(String value) {
        put(ORDERED, value);
    }

    /**
     * Set if copy or move should overwrite an existing destination.
     * Set the boolean value for the OVERWRITE key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for OVERWRITE
     * @see Context#setOverwrite(boolean)
     */
    public void setOverwrite(boolean value) {
        put(OVERWRITE, value ? "T" : "F"); //$NON-NLS-1$ //$NON-NLS-2$
    }

    /**
     * Set the boolean value for the PASSTHROUGH key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for PASSTHROUGH
     * @see Context#setPassthrough(boolean)
     */
    public void setPassthrough(boolean value) {
        put(PASSTHROUGH, value ? "T" : "F"); //$NON-NLS-1$ //$NON-NLS-2$
    }

    /**
     * Set the string value for the POSITION key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param s the value for POSITION
     * @see Context#setPosition(String)
     */
    public void setPosition(String s) {
        put(POSITION, s);
    }

    /**
     * Set the string value for the PRAGMA key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for PRAGMA
     * @see Context#setPragma(String)
     */
    public void setPragma(String value) {
        put(PRAGMA, value);
    }

    /**
     * Set any precondition that must be true in order for method
     * execution to be successful. A precondition corresponds to the
     * WebDAV "If" header.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for the precondition
     * @see Context#setPrecondition(String)
     */
    public void setPrecondition(String value) {
        put(IF, value);
    }

    /**
     * Set the string value for the PROXY_AUTHENTICATE key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for PROXY_AUTHENTICATE
     * @see Context#setProxyAuthenticate(String)
     */
    public void setProxyAuthenticate(String value) {
        put(PROXY_AUTHENTICATE, value);
    }

    /**
     * Set the string value for the PROXY_AUTHORIZATION key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for PROXY_AUTHORIZATION
     * @see Context#setProxyAuthorization(String)
     */
    public void setProxyAuthorization(String value) {
        put(PROXY_AUTHORIZATION, value);
    }

    /**
     * Set the string value for the PUBLIC_KEY key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for PUBLIC_KEY
     * @see Context#setPublicKey(String)
     */
    public void setPublicKey(String value) {
        put(PUBLIC, value);
    }

    /**
     * Set the string value for the RANGE key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for RANGE
     * @see Context#setRange(String)
     */
    public void setRange(String value) {
        put(RANGE, value);
    }

    /**
     * Set the URI of the resource from which the request was obtained.
     * Set the String value for the REFERER key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for REFERER
     * @see Context#setReferer(String)
     */
    public void setReferer(String value) {
        put(REFERER, value);
    }

    /**
     * Set the URI value for the REF_TARGET key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for REF_TARGET
     * @see Context#setRefTarget(String)
     */
    public void setRefTarget(String value) {
        put(REF_TARGET, value);
    }

    /**
     * Set the string value for the RESOURCE_TYPE key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for RESOURCE_TYPE
     * @see Context#setResourceType(String)
     */
    public void setResourceType(String value) {
        put(RES_TYPE, value);
    }

    /**
     * Set the string value for the RETRY_AFTER key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for RETRY_AFTER
     * @see Context#setRetryAfter(String)
     */
    public void setRetryAfter(String value) {
        put(RETRY_AFTER, value);
    }

    /**
     * Set information about the software used by the origin server
     * to handle the request. Set the string value for the SERVER key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for SERVER
     * @see Context#setServer(String)
     */
    public void setServer(String value) {
        put(SERVER, value);
    }

    /**
     * Set the URI of the resource whose method is in process.
     * Set the String value for the STATUS_URI key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for STATUS_URI
     * @see Context#setStatusURI(String)
     */
    public void setStatusURI(String value) {
        put(STATUS_URI, value);
    }

    /**
     * Set the lock timeout value in seconds. Pass -1 to clear the
     * value, pass -2 to set "Infinity". Set the integer value for the
     * TIMEOUT key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for TIMEOUT
     * @see Context#setTimeout(int)
     */
    public void setTimeout(int value) {
        if (value == -1)
            put(TIMEOUT, ""); //$NON-NLS-1$
        else
            put(TIMEOUT, (value == -2) ? DEPTH_INFINITY : "Second-" + Integer.toString(value)); //$NON-NLS-1$
    }

    /**
     * Set the string value for the TRANSFER_ENCODING key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for TRANSFER_ENCODING
     * @see Context#setTransferEncoding(String)
     */
    public void setTransferEncoding(String value) {
        put(TRANSFER_ENCODING, value);
    }

    /**
     * Set the string value for the UPGRADE key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for UPGRADE
     * @see Context#setUpgrade(String)
     */
    public void setUpgrade(String value) {
        put(UPGRADE, value);
    }

    /**
     * Set information about the user agent originating the request.
     * Set the string value for the USER_AGENT key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for USER_AGENT
     * @see Context#setUserAgent(String)
     */
    public void setUserAgent(String value) {
        put(USER_AGENT, value);
    }

    /**
     * Set the string value for the VARY key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for VARY
     * @see Context#setVary(String)
     */
    public void setVary(String value) {
        put(VARY, value);
    }

    /**
     * Set the string value for the VIA key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for VIA
     * @see Context#setVia(String)
     */
    public void setVia(String value) {
        put(VIA, value);
    }

    /**
     * Set the string value for the WARNING key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for WARNING
     * @see Context#setWarning(String)
     */
    public void setWarning(String value) {
        put(WARNING, value);
    }

    /**
     * Set the string value for the WWW_AUTHENTICATE key.
     *
     *
Implements the corresponding API in the interface
     * Context.

     *
     * @param value the value for WWW_AUTHENTICATE
     * @see Context#setWWWAuthenticate(String)
     */
    public void setWWWAuthenticate(String value) {
        put(WWW_AUTHENTICATE, value);
    }

    /**
     * Return a string representation of the context.
     *
     * @return the context, as a String
     */
    public String toString() {
        StringBuffer buffer = new StringBuffer();
        Enumeration keysEnum = keys();
        while (keysEnum.hasMoreElements()) {
            String key = (String) keysEnum.nextElement();
            buffer.append(key);
            buffer.append(": "); //$NON-NLS-1$
            buffer.append(get(key));
            buffer.append('\n');
        }
        return buffer.toString();
    }
}


/*
 * Copyright 2010 Red Hat, Inc. and/or its affiliates.
 *
 * 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 org.drools.eclipse.editors.completion;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.StringTokenizer;

import org.drools.compiler.lang.dsl.DSLMapping;
import org.drools.compiler.lang.dsl.DSLMappingEntry;
import org.drools.compiler.lang.dsl.DSLMappingEntry.Section;

public class DSLTree {

    public static final String separator = "=";
    public static final String tab = "  ";

    private Node current = null;
    private Node last = null;
    private Node rootCond = null;
    private Node rootConseq = null;
    private boolean empty = true;
    private ArrayList<String> suggestions = new ArrayList<String>();
    private HashMap<String, String> objToNL = new HashMap<String, String>();

    public DSLTree() {
        this.rootCond = new Node("root");
        this.rootConseq = new Node("root");
    }

    /**
     * the method will take the dsl file and build a DSLTree using
     * the Node class.
     * @param dslFile
     */
    public void buildTree(String dslFile) {
        buildTree(openDSLFile(dslFile));
    }

    /**
     * the method uses the DSLAdapter to get the contents of the
     * DSL mapping file.
     * @param dslcontents
     */
    public void buildTree(Reader dslcontents) {
        buildTree(createBufferedReader(dslcontents));
    }

    private void buildTree(BufferedReader breader) {
        this.rootCond.clearChildren();
        this.rootConseq.clearChildren();
        parseFile(breader);
        try {
            breader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        this.empty = false;
    }

    /**
     * method will create a BufferedReader to read the file.
     * @param filename
     * @return
     */
    protected BufferedReader openDSLFile(String filename) {
        try {
            FileReader reader = new FileReader(filename);
            BufferedReader breader = new BufferedReader(reader);
            return breader;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * Create a buffered reader for the reader created by the DSLAdapater
     * @param reader
     * @return
     */
    protected BufferedReader createBufferedReader(Reader reader) {
        return new BufferedReader(reader);
    }

    /**
     * if the DSL mapping hasn't been loaded, the method will return
     * true. If the DSL mapping has been loaded, the method returns
     * false.
     * @return
     */
    public boolean isEmpty() {
        return this.empty;
    }

    /**
     * method will use the BufferedReader to read the contents of the file.
     * It calls other methods to parse the line and build the tree.
     * @param reader
     */
    protected void parseFile(BufferedReader reader) {
        String line = null;
        try {
            while ( (line = reader.readLine()) != null) {
                Section section = getSection(line);
                String nl = stripHeadingAndCode(line);
                String objname = this.getObjMetadata(nl);
                nl = this.stripObjMetadata(nl);
                addEntry(section, nl, objname);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void buildTree(DSLMapping mapping) {
        for (DSLMappingEntry entry : mapping.getEntries()) {
            Section section = entry.getSection();
            String nl = entry.getMappingKey();
            String objname = entry.getMetaData().getMetaData();
            addEntry(section, nl, objname);
        }
    }

    private void addEntry(Section section, String nl, String objname) {
        if (!nl.startsWith("-")) {
            if (objname != null && !"".equals(objname)) {
                this.addObjToNLMap(objname, nl);
            }
            String[] tokenz = nl.split("\\s");
            if (section == DSLMappingEntry.CONDITION || section == DSLMappingEntry.ANY) {
                addTokens(tokenz, rootCond);
            }
            if (section == DSLMappingEntry.CONSEQUENCE || section == DSLMappingEntry.ANY) {
                addTokens(tokenz, rootConseq);
            }
        } else {
            String res = this.objToNL.get(objname);
            StringTokenizer tokenz = new StringTokenizer(nl);
            addTokens(res,tokenz);
        }
    }

    public void addObjToNLMap(String objname, String nl) {
        if (!objname.startsWith("-")) {
            this.objToNL.put(objname, nl);
        }
    }

    protected Section getSection(String text) {
        if (text.startsWith(DSLMappingEntry.CONDITION.getSymbol())) {
            return DSLMappingEntry.CONDITION;
        } else if (text.startsWith(DSLMappingEntry.CONSEQUENCE.getSymbol())) {
            return DSLMappingEntry.CONSEQUENCE;
        } else if (text.startsWith(DSLMappingEntry.ANY.getSymbol())) {
            return DSLMappingEntry.ANY;
        } else if (text.startsWith(DSLMappingEntry.KEYWORD.getSymbol())) {
            return DSLMappingEntry.KEYWORD;
        }
        return null;
    }

    /**
     * method will strip out the when, then, * at the beginning of each
     * line and the mapped drl expression
     * @param text
     * @return
     */
    protected String stripHeadingAndCode(String text) {
        if (text.startsWith(DSLMappingEntry.CONDITION.getSymbol())) {
            return text.substring(DSLMappingEntry.CONDITION.getSymbol().length() + 2,text.indexOf("="));
        } else if (text.startsWith(DSLMappingEntry.CONSEQUENCE.getSymbol())) {
            return text.substring(DSLMappingEntry.CONSEQUENCE.getSymbol().length() + 2,text.indexOf("="));
        } else if (text.startsWith(DSLMappingEntry.ANY.getSymbol())) {
            return text.substring(DSLMappingEntry.ANY.getSymbol().length() + 2,text.indexOf("="));
        } else if (text.startsWith("#")) {
            return "";
        } else {
            return text;
        }
    }

    /**
     * Method will return just the object metadata
     * @param text
     * @return
     */
    protected String getObjMetadata(String text) {
        if (text.startsWith("[")) {
            return text.substring(1,text.lastIndexOf("]"));
        } else {
            return "";
        }
    }

    /**
     * method will strip the metadata from the text string
     * @param text
     * @return
     */
    protected String stripObjMetadata(String text) {
        if (text.startsWith("[")) {
            return text.substring(text.lastIndexOf("]") + 1);
        } else {
            return text;
        }
    }

    /**
     * The method is different than addTokens(StringTokenizer). this method
     * expects additional metadata. It expects to get an object name or "*"
     * meaning all. If the metadata is a wildcard all, it will add the
     * tokens to all the top level nodes that are immediate child of root.
     * @param metadata
     * @param tokens
     */
    public void addTokens(String metadata, StringTokenizer tokens) {
        Node mnode = this.rootCond.addToken(metadata);
        Node thenode = mnode;
        while (tokens.hasMoreTokens()) {
            Node newnode = thenode.addToken(tokens.nextToken());
            thenode = newnode;
        }
    }

    /**
     * method adds the token to root
     * @param tokens
     */
    public void addTokens(String[] tokens, Node rootNode) {
        Node thenode = rootNode;
        for (int i = 0; i < tokens.length; i++) {
            Node newnode = thenode.addToken(tokens[i]);
            thenode = newnode;
        }
    }

    /**
     * the method will tokenize the text and try to find
     * the node that matches and return the children. the method
     * will traverse down the network as far as it can and return
     * the children at that level.
     * @param text
     * @return
     */
    public Node[] getConditionChildren(String text) {
        Node thenode = this.rootCond;
        if (text.length() > 0) {
            StringTokenizer tokenz = new StringTokenizer(text);
            this.last = this.current;
            while (tokenz.hasMoreTokens()) {
                String strtk = tokenz.nextToken();
                Node ch = thenode.getChild(strtk);
                // if a child is found, we set thenode to the child Node
                if (ch != null) {
                    thenode = ch;
                } else {
                    break;
                }
            }
            if (thenode != this.rootCond) {
                this.current = thenode;
            }
        }
        Collection<Node> children = thenode.getChildren();
        Node[] nchild = new Node[children.size()];
        return children.toArray(nchild);
    }

    /**
     * the method will tokenize the text and try to find
     * the node that matches and return the children. the method
     * will traverse down the network as far as it can and return
     * the children at that level.
     * @param text
     * @return
     */
    public Node[] getConsequenceChildren(String text) {
        Node thenode = this.rootConseq;
        if (text.length() >= 0) {
            StringTokenizer tokenz = new StringTokenizer(text);
            this.last = this.current;
            while (tokenz.hasMoreTokens()) {
                String strtk = tokenz.nextToken();
                Node ch = thenode.getChild(strtk);
                // if a child is found, we set thenode to the child Node
                if (ch != null) {
                    thenode = ch;
                } else {
                    break;
                }
            }
            if (thenode != this.rootConseq) {
                this.current = thenode;
            }
        }
        Collection<Node> children = thenode.getChildren();
        Node[] nchild = new Node[children.size()];
        return children.toArray(nchild);
    }

    /**
     * the method expects the caller to pass the object
     * @param obj
     * @param text
     * @return
     */
    public Node[] getChildren(String obj, String text) {
        Node thenode = this.rootCond.getChild(obj);
        if (thenode == null) {
            for (Node child: this.rootCond.getChildren()) {
                String tokenText = child.getToken();
                if (tokenText != null) {
                    int index = tokenText.indexOf("{");
                    if (index != -1) {
                        String substring = tokenText.substring(0, index);
                        if (obj != null && obj.startsWith(substring)) {
                            thenode = child;
                        }
                    }
                }
            }
        }
        if (thenode != null && text.length() > 0) {
            StringTokenizer tokenz = new StringTokenizer(text);
            this.last = this.current;
            while (tokenz.hasMoreTokens()) {
                String strtk = tokenz.nextToken();
                Node ch = thenode.getChild(strtk);
                // if a child is found, we set thenode to the child Node
                if (ch != null) {
                    thenode = ch;
                } else {
                    break;
                }
            }
            if (thenode != this.rootCond) {
                this.current = thenode;
            }
        }
        if (thenode == null) {
            return null;
            // thenode = this.rootCond;
        }
        Collection<Node> children = thenode.getChildren();
        Node[] nchild = new Node[children.size()];
        return children.toArray(nchild);
    }

    /**
     * for convienance, the method will return a list of strings
     * that are children of the last node found. If the editor
     * wants to generate the children strings, call the method
     * with true
     * @param text
     * @return
     */
    public ArrayList<String> getConditionChildrenList(String text, boolean addChildren) {
        Node[] c = getConditionChildren(text);
        this.suggestions.clear();
        for (int idx=0; idx < c.length; idx++) {
            this.suggestions.add(c[idx].getToken());
            if (addChildren) {
                this.addChildToList(c[idx], c[idx].getToken(), this.suggestions);
            }
        }
        return this.suggestions;
    }

    /**
     * for convienance, the method will return a list of strings
     * that are children of the last node found. If the editor
     * wants to generate the children strings, call the method
     * with true
     * @param text
     * @return
     */
    public ArrayList<String> getConsequenceChildrenList(String text, boolean addChildren) {
        Node[] c = getConsequenceChildren(text);
        this.suggestions.clear();
        for (int idx=0; idx < c.length; idx++) {
            if (addChildren) {
                this.addChildToList(c[idx], c[idx].getToken(), this.suggestions);
            } else {
                this.suggestions.add(c[idx].getToken());
            }
        }
        return this.suggestions;
    }

    /**
     *
     * @param obj
     * @param text
     * @param addChildren
     * @return
     */
    public ArrayList<String> getChildrenList(String obj, String text, boolean addChildren, boolean firstLine) {
        Node[] c = getChildren(obj,text);
        this.suggestions.clear();
        if (c != null) {
            for (int idx=0; idx < c.length; idx++) {
                if (addChildren) {
                    this.addChildToList(c[idx], c[idx].getToken(), this.suggestions);
                } else {
                    this.suggestions.add(c[idx].getToken());
                }
            }
        }
        if (text.trim().length() == 0 || this.suggestions.isEmpty()) {
            // in the event the list is empty, we also add
            // the top level nodes
            for (Node t : rootCond.getChildren()) {
                if ((!firstLine || t.getToken() != null) && !this.suggestions.contains(t.getToken())) {
                    if (addChildren) {
                        this.addChildToList(t, t.getToken(), this.suggestions);
                    } else {
                        this.suggestions.add(t.getToken());
                    }
                }
            }
        }
        return this.suggestions;
    }

    /**
     * method will prepend the parent text to the child and generate
     * the possible combinations in text format.
     * @param n
     * @param prefix
     * @param list
     */
    public void addChildToList(Node n, String prefix, ArrayList<String> list) {
        if (n.getChildren().size() > 0) {
            for (Node child : n.getChildren()) {
                if (prefix != null && "-".equals(child.getToken())) {
                    if (!list.contains(prefix)) {
                        list.add(prefix);
                    }
                    return;
                }
                String text = (prefix == null ? "" : prefix + " ") + child.getToken();
                // list.add(text);
                addChildToList(child,text,list);
            }
        } else {
            if (!list.contains(prefix)) {
                list.add(prefix);
            }
        }
    }

    public Node getCurrent() {
        return current;
    }

    public void setCurrent(Node current) {
        this.current = current;
    }

    public Node getLast() {
        return last;
    }

    public void setLast(Node last) {
        this.last = last;
    }

    /**
     * The method will print the DSLTree to System.out in text format.
     */
    public void printTree() {
        System.out.println("ROOT");
        for (Node n : rootCond.getChildren()) {
            printNode(n);
        }
    }

    /**
     * method will print the node and then iterate over the children
     * @param n
     */
    protected void printNode(Node n) {
        printTabs(n.getDepth());
        System.out.println("- \"" + n.getToken() + "\"");
        for (Node c : n.getChildren()) {
            printNode(c);
        }
    }

    /**
     * Method will print n number of tabs
     * @param count
     */
    protected void printTabs(int count) {
        for (int idx=0; idx < count; idx++) {
            System.out.print(tab);
        }
    }
}


/*******************************************************************************
 * Copyright (c) 2008, 2013 EclipseSource.
 * 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:
 *    Ralf Sternberg - initial implementation and API
 ******************************************************************************/
package com.eclipsesource.json;

import java.io.IOException;
import java.io.Reader;
import java.io.Serializable;
import java.io.StringWriter;
import java.io.Writer;

/**
 * Represents a JSON value. This can be a JSON object, an  array,
 * a number, a string, or one of the literals
 * true, false, and null.
 *

 * The literals true, false, and null are
 * represented by the constants {@link #TRUE}, {@link #FALSE}, and {@link #NULL}.
 *

 *

 * JSON objects and arrays are represented by the subtypes
 * {@link JsonObject} and {@link JsonArray}. Instances of these types can be created using the
 * public constructors of these classes.
 *

 *

 * Instances that represent JSON numbers, strings and
 * boolean values can be created using the static factory methods
 * {@link #valueOf(String)}, {@link #valueOf(long)}, {@link #valueOf(double)}, etc.
 *

 *

 * In order to find out whether an instance of this class is of a certain type, the methods
 * {@link #isObject()}, {@link #isArray()}, {@link #isString()}, {@link #isNumber()} etc. can be
 * used.
 *

 *

 * If the type of a JSON value is known, the methods {@link #asObject()}, {@link #asArray()},
 * {@link #asString()}, {@link #asInt()}, etc. can be used to get this value directly in the
 * appropriate target type.
 *

 *

 * This class is not supposed to be extended by clients.
 *

 */
@SuppressWarnings( "serial" ) // use default serial UID
public abstract class JsonValue implements Serializable {

  /**
   * Represents the JSON literal true.
   */
  public static final JsonValue TRUE = new JsonLiteral( "true" );

  /**
   * Represents the JSON literal false.
   */
  public static final JsonValue FALSE = new JsonLiteral( "false" );

  /**
   * The JSON literal null.
   */
  public static final JsonValue NULL = new JsonLiteral( "null" );

  JsonValue() {
    // prevent subclasses outside of this package
  }

  /**
   * Reads a JSON value from the given reader.
   *

   * Characters are read in chunks and buffered internally, therefore wrapping an existing reader in
   * an additional BufferedReader does not improve reading
   * performance.
   *

   *
   * @param reader
   *          the reader to read the JSON value from
   * @return the JSON value that has been read
   * @throws IOException
   *           if an I/O error occurs in the reader
   * @throws ParseException
   *           if the input is not valid JSON
   */
  public static JsonValue readFrom( Reader reader ) throws IOException {
    return new JsonParser( reader ).parse();
  }

  /**
   * Reads a JSON value from the given string.
   *
   * @param text
   *          the string that contains the JSON value
   * @return the JSON value that has been read
   * @throws ParseException
   *           if the input is not valid JSON
   */
  public static JsonValue readFrom( String text ) {
    try {
      return new JsonParser( text ).parse();
    } catch( IOException exception ) {
      // JsonParser does not throw IOException for String
      throw new RuntimeException( exception );
    }
  }

  /**
   * Returns a JsonValue instance that represents the given int value.
   *
   * @param value
   *          the value to get a JSON representation for
   * @return a JSON value that represents the given value
   */
  public static JsonValue valueOf( int value ) {
    return new JsonNumber( Integer.toString( value, 10 ) );
  }

  /**
   * Returns a JsonValue instance that represents the given long value.
   *
   * @param value
   *          the value to get a JSON representation for
   * @return a JSON value that represents the given value
   */
  public static JsonValue valueOf( long value ) {
    return new JsonNumber( Long.toString( value, 10 ) );
  }

  /**
   * Returns a JsonValue instance that represents the given float value.
   *
   * @param value
   *          the value to get a JSON representation for
   * @return a JSON value that represents the given value
   */
  public static JsonValue valueOf( float value ) {
    if( Float.isInfinite( value ) || Float.isNaN( value ) ) {
      throw new IllegalArgumentException( "Infinite and NaN values not permitted in JSON" );
    }
    return new JsonNumber( cutOffPointZero( Float.toString( value ) ) );
  }

  /**
   * Returns a JsonValue instance that represents the given double value.
   *
   * @param value
   *          the value to get a JSON representation for
   * @return a JSON value that represents the given value
   */
  public static JsonValue valueOf( double value ) {
    if( Double.isInfinite( value ) || Double.isNaN( value ) ) {
      throw new IllegalArgumentException( "Infinite and NaN values not permitted in JSON" );
    }
    return new JsonNumber( cutOffPointZero( Double.toString( value ) ) );
  }

  /**
   * Returns a JsonValue instance that represents the given string.
   *
   * @param string
   *          the string to get a JSON representation for
   * @return a JSON value that represents the given string
   */
  public static JsonValue valueOf( String string ) {
    return string == null ? NULL : new JsonString( string );
  }

  /**
   * Returns a JsonValue instance that represents the given boolean value.
   *
   * @param value
   *          the value to get a JSON representation for
   * @return a JSON value that represents the given value
   */
  public static JsonValue valueOf( boolean value ) {
    return value ? TRUE : FALSE;
  }

  public static JsonValue valueOf( Object object ) {
	    if (object != null) {
		    Class c = object.getClass();
		    if (String.class.equals(c))
		    	return valueOf( (String) object );
		    if (Integer.class.equals(c) || int.class.equals(c))
		    	return valueOf( (Integer) object );
		    if (Double.class.equals(c) || double.class.equals(c))
		    	return valueOf( (Double) object );
		    if (Float.class.equals(c) || float.class.equals(c))
		    	return valueOf( (Float) object );
		    if (Boolean.class.equals(c) || boolean.class.equals(c))
		    	return valueOf( (Boolean) object );
		    if (Long.class.equals(c) || long.class.equals(c))
		    	return valueOf( (Long) object );
		    throw new IllegalArgumentException("Unsupported object type in JsonValue.valueOf : "+c.getName());
	    }
	    return NULL;
	  }

  /**
   * Detects whether this value represents a JSON object. If this is the case, this value is an
   * instance of {@link JsonObject}.
   *
   * @return true if this value is an instance of JsonObject
   */
  public boolean isObject() {
    return false;
  }

  /**
   * Detects whether this value represents a JSON array. If this is the case, this value is an
   * instance of {@link JsonArray}.
   *
   * @return true if this value is an instance of JsonArray
   */
  public boolean isArray() {
    return false;
  }

  /**
   * Detects whether this value represents a JSON number.
   *
   * @return true if this value represents a JSON number
   */
  public boolean isNumber() {
    return false;
  }

  /**
   * Detects whether this value represents a JSON string.
   *
   * @return true if this value represents a JSON string
   */
  public boolean isString() {
    return false;
  }

  /**
   * Detects whether this value represents a boolean value.
   *
   * @return true if this value represents either the JSON literal
   *         true or false
   */
  public boolean isBoolean() {
    return false;
  }

  /**
   * Detects whether this value represents the JSON literal true.
   *
   * @return true if this value represents the JSON literal
   *         true
   */
  public boolean isTrue() {
    return false;
  }

  /**
   * Detects whether this value represents the JSON literal false.
   *
   * @return true if this value represents the JSON literal
   *         false
   */
  public boolean isFalse() {
    return false;
  }

  /**
   * Detects whether this value represents the JSON literal null.
   *
   * @return true if this value represents the JSON literal
   *         null
   */
  public boolean isNull() {
    return false;
  }

  /**
   * Returns this JSON value as {@link JsonObject}, assuming that this value represents a JSON
   * object. If this is not the case, an exception is thrown.
   *
   * @return a JSONObject for this value
   * @throws UnsupportedOperationException
   *           if this value is not a JSON object
   */
  public JsonObject asObject() {
    throw new UnsupportedOperationException( "Not an object: " + toString() );
  }

  /**
   * Returns this JSON value as {@link JsonArray}, assuming that this value represents a JSON
   * array. If this is not the case, an exception is thrown.
   *
   * @return a JSONArray for this value
   * @throws UnsupportedOperationException
   *           if this value is not a JSON array
   */
  public JsonArray asArray() {
    throw new UnsupportedOperationException( "Not an array: " + toString() );
  }

  /**
   * Returns this JSON value as an int value, assuming that this value represents a
   * JSON number that can be interpreted as Java int. If this is not the case, an
   * exception is thrown.
   *

   * To be interpreted as Java int, the JSON number must neither contain an exponent
   * nor a fraction part. Moreover, the number must be in the Integer range.
   *

   *
   * @return this value as int
   * @throws UnsupportedOperationException
   *           if this value is not a JSON number
   * @throws NumberFormatException
   *           if this JSON number can not be interpreted as int value
   */
  public int asInt() {
    throw new UnsupportedOperationException( "Not a number: " + toString() );
  }

  /**
   * Returns this JSON value as a long value, assuming that this value represents a
   * JSON number that can be interpreted as Java long. If this is not the case, an
   * exception is thrown.
   *

   * To be interpreted as Java long, the JSON number must neither contain an exponent
   * nor a fraction part. Moreover, the number must be in the Long range.
   *

   *
   * @return this value as long
   * @throws UnsupportedOperationException
   *           if this value is not a JSON number
   * @throws NumberFormatException
   *           if this JSON number can not be interpreted as long value
   */
  public long asLong() {
    throw new UnsupportedOperationException( "Not a number: " + toString() );
  }

  /**
   * Returns this JSON value as a float value, assuming that this value represents a
   * JSON number. If this is not the case, an exception is thrown.
   *

   * If the JSON number is out of the Float range, {@link Float#POSITIVE_INFINITY} or
   * {@link Float#NEGATIVE_INFINITY} is returned.
   *

   *
   * @return this value as float
   * @throws UnsupportedOperationException
   *           if this value is not a JSON number
   */
  public float asFloat() {
    throw new UnsupportedOperationException( "Not a number: " + toString() );
  }

  /**
   * Returns this JSON value as a double value, assuming that this value represents a
   * JSON number. If this is not the case, an exception is thrown.
   *

   * If the JSON number is out of the Double range, {@link Double#POSITIVE_INFINITY} or
   * {@link Double#NEGATIVE_INFINITY} is returned.
   *

   *
   * @return this value as double
   * @throws UnsupportedOperationException
   *           if this value is not a JSON number
   */
  public double asDouble() {
    throw new UnsupportedOperationException( "Not a number: " + toString() );
  }

  /**
   * Returns this JSON value as String, assuming that this value represents a JSON string. If this
   * is not the case, an exception is thrown.
   *
   * @return the string represented by this value
   * @throws UnsupportedOperationException
   *           if this value is not a JSON string
   */
  public String asString() {
    throw new UnsupportedOperationException( "Not a string: " + toString() );
  }

  /**
   * Returns this JSON value as a boolean value, assuming that this value is either
   * true or false. If this is not the case, an exception is thrown.
   *
   * @return this value as boolean
   * @throws UnsupportedOperationException
   *           if this value is neither true or false
   */
  public boolean asBoolean() {
    throw new UnsupportedOperationException( "Not a boolean: " + toString() );
  }

  /**
   * Writes the JSON representation for this object to the given writer.
   *

   * Single elements are passed directly to the given writer. Therefore, if the writer is not
   * buffered, wrapping it in a {@link java.io.BufferedWriter BufferedWriter} can drastically
   * improve writing performance.
   *

   *
   * @param writer
   *          the writer to write this value to
   * @throws IOException
   *           if an I/O error occurs in the writer
   */
  public void writeTo( Writer writer ) throws IOException {
    write( new JsonWriter( writer ) );
  }

  /**
   * Returns the JSON string for this value in its minimal form, without any additional whitespace.
   * The result is guaranteed to be a valid input for the method {@link #readFrom(String)} and to
   * create a value that is equal to this object.
   *
   * @return a JSON string that represents this value
   */
  @Override
  public String toString() {
    StringWriter stringWriter = new StringWriter();
    JsonWriter jsonWriter = new JsonWriter( stringWriter );
    try {
      write( jsonWriter );
    } catch( IOException exception ) {
      // StringWriter does not throw IOExceptions
      throw new RuntimeException( exception );
    }
    return stringWriter.toString();
  }

  /**
   * Indicates whether some other object is "equal to" this one according to the contract specified
   * in {@link Object#equals(Object)}.
   *

   * Two JsonValues are considered equal if and only if they represent the same JSON text. As a
   * consequence, two given JsonObjects may be different even though they contain the same set of
   * names with the same values, but in a different order.
   *

   *
   * @param object
   *          the reference object with which to compare
   * @return true if this object is the same as the object argument; false otherwise
   */
  @Override
  public boolean equals( Object object ) {
    return super.equals( object );
  }

  @Override
  public int hashCode() {
    return super.hashCode();
  }

  protected abstract void write( JsonWriter writer ) throws IOException;

  private static String cutOffPointZero( String string ) {
    if( string.endsWith( ".0" ) ) {
      return string.substring( 0, string.length() - 2 );
    }
    return string;
  }

}


/*
 * Copyright 2010 Red Hat, Inc. and/or its affiliates.
 *
 * 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 org.guvnor.tools.views.model;

import java.net.ConnectException;
import java.net.URL;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.ui.progress.IDeferredWorkbenchAdapter;
import org.eclipse.ui.progress.IElementCollector;
import org.eclipse.webdav.IResponse;
import org.guvnor.tools.Activator;
import org.guvnor.tools.GuvnorRepository;
import org.guvnor.tools.Messages;
import org.guvnor.tools.utils.PlatformUtils;
import org.guvnor.tools.utils.webdav.IWebDavClient;
import org.guvnor.tools.utils.webdav.ResourceProperties;
import org.guvnor.tools.utils.webdav.WebDavClientFactory;
import org.guvnor.tools.utils.webdav.WebDavException;
import org.guvnor.tools.utils.webdav.WebDavServerCache;

/**
 * A container node for Guvnor structure.
 */
public class TreeParent extends TreeObject implements IDeferredWorkbenchAdapter {

        private ArrayList<TreeObject> children;

        public TreeParent(String name, Type nodeType) {
            super(name, nodeType);
            children = new ArrayList<TreeObject>();
        }
        public void addChild(TreeObject child) {
            children.add(child);
            child.setParent(this);
        }
        public void removeChild(TreeObject child) {
            children.remove(child);
            child.setParent(null);
        }
        public TreeObject [] getChildren() {
            return (TreeObject [])children.toArray(new TreeObject[children.size()]);
        }
        public boolean hasChildren() {
            return children.size()>0;
        }

        /*
         * (non-Javadoc)
         * @see org.eclipse.ui.progress.IDeferredWorkbenchAdapter#fetchDeferredChildren(java.lang.Object, org.eclipse.ui.progress.IElementCollector, org.eclipse.core.runtime.IProgressMonitor)
         */
        public void fetchDeferredChildren(Object object,
                                         IElementCollector collector,
                                         IProgressMonitor monitor) {
            if (!(object instanceof TreeParent)) {
                return;
            }
            TreeParent node = (TreeParent)object;
            if (node.getNodeType() == Type.NONE) {
                List<GuvnorRepository> reps = Activator.getLocationManager().getRepositories();
                monitor.beginTask(Messages.getString("pending"), reps.size()); //$NON-NLS-1$
                for (int i = 0; i < reps.size(); i++) {
                    TreeParent p = new TreeParent(reps.get(i).getLocation(), Type.REPOSITORY);
                    p.setParent(node);
                    p.setGuvnorRepository(reps.get(i));
                    ResourceProperties props = new ResourceProperties();
                    props.setBase(""); //$NON-NLS-1$
                    p.setResourceProps(props);
                    collector.add(p, monitor);
                    monitor.worked(1);
                }
                monitor.done();
            }
            if (EnumSet.of(Type.REPOSITORY, Type.GLOBALS, Type.PACKAGES, Type.SNAPSHOTS, Type.PACKAGE,
                    Type.SNAPSHOT_PACKAGE).contains(node.getNodeType())) {
                listDirectory(node, collector, monitor);
            }
        }

        /**
         * Creates a directory listing.
         * @param node The directory to list.
         * @param collector The collector for the elements listed.
         * @param monitor Progress monitor for the operation.
         */
        public void listDirectory(TreeParent node,
                                  IElementCollector collector,
                                 IProgressMonitor monitor) {
            monitor.beginTask(Messages.getString("pending"), 1); //$NON-NLS-1$

            monitor.worked(1);
            GuvnorRepository rep = node.getGuvnorRepository();
            try {
                IWebDavClient webdav = WebDavServerCache.getWebDavClient(rep.getLocation());
                if (webdav == null) {
                    webdav = WebDavClientFactory.createClient(new URL(rep.getLocation()));
                    WebDavServerCache.cacheWebDavClient(rep.getLocation(), webdav);
                }
                Map<String, ResourceProperties> listing = null;
                try {
                    listing = webdav.listDirectory(node.getFullPath());
                } catch (WebDavException wde) {
                    if (wde.getErrorCode() != IResponse.SC_UNAUTHORIZED) {
                        // If not an authentication failure, we don't know what to do with it
                        throw wde;
                    }
                    boolean retry = PlatformUtils.getInstance().authenticateForServer(
                                                node.getGuvnorRepository().getLocation(), webdav);
                    if (retry) {
                        listing = webdav.listDirectory(node.getFullPath());
                    }
                }
                if (listing != null) {
                    for (String s: listing.keySet()) {
                        ResourceProperties resProps = listing.get(s);
                        TreeObject o = null;
                        if (resProps.isDirectory()) {
                            Type childType;
                            switch (getNodeType()) {
                            case REPOSITORY:
                                if (s.startsWith("snapshot")) {
                                    childType = Type.SNAPSHOTS;
                                } else if (s.startsWith("packages")) {
                                    childType = Type.PACKAGES;
                                } else if (s.startsWith("globalarea")) {
                                    childType = Type.GLOBALS;
                                } else {
                                    childType = Type.PACKAGE;
                                }
                                break;
                            case SNAPSHOTS:
                                childType = Type.SNAPSHOT_PACKAGE;
                                break;
                            case SNAPSHOT_PACKAGE:
                                childType = Type.SNAPSHOT;
                                break;
                            default:
                                childType = Type.PACKAGE;
                            }
                            o = new TreeParent(s, childType);
                        } else {
                            o = new TreeObject(s, Type.RESOURCE);
                        }
                        o.setGuvnorRepository(rep);
                        o.setResourceProps(resProps);
                        node.addChild(o);
                        collector.add(o, monitor);
                    }
                }
                monitor.worked(1);
            } catch (WebDavException e) {
                if (e.getErrorCode() == IResponse.SC_UNAUTHORIZED) {
                    PlatformUtils.reportAuthenticationFailure();
                } else {
                    if (e.getErrorCode() == IResponse.SC_NOT_IMPLEMENTED) {
                        Activator.getDefault().displayMessage(IStatus.ERROR,
                                                           Messages.getString("rep.connect.fail")); //$NON-NLS-1$
                    } else {
                        Activator.getDefault().displayError(IStatus.ERROR, e.getMessage(), e, true);
                    }
                }
            } catch (ConnectException ce) {
                Activator.getDefault().
                    displayMessage(IStatus.ERROR,
                                  Messages.getString("rep.connect.fail")); //$NON-NLS-1$
            } catch (Exception e) {
                Activator.getDefault().displayError(IStatus.ERROR, e.getMessage(), e, true);
            }
        }

        /*
         * (non-Javadoc)
         * @see org.eclipse.ui.progress.IDeferredWorkbenchAdapter#getRule(java.lang.Object)
         */
        public ISchedulingRule getRule(Object object) {
            return null;
        }
        /*
         * (non-Javadoc)
         * @see org.eclipse.ui.progress.IDeferredWorkbenchAdapter#isContainer()
         */
        public boolean isContainer() {
            return true;
        }
        /*
         * (non-Javadoc)
         * @see org.eclipse.ui.model.IWorkbenchAdapter#getChildren(java.lang.Object)
         */
        public Object[] getChildren(Object o) {
            return children.toArray();
        }
        /*
         * (non-Javadoc)
         * @see org.eclipse.ui.model.IWorkbenchAdapter#getImageDescriptor(java.lang.Object)
         */
        public ImageDescriptor getImageDescriptor(Object object) {
            return null;
        }
        /*
         * (non-Javadoc)
         * @see org.eclipse.ui.model.IWorkbenchAdapter#getLabel(java.lang.Object)
         */
        public String getLabel(Object o) {
            return o.toString();
        }
        /*
         * (non-Javadoc)
         * @see org.eclipse.ui.model.IWorkbenchAdapter#getParent(java.lang.Object)
         */
        public Object getParent(Object o) {
            if (o instanceof TreeObject) {
                return ((TreeObject)o).getParent();
            } else {
                return null;
            }
        }
}