/*
 * #%L
 * Alfresco Remote API
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.alfresco.repo.web.scripts.bean;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.io.Writer;
import java.net.SocketException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.query.CannedQueryPageDetails;
import org.alfresco.query.PagingRequest;
import org.alfresco.query.PagingResults;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.model.filefolder.HiddenAspect;
import org.alfresco.repo.policy.BehaviourFilter;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.alfresco.repo.security.permissions.AccessDeniedException;
import org.alfresco.repo.site.SiteModel;
import org.alfresco.service.cmr.model.FileExistsException;
import org.alfresco.service.cmr.model.FileFolderService;
import org.alfresco.service.cmr.model.FileFolderUtil;
import org.alfresco.service.cmr.model.FileInfo;
import org.alfresco.service.cmr.model.FileNotFoundException;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.security.OwnableService;
import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.service.cmr.site.SiteInfo;
import org.alfresco.service.cmr.site.SiteService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.extensions.surf.util.URLDecoder;
import org.springframework.extensions.webscripts.Status;
import org.springframework.extensions.webscripts.WebScriptException;
import org.springframework.extensions.webscripts.WebScriptResponse;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

/**
 * ADM Remote Store service.
 * <p>
 * This implementation of the RemoteStore is tied to the current SiteService implementation.
 * <p>
 * It remaps incoming generic document path requests to the appropriate folder structure
 * in the Sites folder. Dashboard pages and component bindings are remapped to take advantage
 * of inherited permissions in the appropriate root site folder, ensuring that only valid
 * users can write to the appropriate configuration objects.
 * 
 * @see BaseRemoteStore for the available API methods.
 * 
 * @author Kevin Roast
 */
public class ADMRemoteStore extends BaseRemoteStore
{
    private static final Log logger = LogFactory.getLog(ADMRemoteStore.class);
    
    // name of the surf config folder
    private static final String SURF_CONFIG = "surf-config";

    // patterns used to match site and user specific configuration locations
    private static final String PATH_COMPONENTS = "components";
    private static final String PATH_PAGES      = "pages";
    private static final String PATH_USER       = "user";
    private static final String PATH_SITE       = "site";
    private static final String USER_CONFIG = ".*\\." + PATH_USER + "~(.*)~.*";
    private static final String USER_CONFIG_PATTERN = USER_CONFIG.replaceAll("\\.\\*", "*").replace("\\", "");
    private static final Pattern USER_PATTERN_1 = Pattern.compile(".*/" + PATH_COMPONENTS + "/" + USER_CONFIG);
    private static final Pattern USER_PATTERN_2 = Pattern.compile(".*/" + PATH_PAGES + "/" + PATH_USER + "/(.*?)(/.*)?$");
    private static final Pattern SITE_PATTERN_1 = Pattern.compile(".*/" + PATH_COMPONENTS + "/.*\\." + PATH_SITE + "~(.*)~.*");
    private static final Pattern SITE_PATTERN_2 = Pattern.compile(".*/" + PATH_PAGES + "/" + PATH_SITE + "/(.*?)(/.*)?$");
    
    
    // service beans
    protected NodeService nodeService;
    protected NodeService unprotNodeService;
    protected FileFolderService fileFolderService;
    protected NamespaceService namespaceService;
    protected SiteService siteService;
    protected ContentService contentService;
    protected HiddenAspect hiddenAspect;
    protected PermissionService permissionService;
    protected OwnableService ownableService;
    private BehaviourFilter behaviourFilter;
    
    /**
     * Date format pattern used to parse HTTP date headers in RFC 1123 format.
     */
    private static final String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz";
    private static final TimeZone GMT = TimeZone.getTimeZone("GMT");


    /**
     * @param nodeService       the NodeService to set
     */
    public void setNodeService(NodeService nodeService)
    {
        this.nodeService = nodeService;
    }

    /**
     * @param nodeService       the NodeService to set
     */
    public void setUnprotectedNodeService(NodeService nodeService)
    {
        this.unprotNodeService = nodeService;
    }

    /**
     * @param fileFolderService the FileFolderService to set
     */
    public void setFileFolderService(FileFolderService fileFolderService)
    {
        this.fileFolderService = fileFolderService;
    }

    /**
     * @param namespaceService  the NamespaceService to set
     */
    public void setNamespaceService(NamespaceService namespaceService)
    {
        this.namespaceService = namespaceService;
    }
    
    /**
     * @param siteService       the SiteService to set
     */
    public void setSiteService(SiteService siteService)
    {
        this.siteService = siteService;
    }
    
    /**
     * @param contentService    the ContentService to set
     */
    public void setContentService(ContentService contentService)
    {
        this.contentService = contentService; 
    }
    
    public void setHiddenAspect(HiddenAspect hiddenAspect)
    {
        this.hiddenAspect = hiddenAspect;
    }
    
    public void setBehaviourFilter(BehaviourFilter behaviourFilter)
    {
        this.behaviourFilter = behaviourFilter;
    }

    public void setPermissionService(PermissionService permissionService)
    {
        this.permissionService = permissionService;
    }

    public void setOwnableService(OwnableService ownableService)
    {
        this.ownableService = ownableService;
    }

    /**
     * Gets the last modified timestamp for the document.
     * <p>
     * The output will be the last modified date as a long toString().
     * 
     * @param path  document path to an existing document
     */
    @Override
    protected void lastModified(final WebScriptResponse res, final String store, final String path)
        throws IOException
    {
        AuthenticationUtil.runAs(new RunAsWork<Void>()
        {
            @SuppressWarnings("synthetic-access")
            public Void doWork() throws Exception
            {
                final String encpath = encodePath(path);
                final FileInfo fileInfo = resolveFilePath(encpath);
                if (fileInfo == null)
                {
                    throw new WebScriptException("Unable to locate file: " + encpath);
                }
                
                Writer out = res.getWriter();
                out.write(Long.toString(fileInfo.getModifiedDate().getTime()));
                out.close();
                if (logger.isDebugEnabled())
                    logger.debug("lastModified: " + Long.toString(fileInfo.getModifiedDate().getTime()));
                return null;
            }
        }, AuthenticationUtil.getSystemUserName());
    }

    /**
     * Gets a document.
     * <p>
     * The output will be the document content stream.
     * 
     * @param path  document path
     */
    @Override
    protected void getDocument(final WebScriptResponse res, final String store, final String path)
    {
        AuthenticationUtil.runAs(new RunAsWork<Void>()
        {
            @SuppressWarnings("synthetic-access")
            public Void doWork() throws Exception
            {
                final String encpath = encodePath(path);
                final FileInfo fileInfo = resolveFilePath(encpath);
                if (fileInfo == null || fileInfo.isFolder())
                {
                    res.setStatus(Status.STATUS_NOT_FOUND);
                    return null;
                }
                
                final ContentReader reader;
                try
                {
                    reader = contentService.getReader(fileInfo.getNodeRef(), ContentModel.PROP_CONTENT);
                    if (reader == null || !reader.exists())
                    {
                        throw new WebScriptException("No content found for file: " + encpath);
                    }
                    
                    // establish mimetype
                    String mimetype = reader.getMimetype();
                    if (mimetype == null || mimetype.length() == 0)
                    {
                        mimetype = MimetypeMap.MIMETYPE_BINARY;
                        int extIndex = encpath.lastIndexOf('.');
                        if (extIndex != -1)
                        {
                            String ext = encpath.substring(extIndex + 1);
                            String mt = mimetypeService.getMimetypesByExtension().get(ext);
                            if (mt != null)
                            {
                                mimetype = mt;
                            }
                        }
                    }
                    
                    // set mimetype for the content and the character encoding + length for the stream
                    res.setContentType(mimetype);
                    res.setContentEncoding(reader.getEncoding());
                    SimpleDateFormat formatter = new SimpleDateFormat(PATTERN_RFC1123, Locale.US);
                    formatter.setTimeZone(GMT);
                    res.setHeader("Last-Modified", formatter.format(fileInfo.getModifiedDate()));
                    res.setHeader("Content-Length", Long.toString(reader.getSize()));
                    
                    if (logger.isDebugEnabled())
                        logger.debug("getDocument: " + fileInfo.toString());
                    
                    // get the content and stream directly to the response output stream
                    // assuming the repository is capable of streaming in chunks, this should allow large files
                    // to be streamed directly to the browser response stream.
                    try
                    {
                        reader.getContent(res.getOutputStream());
                    }
                    catch (SocketException e1)
                    {
                        // the client cut the connection - our mission was accomplished apart from a little error message
                        if (logger.isDebugEnabled())
                            logger.debug("Client aborted stream read:\n\tnode: " + encpath + "\n\tcontent: " + reader);
                    }
                    catch (ContentIOException e2)
                    {
                        if (logger.isInfoEnabled())
                            logger.info("Client aborted stream read:\n\tnode: " + encpath + "\n\tcontent: " + reader);
                    }
                    catch (Throwable err)
                    {
                       if (err.getCause() instanceof SocketException)
                       {
                          if (logger.isDebugEnabled())
                              logger.debug("Client aborted stream read:\n\tnode: " + encpath + "\n\tcontent: " + reader);
                       }
                       else
                       {
                           if (logger.isInfoEnabled())
                               logger.info(err.getMessage());
                           res.setStatus(Status.STATUS_INTERNAL_SERVER_ERROR);
                       }
                    }
                }
                catch (AccessDeniedException ae)
                {
                    res.setStatus(Status.STATUS_UNAUTHORIZED);
                }
                return null;
            }
        }, AuthenticationUtil.getSystemUserName());
    }

    /**
     * Determines if the document exists.
     * 
     * The output will be either the string "true" or the string "false".
     * 
     * @param path  document path
     */
    @Override
    protected void hasDocument(final WebScriptResponse res, final String store, final String path) throws IOException
    {
        AuthenticationUtil.runAs(new RunAsWork<Void>()
        {
            @SuppressWarnings("synthetic-access")
            public Void doWork() throws Exception
            {
                final String encpath = encodePath(path);
                final FileInfo fileInfo = resolveFilePath(encpath);
                
                Writer out = res.getWriter();
                out.write(Boolean.toString(fileInfo != null && !fileInfo.isFolder()));
                out.close();
                if (logger.isDebugEnabled())
                    logger.debug("hasDocument: " + Boolean.toString(fileInfo != null && !fileInfo.isFolder()));
                return null;
            }
        }, AuthenticationUtil.getSystemUserName());
    }

    /**
     * Creates a document.
     * <p>
     * Create methods are user authenticated, so the creation of site config must be
     * allowed for the current user.
     * 
     * @param path          document path
     * @param content       content of the document to write
     */
    @Override
    protected void createDocument(final WebScriptResponse res, final String store, final String path, final InputStream content)
    {
        try
        {
            writeDocument(path, content);
        }
        catch (AccessDeniedException ae)
        {
            res.setStatus(Status.STATUS_UNAUTHORIZED);
            throw ae;
        }
        catch (FileExistsException feeErr)
        {
            res.setStatus(Status.STATUS_CONFLICT);
            throw feeErr;
        }
    }
    
    /**
     * Creates multiple XML documents encapsulated in a single one. 
     * 
     * @param res       WebScriptResponse
     * @param store       String
     * @param in       XML document containing multiple document contents to write
     */
    @Override
    protected void createDocuments(WebScriptResponse res, String store, InputStream in)
    {
        try
        {
            DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); 
            Document document;
            document = documentBuilder.parse(in);
            Element docEl = document.getDocumentElement();
            Transformer transformer = ADMRemoteStore.this.transformer.get();
            for (Node n = docEl.getFirstChild(); n != null; n = n.getNextSibling())
            {
                if (!(n instanceof Element))
                {
                    continue;
                }
                final String path = ((Element) n).getAttribute("path");
                
                // Turn the first element child into a document
                Document doc = documentBuilder.newDocument();
                Node child;
                for (child = n.getFirstChild(); child != null ; child=child.getNextSibling())
                {
                   if (child instanceof Element)
                   {
                       doc.appendChild(doc.importNode(child, true));
                       break;
                   }
                }
                ByteArrayOutputStream out = new ByteArrayOutputStream(512);
                transformer.transform(new DOMSource(doc), new StreamResult(out));
                out.close();
                
                writeDocument(path, new ByteArrayInputStream(out.toByteArray()));
            }
        }
        catch (AccessDeniedException ae)
        {
            res.setStatus(Status.STATUS_UNAUTHORIZED);
            throw ae;
        }
        catch (FileExistsException feeErr)
        {
            res.setStatus(Status.STATUS_CONFLICT);
            throw feeErr;
        }
        catch (Exception e)
        {
            // various annoying checked SAX/IO exceptions related to XML processing can be thrown
            // none of them should occur if the XML document is well formed
            logger.error(e);
            res.setStatus(Status.STATUS_INTERNAL_SERVER_ERROR);
            throw new AlfrescoRuntimeException(e.getMessage(), e);
        }
    }
    
    protected void writeDocument(final String path, final InputStream content)
    {
        final String encpath = encodePath(path);
        final int off = encpath.lastIndexOf('/');
        if (off != -1)
        {
            // check we actually are the user we are creating a user specific path for
            final String runAsUser = getPathRunAsUser(path);
            AuthenticationUtil.runAs(new RunAsWork<Void>()
            {
                @SuppressWarnings("synthetic-access")
                public Void doWork() throws Exception
                {
                    final FileInfo parentFolder = resolveNodePath(encpath, true, false);
                    if (parentFolder == null)
                    {
                        throw new IllegalStateException("Unable to aquire parent folder reference for path: " + path);
                    }
                    
                    // ALF-17729 / ALF-17796 - disable auditable on parent folder
                    NodeRef parentFolderRef = parentFolder.getNodeRef();
                    behaviourFilter.disableBehaviour(parentFolderRef, ContentModel.ASPECT_AUDITABLE);
                    
                    try
                    {
                        final String name = encpath.substring(off + 1);
                        // existence check - convert to an UPDATE - could occur if multiple threads request
                        // a write to the same document - a valid possibility but rare
                        if (nodeService.getChildByName(parentFolderRef, ContentModel.ASSOC_CONTAINS, name) == null)
                        {
                            FileInfo fileInfo = fileFolderService.create(
                                    parentFolderRef, name, ContentModel.TYPE_CONTENT);
                            final NodeRef nodeRef = fileInfo.getNodeRef();
                            // MNT-16371: Revoke ownership privileges for surf-config folder contents, to tighten access for former SiteManagers.
                            ownableService.setOwner(nodeRef, AuthenticationUtil.getAdminUserName());

                            Map<QName, Serializable> aspectProperties = new HashMap<QName, Serializable>(1, 1.0f);
                            aspectProperties.put(ContentModel.PROP_IS_INDEXED, false);
                            unprotNodeService.addAspect(nodeRef, ContentModel.ASPECT_INDEX_CONTROL, aspectProperties);
                            ContentWriter writer = contentService.getWriter(nodeRef, ContentModel.PROP_CONTENT, true);
                            writer.guessMimetype(fileInfo.getName());
                            writer.putContent(content);
                            if (logger.isDebugEnabled())
                                logger.debug("createDocument: " + fileInfo.toString());
                        }
                        else
                        {
                            ContentWriter writer = contentService.getWriter(
                                    nodeService.getChildByName(parentFolderRef, ContentModel.ASSOC_CONTAINS, name),
                                    ContentModel.PROP_CONTENT,
                                    true);
                            writer.guessMimetype(name);
                            writer.putContent(content);
                            if (logger.isDebugEnabled())
                                logger.debug("createDocument (updated): " + name);
                        }
                    }
                    finally
                    {
                        behaviourFilter.enableBehaviour(parentFolderRef, ContentModel.ASPECT_AUDITABLE);
                    }
                    
                    return null;
                }
            }, runAsUser);
        }
    }
    
    /**
     * Get the RunAs user need to execute a Write operation on the given path.
     * 
     * @param path  Document path
     * @return runas user - will be the Full Authenticated User or System as required
     */
    protected String getPathRunAsUser(final String path)
    {
        // check we actually are the user we are creating a user specific path for
        String runAsUser = AuthenticationUtil.getFullyAuthenticatedUser();
        String userId = null;
        Matcher matcher;
        if ((matcher = USER_PATTERN_1.matcher(path)).matches())
        {
            userId = matcher.group(1);
        }
        else if ((matcher = USER_PATTERN_2.matcher(path)).matches())
        {
            userId = matcher.group(1);
        }
        if (userId != null && userId.equals(runAsUser))
        {
            runAsUser = AuthenticationUtil.getSystemUserName();
        }
        return runAsUser;
    }

    /**
     * Updates an existing document.
     * <p>
     * Update methods are user authenticated, so the modification of site config must be
     * allowed for the current user.
     * 
     * @param path          document path to update
     * @param content       content to update the document with
     */
    @Override
    protected void updateDocument(final WebScriptResponse res, String store, final String path, final InputStream content)
    {
        final String runAsUser = getPathRunAsUser(path);
        AuthenticationUtil.runAs(new RunAsWork<Void>()
        {
            @SuppressWarnings("synthetic-access")
            public Void doWork() throws Exception
            {
                final String encpath = encodePath(path);
                final FileInfo fileInfo = resolveFilePath(encpath);
                if (fileInfo == null || fileInfo.isFolder())
                {
                    res.setStatus(Status.STATUS_NOT_FOUND);
                    return null;
                }
                
                try
                {
                    ContentWriter writer = contentService.getWriter(fileInfo.getNodeRef(), ContentModel.PROP_CONTENT, true);
                    writer.putContent(content);
                    if (logger.isDebugEnabled())
                        logger.debug("updateDocument: " + fileInfo.toString());
                }
                catch (AccessDeniedException ae)
                {
                    res.setStatus(Status.STATUS_UNAUTHORIZED);
                    throw ae;
                }
                return null;
            }
        }, runAsUser);
    }
    
    /**
     * Deletes an existing document.
     * <p>
     * Delete methods are user authenticated, so the deletion of the document must be
     * allowed for the current user.
     * 
     * @param path  document path
     */
    @Override
    protected void deleteDocument(final WebScriptResponse res, final String store, final String path)
    {
        final String encpath = encodePath(path);
        final FileInfo fileInfo = resolveFilePath(encpath);
        if (fileInfo == null || fileInfo.isFolder())
        {
            res.setStatus(Status.STATUS_NOT_FOUND);
            return;
        }
        
        final String runAsUser = getPathRunAsUser(path);
        AuthenticationUtil.runAs(new RunAsWork<Void>()
        {
            @SuppressWarnings("synthetic-access")
            public Void doWork() throws Exception
            {
                try
                {
                    final NodeRef fileRef = fileInfo.getNodeRef();
                    // MNT-16371: Revoke ownership privileges for surf-config folder contents, to tighten access for former SiteManagers.
                    nodeService.addAspect(fileRef, ContentModel.ASPECT_TEMPORARY, null);

                    // ALF-17729
                    NodeRef parentFolderRef = unprotNodeService.getPrimaryParent(fileRef).getParentRef();
                    behaviourFilter.disableBehaviour(parentFolderRef, ContentModel.ASPECT_AUDITABLE);

                    try
                    {
                        nodeService.deleteNode(fileRef);
                    }
                    finally
                    {
                        behaviourFilter.enableBehaviour(parentFolderRef, ContentModel.ASPECT_AUDITABLE);
                    }

                    if (logger.isDebugEnabled())
                        logger.debug("deleteDocument: " + fileInfo.toString());
                }
                catch (AccessDeniedException ae)
                {
                    res.setStatus(Status.STATUS_UNAUTHORIZED);
                    throw ae;
                }
                return null;
            }
        }, runAsUser);
    }

    /**
     * Lists the document paths under a given path.
     * <p>
     * The output will be the list of relative document paths found under the path.
     * Separated by newline characters.
     * 
     * @param path      document path
     * @param recurse   true to peform a recursive list, false for direct children only.
     * 
     * @throws IOException if an error occurs listing the documents
     */
    @Override
    protected void listDocuments(final WebScriptResponse res, final String store, final String path, final boolean recurse)
        throws IOException
    {
        AuthenticationUtil.runAs(new RunAsWork<Void>()
        {
            @SuppressWarnings("synthetic-access")
            public Void doWork() throws Exception
            {
                res.setContentType("text/plain;charset=UTF-8");
                
                final String encpath = encodePath(path);
                final FileInfo fileInfo = resolveNodePath(encpath, false, true);
                if (fileInfo == null || !fileInfo.isFolder())
                {
                    res.setStatus(Status.STATUS_NOT_FOUND);
                    return null;
                }
                
                try
                {
                    outputFileNodes(res.getWriter(), fileInfo, aquireSurfConfigRef(encpath, false), "*", recurse);
                }
                catch (AccessDeniedException ae)
                {
                    res.setStatus(Status.STATUS_UNAUTHORIZED);
                }
                finally
                {
                    res.getWriter().close();
                }
                return null;
            }
        }, AuthenticationUtil.getSystemUserName());
    }
    
    /**
     * Lists the document paths matching a file pattern under a given path.
     * 
     * The output will be the list of relative document paths found under the path that
     * match the given file pattern. Separated by newline characters.
     * 
     * @param path      document path
     * @param pattern   file pattern to match - allows wildcards e.g. page.*.site.xml
     * 
     * @throws IOException if an error occurs listing the documents
     */
    @Override
    protected void listDocuments(final WebScriptResponse res, final String store, final String path, final String pattern)
        throws IOException
    {
        AuthenticationUtil.runAs(new RunAsWork<Void>()
        {
            @SuppressWarnings("synthetic-access")
            public Void doWork() throws Exception
            {
                res.setContentType("text/plain;charset=UTF-8");
                
                String filePattern;
                if (pattern == null || pattern.length() == 0)
                {
                    filePattern = "*";
                }
                else
                {
                    // need to ensure match pattern is path encoded - but don't encode * character!
                    StringBuilder buf = new StringBuilder(pattern.length());
                    for (StringTokenizer t = new StringTokenizer(pattern, "*"); t.hasMoreTokens(); /**/)
                    {
                        buf.append(encodePath(t.nextToken()));
                        if (t.hasMoreTokens())
                        {
                            buf.append('*');
                        }
                    }
                    // ensure the escape character is itself escaped
                    filePattern = buf.toString().replace("\\", "\\\\");
                }
                
                // ensure we pass in the file pattern as it is used as part of the folder match - i.e.
                // for a site component set e.g. /alfresco/site-data/components/page.*.site~xyz~dashboard.xml
                final String encpath = encodePath(path);
                final FileInfo fileInfo = resolveNodePath(encpath, filePattern, false, true);
                if (fileInfo == null || !fileInfo.isFolder())
                {
                    res.setStatus(Status.STATUS_NOT_FOUND);
                    return null;
                }
                
                if (logger.isDebugEnabled())
                    logger.debug("listDocuments() pattern: " + filePattern);
                
                try
                {
                    outputFileNodes(
                            res.getWriter(), fileInfo,
                            aquireSurfConfigRef(encpath + "/" + filePattern, false),
                            filePattern, false);
                }
                catch (AccessDeniedException ae)
                {
                    res.setStatus(Status.STATUS_UNAUTHORIZED);
                }
                finally
                {
                    res.getWriter().close();
                }
                return null;
            }
        }, AuthenticationUtil.getSystemUserName());
    }

    /**
     * @param path      cm:name based root relative path
     *                  example: /alfresco/site-data/pages/customise-user-dashboard.xml
     * 
     * @return FileInfo representing the file/folder at the specified path location
     *         or null if the supplied path does not exist in the store
     */
    private FileInfo resolveFilePath(final String path)
    {
        return resolveNodePath(path, false, false);
    }
    
   /**
     * @param path      cm:name based root relative path
     *                  example: /alfresco/site-data/pages/customise-user-dashboard.xml
     *                           /alfresco/site-data/components
     * @param create    if true create the config and folder dirs for the given path returning
     *                  the FileInfo for the last parent in the path, if false only attempt to
     *                  resolve the folder path if it exists returning the last element.
     * @param isFolder  True if the path is for a folder, false if it ends in a filename
     * 
     * @return FileInfo representing the file/folder at the specified path location (see create
     *         parameter above) or null if the supplied path does not exist in the store.
     */
    private FileInfo resolveNodePath(final String path, final boolean create, final boolean isFolder)
    {
        return resolveNodePath(path, null, create, isFolder);
    }
    
    /**
     * @param path      cm:name based root relative path
     *                  example: /alfresco/site-data/pages/customise-user-dashboard.xml
     *                           /alfresco/site-data/components
     * @param pattern   optional pattern that is used as part of the match to aquire the surf-config
     *                  folder under the appropriate sites or user location.
     * @param create    if true create the config and folder dirs for the given path returning
     *                  the FileInfo for the last parent in the path, if false only attempt to
     *                  resolve the folder path if it exists returning the last element.
     * @param isFolder  True if the path is for a folder, false if it ends in a filename
     * 
     * @return FileInfo representing the file/folder at the specified path location (see create
     *         parameter above) or null if the supplied path does not exist in the store.
     */
    private FileInfo resolveNodePath(final String path, final String pattern, final boolean create, final boolean isFolder)
    {
        if (logger.isDebugEnabled())
            logger.debug("Resolving path: " + path);

        final String adminUserName = AuthenticationUtil.getAdminUserName();

        FileInfo result = null;
        if (path != null)
        {
            // break down the path into its component elements
            List<String> pathElements = new ArrayList<String>(4);
            final StringTokenizer t = new StringTokenizer(path, "/");
            // the store requires paths of the form /alfresco/site-data/<objecttype>[/<folder>]/<file>.xml
            if (t.countTokens() >= 3)
            {
                t.nextToken();  // skip /alfresco
                t.nextToken();  // skip /site-data
                // collect remaining folder path (and file)
                while (t.hasMoreTokens())
                {
                    pathElements.add(t.nextToken());
                }
                
                NodeRef surfConfigRef = aquireSurfConfigRef(path + (pattern != null ? ("/" + pattern) : ""), create);
                try
                {
                    if (surfConfigRef != null)
                    {
                        if (create)
                        {
                            List<String> folders = isFolder ? pathElements : pathElements.subList(0, pathElements.size() - 1);
                            
                            List<FileFolderUtil.PathElementDetails> folderDetails = new ArrayList<>(pathElements.size());
                            Map<QName, Serializable> prop = new HashMap<>(2);
                            prop.put(ContentModel.PROP_IS_INDEXED, false);
                            prop.put(ContentModel.PROP_IS_CONTENT_INDEXED, false);
                            for (String element : folders)
                            {
                                Map<QName, Map<QName, Serializable>> aspects = Collections.singletonMap(ContentModel.ASPECT_INDEX_CONTROL, prop);
                                folderDetails.add(new FileFolderUtil.PathElementDetails(element, aspects));
                            }
                            // ensure folders exist down to the specified parent
                            // ALF-17729 / ALF-17796 - disable auditable on parent folders
                            Set<NodeRef> allCreatedFolders = new LinkedHashSet<>();
                            result = FileFolderUtil.makeFolders(
                                    this.fileFolderService,nodeService,
                                    surfConfigRef,
                                    folderDetails,
                                    ContentModel.TYPE_FOLDER,
                                    behaviourFilter,
                                    new HashSet<QName>(Arrays.asList(new QName[]{ContentModel.ASPECT_AUDITABLE})), allCreatedFolders);

                            // MNT-16371: Revoke ownership privileges for surf-config folder, to tighten access for former SiteManagers.
                            for(NodeRef nodeRef : allCreatedFolders)
                            {
                                ownableService.setOwner(nodeRef, adminUserName);