/*
 * $Header: /home/cvs/jakarta-slide/src/webdav/server/org/apache/slide/webdav/util/WebdavUtils.java,v 1.24.2.3 2004/09/27 08:46:55 luetzkendorf Exp $
 * $Revision: 1.24.2.3 $
 * $Date: 2004/09/27 08:46:55 $
 *
 * ====================================================================
 *
 * 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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.apache.slide.authenticate.CredentialsToken;
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.structure.ObjectAlreadyExistsException;
import org.apache.slide.structure.ObjectNotFoundException;
import org.apache.slide.util.Configuration;
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.24.2.3 $
 **/
public class WebdavUtils {
    
    
    private static final String CREDENTIALS_ATTRIBUTE =
        "org.apache.slide.webdav.method.credentials";
    
    
    // --------------------------------------------------------- Public Methods
    
    
    /**
     * 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 decodeURL(String path) {
        return decodeURL(path, Configuration.urlEncoding());
    }
    
    /**
     * 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 decodeURL(String path, String enc) {

       if (path == null)
            return null;
        
        // Resolve encoded characters in the normalized path,
        // which also handles encoded spaces so we can skip that later.
        // Placed at the beginning of the chain so that encoded
        // bad stuff(tm) can be caught by the later checks
        String normalized = URLUtil.URLDecode(path, enc);
        
        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);
        }
                
        // 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.urlEncoding());
    }
    
    
    /**
     * 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;
        
        int scopeLength = config.getScope().length();
        if (scopeLength > 0) {
            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;
        
        int scopeLength = config.getScope().length();
        if (scopeLength > 0) {
            result = uri.substring(scopeLength);
        }
        
        result = slideContextPath + 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;
        if (config.isDefaultServlet()) {
            result = req.getServletPath();
        } else {
            result = req.getPathInfo();
        }
        
        // 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;
        
        
        
        return decodeURL(fixTomcatURL(result, "UTF-8"));  // the request URL is utf-8 encoded
    }
    
    
    /**
     * 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
     * @param fromEncoding the used encoding of the input URL
     *
     * @return a new URL encoded in Configuration.urlEncoding()
     **/
    public static String fixTomcatURL(String input, String fromEncoding) {
        
        if (input == null) return null;
        
        String result = null;
        try {
            
            //          printString(input.substring(27,input.length()));
            //          byte[] a = input.getBytes("UTF-8");
            result = encodeURL(new String(input.getBytes(fromEncoding), Configuration.urlEncoding()),
                               Configuration.urlEncoding());
            //          printString(result.substring(27,result.length()));
            //          byte[] b = result.getBytes(Configuration.urlEncoding());
            //          System.out.println("Length b " + b.length);
        } catch (Exception e) { e.printStackTrace(); }
        //      System.out.println("Length = " + input.length());
        //      System.out.println("Length = " + result.length());
        return result;
    }
    
    
    
    /**
     * 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) {
        
        if (header == null) return null;
        
        String result = null;
        try {
//            result = URLUtil.URLDecode(new String(header.getBytes("ISO-8859-1"), toEncoding),
//                                       toEncoding);
            result = URLUtil.URLDecode(new String(header.getBytes("UTF-8"), toEncoding),
                    toEncoding);
        } 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;
    }
    
    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(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(VetoException e) {
        	// FIXME: Should be mapped to a matching error code
        	//return WebdavStatus.SC_NOT_ACCEPTABLE;
        	if (e.getCause() instanceof InsufficientStorageException) {
        		return WebdavStatus.SC_INSUFFICIENT_STORAGE;
			}
        	else return WebdavStatus.SC_NOT_ACCEPTABLE;
        } catch(SlideException e) {
            //            e.printStackTrace();
            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);
        }
    }
    
    //TB
    /**
     * 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");
    }
    /**
     * 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);
    }
    
 
}

