/*
 * $Header: /home/cvspublic/jakarta-slide/src/webdav/server/org/apache/slide/webdav/util/WebdavUtils.java,v 1.39 2005/05/27 17:20:19 luetzkendorf Exp $
 * $Revision: 1.39 $
 * $Date: 2005/05/27 17:20:19 $
 *
 * ====================================================================
 *
 * Copyright 1999-2002 The Apache Software Foundation 
 *
 * 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.apache.slide.webdav.util;

import java.net.SocketException;
import java.security.Principal;
import java.util.Enumeration;

import java.io.UnsupportedEncodingException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.apache.slide.authenticate.CredentialsToken;
import org.apache.slide.common.Domain;
import org.apache.slide.common.NamespaceAccessToken;
import org.apache.slide.common.ServiceAccessException;
import org.apache.slide.common.SlideException;
import org.apache.slide.common.SlideToken;
import org.apache.slide.common.SlideTokenImpl;
import org.apache.slide.content.Content;
import org.apache.slide.content.InsufficientStorageException;
import org.apache.slide.content.NodeRevisionDescriptor;
import org.apache.slide.content.NodeRevisionDescriptors;
import org.apache.slide.event.VetoException;
import org.apache.slide.lock.ObjectLockedException;
import org.apache.slide.macro.ConflictException;
import org.apache.slide.macro.ForbiddenException;
import org.apache.slide.security.AccessDeniedException;
import org.apache.slide.security.UnauthenticatedException;
import org.apache.slide.structure.ObjectAlreadyExistsException;
import org.apache.slide.structure.ObjectNotFoundException;
import org.apache.slide.util.Configuration;
import org.apache.slide.util.logger.Logger;
import org.apache.slide.webdav.WebdavException;
import org.apache.slide.webdav.WebdavServletConfig;
import org.apache.slide.webdav.method.MethodNotAllowedException;

/**
 * A collection of various utility and convenience methods.
 *
 *
 * @version $Revision: 1.39 $
 **/
public class WebdavUtils {
    
    
    private static final String CREDENTIALS_ATTRIBUTE =
        "org.apache.slide.webdav.method.credentials";

    private static final String LOG_CHANNEL =
        WebdavUtils.class.getName();

    /**
     * Constructs a new String based on bytes sequence contained
     * in the
     * Non-ASCII parts of the sequence will be decoded with UTF-8
     * if possible, or with the specified encoding.
     *
     * @param string A "String" returned by tomcat getPathInfo()
     * @param enc Encoding to use if non UTF-8
     * @return A properly encoded String object
     */
    public static String decodeString(String string, String enc)
        throws UnsupportedEncodingException {
        if(string == null)
            return null;
        StringBuffer sb = null;
        int j = 0;
        int sf = 0;
        sb = new StringBuffer();
        byte utf8buffer[] = new byte[5];
        // Get bytes without any decoding
        byte bytes[] = string.getBytes("ISO-8859-1");
        for (int i = 0; i < bytes.length; i+=1) {
            byte b = bytes[i];
            int bb = (b >= 0) ? b : b+256;
            utf8buffer[j++] = b;
            boolean ok = false;
            // First test if non-ascii
            if (bb >= 128) {
                // No ongoing UTF-8 decoding ?
                if (sf==0) {
                    // Now test if this can be a UTF-8 first byte
                    if (bb >= 192 && i < 252) {
                        ok = true;
                        // Determine UTF-8 size
                        if (bb >= 224)
                            if (bb >= 240)
                                if (bb >= 248)
                                    sf = 4;
                                else
                                    sf = 3;
                            else
                                sf = 2;
                        else
                            sf = 1;
                    }
                } else if (bb >= 128 && bb < 192) {
                    // This is a UTF-8 part
                    sf--;
                    if (sf == 0) {
                        sb.append(new String(utf8buffer,0,j,"UTF-8"));
                        j = 0;
                    }
                    ok = true;
                }
            }
            // If there was an error during UTF-8 decoding, decode all remaining chars with default encoding
            if (!ok) {
                sb.append(new String(utf8buffer,0,j, enc));
                j = 0;
                sf = 0;
            }
        }
        // Remaining chars
        if (j > 0) {
            sb.append(new String(utf8buffer,0,j, enc));
        }
        return sb.toString();
    }


    /**
     * Return a context-relative path, beginning with a "/", that represents
     * the canonical version of the specified path after ".." and "." elements
     * are resolved out.  If the specified path attempts to go outside the
     * boundaries of the current context (i.e. too many ".." path elements
     * are present), return <code>null</code> instead.
     *
     * @param path the path to be normalized
     **/
    public static String normalizeURL(String path) {

       if (path == null)
            return null;
        
        String normalized = path;

        if (normalized == null)
            return (null);
        
        // Normalize the slashes and add leading slash if necessary
        if (normalized.indexOf('\\') >= 0)
            normalized = normalized.replace('\\', '/');
        if (!normalized.startsWith("/"))
            normalized = "/" + normalized;
        
        // Resolve occurrences of "//" in the normalized path
        while (true) {
            int index = normalized.indexOf("//");
            if (index < 0)
                break;
            normalized = normalized.substring(0, index) +
                normalized.substring(index + 1);
        }
        
        // Resolve occurrences of "/./" in the normalized path
        while (true) {
            int index = normalized.indexOf("/./");
            if (index < 0)
                break;
            normalized = normalized.substring(0, index) +
                normalized.substring(index + 2);
        }
        
        // Resolve occurrences of "/../" in the normalized path
        while (true) {
            int index = normalized.indexOf("/../");
            if (index < 0)
                break;
            if (index == 0)
                return (null);  // Trying to go outside our context
            int index2 = normalized.lastIndexOf('/', index - 1);
            normalized = normalized.substring(0, index2) +
                normalized.substring(index + 3);
        }
        normalized = normalized.replace('?','_');
        // Return the normalized path that we have completed
        return (normalized);
    }
    
    
    /**
     * URL rewriter.
     *
     * @param path the path to be rewritten
     **/
    public static String encodeURL(String path) {
        return URLUtil.URLEncode(path, Configuration.realUrlEncoding());
    }
    
    
    /**
     * URL rewriter.
     *
     * @param path the path to be rewritten
     * @param enc the encoding
     **/
    public static String encodeURL(String path, String enc) {
        return URLUtil.URLEncode(path, enc);
    }

    /**
     * Maps the URI of a node in the Slide namespace to an external URI as seen
     * by clients. This involves adding the context and servlet path to the
     * URI.
     *
     * @param uri the node's URI
     * @param config configuration of the WebdavServlet
     *
     * @return the node's URI as absolute path
     **/
    public static String getAbsolutePath
        (String uri, HttpServletRequest req, WebdavServletConfig config)
    {
        return getAbsolutePath (uri, req.getContextPath(), req.getServletPath(), config);
    }
        
    /**
     * Maps the URI of a node in the Slide namespace to an external URI as seen
     * by clients. This involves adding the context and servlet path to the
     * URI.
     *
     * @param    uri                 the node's URI
     * @param    servletPath         a String, the result of HttpRequest.getServletPath()
     * @param    contextPath         a  String , the result of HttpRequest.getContextPath()
     * @param    config              configuration of the WebdavServlet
     *
     * @return   a String
     *
     */
    public static String getAbsolutePath (String uri, String contextPath,
                                          String servletPath,
                                          WebdavServletConfig config)
    {
        String result = uri;
        
        String scope = config.getScope();
        int scopeLength = scope.length();
        if (scopeLength > 0 && uri.startsWith(scope)) {
            result = uri.substring(scopeLength);
        }
        
        if (!config.isDefaultServlet()) {
            contextPath += servletPath;
        }
        result = contextPath + result;
        
        return encodeURL(result);
    }
    
    public static String getAbsolutePath (String uri, String slideContextPath,
            WebdavServletConfig config)
    {
        String result = uri;
        
        String scope = config.getScope();
        int scopeLength = scope.length();
        if (scopeLength > 0 && uri.startsWith(scope)) {
            result = uri.substring(scopeLength);
        }
        
        result = slideContextPath + result;
        
        return encodeURL(result);
    }
    
    public static String getAbsolutePath (String uri, WebdavContext context)
    {
        String result = uri;
        
        String scope = context.getServletConfig().getScope();
        int scopeLength = scope.length();
        if (scopeLength > 0 && uri.startsWith(scope)) {
            result = uri.substring(scopeLength);
        }
        
        result = context.getContextPath() + result;
        
        return encodeURL(result);
    }
    
    /**
     * Maps the request URI of a HTTP request to a URI in the Slide namespace
     * (this does not necessarily mean that a node exists at that URI).
     *
     * @param req the request object
     * @param config configuration of the WebdavServlet
     *
     * @return the request URI mapped into the Slide namespace
     **/
    public static String getRelativePath
        (HttpServletRequest req, WebdavServletConfig config) {
        
        // get the requested path, depending on whether the servlet is mapped
        // as default servlet.
        String result = null;
    
        String pathMapperClassName = config.getInitParameter("path-mapper-hook");
        if (pathMapperClassName != null) {
            try {
                Class storeClass = Class.forName(pathMapperClassName);
                PathMapper mapper = (PathMapper) storeClass.newInstance();
                result = mapper.map(req, config);
            } catch (Exception e) {
                Domain.log(e, LOG_CHANNEL, Logger.WARNING);
            }
        }
    
        if (result == null) {
            PathMapper mapper = (PathMapper) config.getServletContext().getAttribute(
                    "org.apache.slide.webdav.util.PathMapper");
            if (mapper != null) {
                result = mapper.map(req, config);
            }
        }
        
        if (result == null) {
            if (config.isDefaultServlet()) {
                result = req.getServletPath();
            } else {
                result = req.getRequestURI();
                result = result.substring(req.getContextPath().length()+ req.getServletPath().length());
            }
        }
        
        // default to the namespace root if no path-info is specified
        if ((result == null) || (result.length() == 0)) {
            result = "/";
        }
    
        // prefix the URI with the configured scope
        result = config.getScope() + result;
        result = normalizeURL(fixTomcatURL(result));  // the request URL is utf-8 encoded
        return result;
    
    }

    /**
     * Returns an URL based on input. The input URL is encoded with "fromEncoding".
     *    The resulting URL is encoded as specified in Configuration.urlEncoding()
     *
     * @param input the input URL
     *
     * @return a new URL encoded in Configuration.urlEncoding()
     **/
    public static String fixTomcatURL(String input) {
        return fixTomcatHeader(input, "UTF-8");
    }
    
    
    
    /**
     * Returns an decoded header value. The input header is encoded with "ISO-8859-1".
     *    The resulting header is encoded as specified in toEncoding
     *
     * @param header the input header
     * @param toEncoding the target encoding of the header
     *
     * @return a new header value encoded in toEncoding
     **/
    public static String fixTomcatHeader(String header, String toEncoding) {
        // todo: toEncoding parameter not used anymore
        if (header == null) return null;

        String result = null;
        try {
            result = URLUtil.URLDecode(header.getBytes("ISO-8859-1"),"ISO-8859-1");
            result = decodeString(result,Configuration.urlEncoding());
            if (!Configuration.useUTF8()) {
                // Remove unsupported characters
                result = new String(result.getBytes(Configuration.urlEncoding()),Configuration.urlEncoding());
            }
        } catch (Exception e) { e.printStackTrace(); }
        return result;
    }
    
    /**
     * Returns a SlideToken using the authentication information of an HTTP
     * request.
     *
     * @param req the HTTP request
     *
     * @return a new SlideToken instance
     **/
    public static SlideToken getSlideToken(HttpServletRequest req) {
        
        CredentialsToken credentialsToken;
        Principal principal = req.getUserPrincipal();
        HttpSession session = req.getSession();
        
        // store the current principal in the session, to get around a bug in
        // IE 5 where the authentication info is not submitted by IE when
        // doing a HEAD request.
        if (principal == null) {
            final String credentials = (String) session.getAttribute(CREDENTIALS_ATTRIBUTE);
            credentialsToken = new CredentialsToken(credentials == null ? "" : credentials);
        } else {
            // because the principal is not guaranteed to be serializable
            // and could thus create problems in a distributed deployment
            // we store the principal name instead of the principal itself
            session.setAttribute(CREDENTIALS_ATTRIBUTE, principal.getName());
            credentialsToken = new CredentialsToken(principal);
        }
        
        SlideToken token = new SlideTokenImpl(credentialsToken);
        token.setEnforceLockTokens(true);

        // store session attributes in token parameters to pass them through
        // to the stores
        for(Enumeration e = session.getAttributeNames(); e.hasMoreElements();) {
            String name = (String)e.nextElement();
            token.addParameter(name, session.getAttribute(name));
        }
        
        return token;
    }
    
    
    /**
     * Tests whether a given path maps to a URI in the Slide namespace that
     * identifies a collection resource.
     *
     * @param token the namespace access token
     * @param slideToken the slide token
     * @param path relative path of the resource
     *
     * @return true if the requested resource is a collection, false otherwise
     **/
    public static boolean isCollection
        (NamespaceAccessToken token, SlideToken slideToken,
         String path) {
        
        // Added for DeltaV --start--
        if( Configuration.useVersionControl() ) {
            UriHandler uh = UriHandler.getUriHandler( path );
            if( uh.isWorkspaceUri() )
                return true;
            if( uh.isHistoryUri() )
                return true;
            if( uh.isVersionUri() )
                return false;
        }
        // Added for DeltaV --end--
        
        try {
            Content content = token.getContentHelper();
            NodeRevisionDescriptors revisionDescriptors =
                content.retrieve(slideToken, path);
            if (revisionDescriptors.hasRevisions()) {
                NodeRevisionDescriptor revisionDescriptor =
                    content.retrieve(slideToken, revisionDescriptors);
                return isCollection(revisionDescriptor);
            } else {
                return true;
            }
        } catch(ObjectNotFoundException e) {
            // if the Object is not found return false for no 207 is generated
            return false;
        } catch(SlideException e) {
            // this is the default
            return true;
        }
    }
    
    
    /**
     * Tests whether a resource is a collection resource.
     *
     * @param revisionDescriptor revision descriptor of the resource
     *
     * @return true if the descriptor represents a collection, false otherwise
     **/
    public static boolean isCollection
        (NodeRevisionDescriptor revisionDescriptor) {
        
        boolean result = false;
        
        if (revisionDescriptor == null)
            return true;
        
        if (revisionDescriptor.propertyValueContains(
                NodeRevisionDescriptor.RESOURCE_TYPE ,"collection")) {
            result = true;
        }
        
        return result;
    }


    /**
     * Tests whether a resource is a redirect reference.
     *
     * @param revisionDescriptor revision descriptor of the resource
     *
     * @return <code>true</code> if the descriptor represents a redirect
     *       reference, <code>false</code> otherwise.
     */
    public static boolean isRedirectref
        (NodeRevisionDescriptor revisionDescriptor) {
        if (revisionDescriptor == null)
            return false;

        return revisionDescriptor.propertyValueContains(
                NodeRevisionDescriptor.RESOURCE_TYPE, "redirectref");
    }


    public static String getSlidePath(String fullpath, String slideContextPath) {
        // strip off the protocol://host:port part
        if (fullpath.indexOf("://") >= 0) {
            fullpath=fullpath.substring(fullpath.indexOf("://")+3);
            fullpath=fullpath.substring(fullpath.indexOf("/"));
        }
    
        // strip off the servlet context path
        if (fullpath.startsWith(slideContextPath)) {
            fullpath=fullpath.substring(slideContextPath.length());
        }
        return fullpath;
    }
    
    /**
     * Get return status based on exception type.
     */
    public static int getErrorCode(Throwable ex) {
        if ( !(ex instanceof SlideException) ) {
            //            if( ex != null ) ex.printStackTrace();
            return WebdavStatus.SC_INTERNAL_SERVER_ERROR;
        } else {
            return getErrorCode((SlideException)ex);
        }
    }
    
    /**
     * Get return status based on exception type.
     */
    public static int getErrorCode(SlideException ex) {
        try {
            throw ex;
        } catch(ObjectNotFoundException e) {
            return WebdavStatus.SC_NOT_FOUND;
        } catch(ConflictException e) {
            return WebdavStatus.SC_CONFLICT;
        } catch(ForbiddenException e) {
            return WebdavStatus.SC_FORBIDDEN;
        } catch(AccessDeniedException e) {
            return WebdavStatus.SC_FORBIDDEN;
        } catch(UnauthenticatedException e) {
            return WebdavStatus.SC_UNAUTHORIZED;
        } catch(ObjectAlreadyExistsException e) {
            return WebdavStatus.SC_PRECONDITION_FAILED;
        } catch(ServiceAccessException e) {
            return getErrorCode((ServiceAccessException)ex);
        } catch(ObjectLockedException e) {
            return WebdavStatus.SC_LOCKED;
        } catch(WebdavException e) {
            return e.getStatusCode();
        } catch(MethodNotAllowedException e) {
            return WebdavStatus.SC_METHOD_NOT_ALLOWED;
        } catch(InsufficientStorageException e) {
            return WebdavStatus.SC_INSUFFICIENT_STORAGE;
        } catch(VetoException e) {
            if (e != e.getCause()) {
                return getErrorCode(e.getCause());
            }
        	return WebdavStatus.SC_NOT_ACCEPTABLE;
        } catch(SlideException e) {
            return WebdavStatus.SC_INTERNAL_SERVER_ERROR;
        }
    }

    /**
     * Get return status based on exception type.
     */
    public static int getErrorCode(ServiceAccessException ex) {
        Throwable cause = ex.getCauseException();
        // XXX SocketException pops up when the client closes the connection in the middle of transferring
        // the HTTP body, so this is no internal error, really!
        if (cause != null && cause instanceof SocketException) {
            // XXX is this a reasonable code? 
            return WebdavStatus.SC_PRECONDITION_FAILED;
        } else if (cause == null || !(cause instanceof SlideException))  {
            //            ex.printStackTrace();
            if( cause != null ) cause.printStackTrace();
            return WebdavStatus.SC_INTERNAL_SERVER_ERROR;  // this is the default}

        } else  {
            return getErrorCode((SlideException)cause);
        }
    }
}

