Swift – SE.S21 Share - DM Remote Store - WIP

- not hooked in as the default yet - fleshing out methods and impl

git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@28338 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
Kevin Roast
2011-06-10 15:35:26 +00:00
parent 27fe806253
commit 227b9bf7bf
7 changed files with 973 additions and 131 deletions

View File

@@ -0,0 +1,8 @@
<webscript>
<shortname>Remote ADM Store</shortname>
<description>Remote service mirroring the Store interface - to an ADM store</description>
<url>/remoteadm/{method}/{path}</url>
<authentication>user</authentication>
<transaction>required</transaction>
<format default="">argument</format>
</webscript>

View File

@@ -0,0 +1,9 @@
<webscript>
<shortname>Remote ADM Store</shortname>
<description>Remote service mirroring the Store interface - to an ADM store</description>
<url>/remoteadm/{method}</url>
<url>/remoteadm/{method}/{path}</url>
<authentication>none</authentication>
<transaction allow="readonly">required</transaction>
<format default="">argument</format>
</webscript>

View File

@@ -0,0 +1,8 @@
<webscript>
<shortname>Remote ADM Store</shortname>
<description>Remote service mirroring the Store interface - to an ADM store</description>
<url>/remoteadm/{method}/{path}</url>
<authentication>user</authentication>
<transaction>required</transaction>
<format default="">argument</format>
</webscript>

View File

@@ -474,6 +474,33 @@
<property name="searchService" ref="SearchService" /> <property name="searchService" ref="SearchService" />
</bean> </bean>
<!-- Remote Store service - ADM -->
<bean id="webscript.org.alfresco.repository.store.remoteadm.get" class="org.alfresco.repo.web.scripts.bean.ADMRemoteStore" parent="webscript">
<property name="nodeService" ref="NodeService" />
<property name="unprotectedNodeService" ref="nodeService" />
<property name="mimetypeService" ref="MimetypeService" />
<property name="fileFolderService" ref="FileFolderService"/>
<property name="contentService" ref="ContentService"/>
<property name="siteService" ref="SiteService" />
</bean>
<bean id="webscript.org.alfresco.repository.store.remoteadm.post" class="org.alfresco.repo.web.scripts.bean.ADMRemoteStore" parent="webscript">
<property name="nodeService" ref="NodeService" />
<property name="unprotectedNodeService" ref="nodeService" />
<property name="mimetypeService" ref="MimetypeService" />
<property name="fileFolderService" ref="FileFolderService"/>
<property name="contentService" ref="ContentService"/>
<property name="siteService" ref="SiteService" />
</bean>
<bean id="webscript.org.alfresco.repository.store.remoteadm.delete" class="org.alfresco.repo.web.scripts.bean.ADMRemoteStore" parent="webscript">
<property name="nodeService" ref="NodeService" />
<property name="unprotectedNodeService" ref="nodeService" />
<property name="mimetypeService" ref="MimetypeService" />
<property name="fileFolderService" ref="FileFolderService"/>
<property name="contentService" ref="ContentService"/>
<property name="siteService" ref="SiteService" />
</bean>
<!-- Authenticated Web Framework AVM Store service --> <!-- Authenticated Web Framework AVM Store service -->
<bean id="webscript.org.alfresco.webframework.avmstore.get" class="org.alfresco.repo.web.scripts.bean.AVMRemoteStore" parent="webscript"> <bean id="webscript.org.alfresco.webframework.avmstore.get" class="org.alfresco.repo.web.scripts.bean.AVMRemoteStore" parent="webscript">
<property name="mimetypeService" ref="MimetypeService" /> <property name="mimetypeService" ref="MimetypeService" />

View File

@@ -0,0 +1,795 @@
/*
* Copyright (C) 2005-2007 Alfresco Software Limited.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* As a special exception to the terms and conditions of version 2.0 of
* the GPL, you may redistribute this Program in connection with Free/Libre
* and Open Source Software ("FLOSS") applications as described in Alfresco's
* FLOSS exception. You should have recieved a copy of the text describing
* the FLOSS exception, and it is also available here:
* http://www.alfresco.com/legal/licensing
*/
package org.alfresco.repo.web.scripts.bean;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.io.Writer;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.content.MimetypeMap;
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.security.permissions.noop.PermissionServiceNOOPImpl;
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.repository.Path;
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.webscripts.Status;
import org.springframework.extensions.webscripts.WebScriptException;
import org.springframework.extensions.webscripts.WebScriptResponse;
/**
* 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.
* <p>
* System folders are used to hide configuration from general folder browsing UI.
*
* @see BaseRemoteStore for the available API methods.
*
* @author Kevin Roast
*/
public class ADMRemoteStore extends BaseRemoteStore
{
private static final Log logger = LogFactory.getLog(ADMRemoteStore.class);
private static final String SURF_CONFIG = "surf-config";
private static final Pattern USER_PATTERN_1 = Pattern.compile(".*/components/.*\\.user~(.*)~.*");
private static final Pattern USER_PATTERN_2 = Pattern.compile(".*/pages/user/(.*?)(/.*)?$");
private static final Pattern SITE_PATTERN_1 = Pattern.compile(".*/components/.*\\.site~(.*)~.*");
private static final Pattern SITE_PATTERN_2 = Pattern.compile(".*/pages/site/(.*?)(/.*)?$");
private NodeService nodeService;
private NodeService unprotNodeService;
private FileFolderService fileFolderService;
private NamespaceService namespaceService;
private SiteService siteService;
private ContentService contentService;
/**
* @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;
}
/**
* Gets the last modified timestamp for the document.
*
* The output will be the last modified date as a long toString().
*
* @param store the store id
* @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<Object>()
{
@SuppressWarnings("synthetic-access")
public Object doWork() throws Exception
{
final FileInfo fileInfo = resolveFilePath(path);
if (fileInfo == null)
{
throw new WebScriptException("Unable to locate file: " + path);
}
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.
*
* The output will be the document content stream.
*
* @param store the store id
* @param path document path
*/
@Override
protected void getDocument(final WebScriptResponse res, final String store, final String path)
{
// TODO: could only allow appropriate users to read config docs i.e. site specific...
// but currently GET requests need to run unauthenticated before user login occurs.
AuthenticationUtil.runAs(new RunAsWork<Object>()
{
@SuppressWarnings("synthetic-access")
public Object doWork() throws Exception
{
final FileInfo fileInfo = resolveFilePath(path);
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: " + path);
}
// establish mimetype
String mimetype = reader.getMimetype();
if (mimetype == null || mimetype.length() == 0)
{
mimetype = MimetypeMap.MIMETYPE_BINARY;
int extIndex = path.lastIndexOf('.');
if (extIndex != -1)
{
String ext = path.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());
res.setHeader("Last-Modified", Long.toString(fileInfo.getModifiedDate().getTime()));
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: " + path + "\n\tcontent: " + reader);
}
catch (ContentIOException e2)
{
if (logger.isInfoEnabled())
logger.info("Client aborted stream read:\n\tnode: " + path + "\n\tcontent: " + reader);
}
catch (Throwable err)
{
if (err.getCause() instanceof SocketException)
{
if (logger.isDebugEnabled())
logger.debug("Client aborted stream read:\n\tnode: " + path + "\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 store the store id
* @param path document path
*/
@Override
protected void hasDocument(final WebScriptResponse res, final String store, final String path) throws IOException
{
AuthenticationUtil.runAs(new RunAsWork<Object>()
{
@SuppressWarnings("synthetic-access")
public Object doWork() throws Exception
{
final FileInfo fileInfo = resolveFilePath(path);
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 store the store id
* @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
{
// TODO: don't need to support filenames at the root?
final int off = path.lastIndexOf('/');
if (off != -1)
{
FileInfo parentFolder = resolveNodePath(path, true, false);
FileInfo fileInfo = this.fileFolderService.create(
parentFolder.getNodeRef(), encodePath(path.substring(off + 1)), ContentModel.TYPE_CONTENT);
this.contentService.getWriter(
fileInfo.getNodeRef(), ContentModel.PROP_CONTENT, true).putContent(content);
if (logger.isDebugEnabled())
logger.debug("createDocument: " + fileInfo.toString());
}
}
catch (FileExistsException feeErr)
{
res.setStatus(Status.STATUS_CONFLICT);
}
}
/**
* Creates multiple XML documents encapsulated in a single one.
*
* @param store the store id
* @param path document path
* @param content content of the document to write
*/
@Override
protected void createDocuments(WebScriptResponse res, String store, InputStream content)
{
// no implementation currently
}
/**
* Updates an existing document.
* <p>
* Update methods are user authenticated, so the modification of site config must be
* allowed for the current user.
*
* @param store the store id
* @param path document path
* @param content content to update the document with
*/
@Override
protected void updateDocument(final WebScriptResponse res, String store, final String path, final InputStream content)
{
final FileInfo fileInfo = resolveFilePath(path);
if (fileInfo == null || fileInfo.isFolder())
{
res.setStatus(Status.STATUS_NOT_FOUND);
}
ContentWriter writer = contentService.getWriter(fileInfo.getNodeRef(), ContentModel.PROP_CONTENT, true);
writer.putContent(content);
if (logger.isDebugEnabled())
logger.debug("updateDocument: " + fileInfo.toString());
}
/**
* Deletes an existing document.
* <p>
* Delete methods are user authenticated, so the deletion of site config must be
* allowed for the current user.
*
* @param store the store id
* @param path document path
*/
@Override
protected void deleteDocument(final WebScriptResponse res, final String store, final String path)
{
final FileInfo fileInfo = resolveFilePath(path);
if (fileInfo == null || fileInfo.isFolder())
{
res.setStatus(Status.STATUS_NOT_FOUND);
}
this.nodeService.deleteNode(fileInfo.getNodeRef());
if (logger.isDebugEnabled())
logger.debug("deleteDocument: " + fileInfo.toString());
}
/**
* 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 store the store id
* @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<Object>()
{
@SuppressWarnings("synthetic-access")
public Object doWork() throws Exception
{
res.setContentType("text/plain;charset=UTF-8");
final FileInfo fileInfo = resolveNodePath(path, false, true);
if (fileInfo == null || !fileInfo.isFolder())
{
res.setStatus(Status.STATUS_NOT_FOUND);
return null;
}
try
{
final boolean debug = logger.isDebugEnabled();
final Writer out = res.getWriter();
final NodeRef surfConfigRef = aquireSurfConfigRef(path, false);
Map<NodeRef, String> nameCache = new HashMap<NodeRef, String>();
List<FileInfo> files = fileFolderService.search(fileInfo.getNodeRef(), "*", true, false, recurse);
for (final FileInfo file : files)
{
// walking up the parent tree manually until the "surf-config" parent is hit
// and manually appending the rest of the cm:name path down to the node.
StringBuilder displayPath = new StringBuilder(64);
NodeRef ref = unprotNodeService.getPrimaryParent(file.getNodeRef()).getParentRef();
while (!ref.equals(surfConfigRef))
{
String name = nameCache.get(ref);
if (name == null)
{
name = (String)unprotNodeService.getProperty(ref, ContentModel.PROP_NAME);
nameCache.put(ref, name);
}
displayPath.insert(0, '/');
displayPath.insert(0, name);
ref = unprotNodeService.getPrimaryParent(ref).getParentRef();
}
out.write("/alfresco/site-data/");
out.write(displayPath.toString());
out.write(file.getName());
out.write('\n');
if (debug) logger.debug(" /alfresco/site-data/" + displayPath.toString() + "/" + file.getName());
}
}
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 store the store id
* @param path document path
* @param pattern file pattern to match - allows wildcards e.g. *.xml or 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<Object>()
{
@SuppressWarnings("synthetic-access")
public Object doWork() throws Exception
{
res.setContentType("text/plain;charset=UTF-8");
final FileInfo fileInfo = resolveNodePath(path, false, true);
if (fileInfo == null || !fileInfo.isFolder())
{
res.setStatus(Status.STATUS_NOT_FOUND);
return null;
}
String filePattern = pattern;
if (filePattern == null || filePattern.length() == 0)
{
filePattern = "*";
}
final boolean debug = logger.isDebugEnabled();
//
// TODO: remove use of SearchService here! Jan wrote a DB version for AVM - same for DM required.
//
// encode the qname match - and convert back any wildcard characters
/*String qname = ISO9075.encode(filePattern);
qname = qname.replace(ISO9075_ASTERIX, "*");
final String encPath = toQNamePath(path);
final StringBuilder query = new StringBuilder(128);
query.append("+PATH:\"/").append(NamespaceService.APP_MODEL_PREFIX).append(":").append(ADMROOT)
.append(encPath != null && encPath.length() != 0 ? encPath : "")
.append("//*\" +QNAME:")
.append(qname);
final Writer out = res.getWriter();
final int cropPoint = rootPath.length() + 1;
StoreRef storeRef = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, store);
String sQuery = query.toString();
ResultSet resultSet = searchService.query(storeRef, SearchService.LANGUAGE_LUCENE, sQuery);
try
{
List<NodeRef> nodes = resultSet.getNodeRefs();
for (final NodeRef nodeRef : nodes)
{
String name = (String)unprotNodeService.getProperty(nodeRef, ContentModel.PROP_NAME);
Path path = unprotNodeService.getPath(nodeRef);
// TODO: optimize this section
String displayPath = path.toDisplayPath(unprotNodeService, new PermissionServiceNOOPImpl());
out.write(displayPath.substring(cropPoint));
out.write('/');
out.write(name);
out.write('\n');
}
}
finally
{
resultSet.close();
}*/
// TODO: close writer!
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)
{
if (logger.isDebugEnabled())
logger.debug("Resolving path: " + path);
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(encodePath(path), "/");
// we require 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, create);
try
{
if (create)
{
// ensure folders exist down to the specified parent
result = FileFolderUtil.makeFolders(
this.fileFolderService,
surfConfigRef,
isFolder ? pathElements : pathElements.subList(0, pathElements.size() - 1),
ContentModel.TYPE_FOLDER);
}
else
{
// perform the cm:name path lookup against our config root node
if (surfConfigRef != null)
{
result = this.fileFolderService.resolveNamePath(surfConfigRef, pathElements);
}
}
}
catch (FileNotFoundException fnfErr)
{
// this is a valid condition - we return null to indicate failed lookup
}
}
}
return result;
}
/**
* Aquire (optionally create) the NodeRef to the "surf-config" folder as appropriate
* for the given path.
* <p>
* Disassmbles the path to correct match either user, site or generic folder path.
*
* @param path
* @param create
*
* @return NodeRef to the "surf-config" folder, or null if it does not exist yet.
*/
private NodeRef aquireSurfConfigRef(final String path, final boolean create)
{
// remap the path into the appropriate Sites or site relative folder location
// by first matching the path to appropriate user or site regex
String userId = null;
String siteName = 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);
}
else if ((matcher = SITE_PATTERN_1.matcher(path)).matches())
{
siteName = matcher.group(1);
}
else if ((matcher = SITE_PATTERN_2.matcher(path)).matches())
{
siteName = matcher.group(1);
}
NodeRef surfConfigRef;
if (userId != null)
{
if (logger.isDebugEnabled())
logger.debug("...resolved user path id: " + userId);
surfConfigRef = getSurfConfigNodeRef(getRootNodeRef(), create);
}
else if (siteName != null)
{
if (logger.isDebugEnabled())
logger.debug("...resolved site path id: " + siteName);
surfConfigRef = getSurfConfigNodeRef(getSiteNodeRef(siteName), create);
}
else
{
if (logger.isDebugEnabled())
logger.debug("...resolved to generic path.");
surfConfigRef = getSurfConfigNodeRef(getRootNodeRef(), create);
}
return surfConfigRef;
}
/**
* Return the "surf-config" noderef under the given root. No attempt will be made
* to create the node if it does not exist yet.
*
* @param rootRef Root node reference where the "surf-config" folder should live
*
* @return surf-config folder ref if found, null otherwise
*/
private NodeRef getSurfConfigNodeRef(final NodeRef rootRef)
{
return getSurfConfigNodeRef(rootRef, false);
}
/**
* Return the "surf-config" noderef under the given root. Optionally create the
* folder if it does not exist yet. NOTE: must only be set to create if within a
* WRITE transaction context.
*
* @param rootRef Root node reference where the "surf-config" folder should live
* @param create True to create the folder if missing, false otherwise
*
* @return surf-config folder ref if found, null otherwise if not creating
*/
private NodeRef getSurfConfigNodeRef(final NodeRef rootRef, final boolean create)
{
NodeRef surfConfigRef = this.unprotNodeService.getChildByName(
rootRef, ContentModel.ASSOC_CONTAINS, SURF_CONFIG);
//
// TODO: protect with RRW - keyed from rootRef?
//
if (surfConfigRef == null && create)
{
if (logger.isDebugEnabled())
logger.debug("'surf-config' system folder not found under path, creating...");
QName assocQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, SURF_CONFIG);
Map<QName, Serializable> properties = new HashMap<QName, Serializable>(1, 1.0f);
properties.put(ContentModel.PROP_NAME, (Serializable) SURF_CONFIG);
//
// TODO: change this to TYPE_SYSTEMFOLDER before commit? that won't stop config files getting found...
// suggest new model type that is not cm:content - and is NOT fts enabled...
//
ChildAssociationRef ref = this.unprotNodeService.createNode(
rootRef, ContentModel.ASSOC_CONTAINS, assocQName, ContentModel.TYPE_FOLDER, properties);
surfConfigRef = ref.getChildRef();
}
return surfConfigRef;
}
/**
* @return the Sites folder root node reference
*/
private NodeRef getRootNodeRef()
{
return this.siteService.getSiteRoot();
}
/**
* @param shortName Site shortname
*
* @return the given Site folder node reference
*/
private NodeRef getSiteNodeRef(String shortName)
{
SiteInfo siteInfo = this.siteService.getSite(shortName);
return siteInfo != null ? siteInfo.getNodeRef() : null;
}
/**
* Traverse a Node and recursively output the file paths it contains.
*
* @param out Writer for output - relative paths separated by newline characters
* @param node The FileInfo node to traverse
* @param pattern Optional pattern to match filenames against
* @param recurse True to recurse sub-directories
*
* @throws IOException
*/
private void traverseNode(Writer out, FileInfo node, String pattern, boolean recurse)
throws IOException
{
final int cropPoint = 0;//TODO:rootPath.length() + 1;
// TODO: this could be optimized a lot:
// 1. Perform the traverse and search by hand (is this faster?) using small nodeService
// 2. Keep track of parent path cm:name values so Path is not required
List<FileInfo> files = this.fileFolderService.search(getRootNodeRef(), pattern, true, false, recurse);
for (final FileInfo file : files)
{
Path path = this.unprotNodeService.getPath(file.getNodeRef());
String displayPath = path.toDisplayPath(this.unprotNodeService, new PermissionServiceNOOPImpl());
out.write(displayPath.substring(cropPoint));
out.write("\n");
}
}
}

View File

@@ -73,9 +73,6 @@ public class AVMRemoteStore extends BaseRemoteStore
private static final TransformerFactory TRANSFORMER_FACTORY = TransformerFactory.newInstance(); private static final TransformerFactory TRANSFORMER_FACTORY = TransformerFactory.newInstance();
private static ThreadLocal<Transformer> transformer = new ThreadLocal<Transformer>() private static ThreadLocal<Transformer> transformer = new ThreadLocal<Transformer>()
{ {
/* (non-Javadoc)
* @see java.lang.ThreadLocal#initialValue()
*/
@Override @Override
protected Transformer initialValue() protected Transformer initialValue()
{ {
@@ -585,103 +582,4 @@ public class AVMRemoteStore extends BaseRemoteStore
} }
} }
} }
private static String encodePath(final String s)
{
StringBuilder sb = null; //create on demand
char ch;
final int len = s.length();
for (int i = 0; i < len; i++)
{
ch = s.charAt(i);
if (('A' <= ch && ch <= 'Z') || // 'A'..'Z'
('a' <= ch && ch <= 'z') || // 'a'..'z'
('0' <= ch && ch <= '9') || // '0'..'9'
ch == '/' ||
ch == '\'' || ch == ' ' ||
ch == '.' || ch == '~' ||
ch == '-' || ch == '_' ||
ch == '@' || ch == '!' ||
ch == '(' || ch == ')' ||
ch == ';' || ch == ',' ||
ch == '+' || ch == '$')
{
if (sb != null)
{
sb.append(ch);
}
}
else if ((int)ch <= 0x007f) // other ASCII
{
if (sb == null)
{
final String soFar = s.substring(0, i);
sb = new StringBuilder(len + 16);
sb.append(soFar);
}
sb.append(hex[ch]);
}
else if ((int)ch <= 0x07FF) // non-ASCII <= 0x7FF
{
if (sb == null)
{
final String soFar = s.substring(0, i);
sb = new StringBuilder(len + 16);
sb.append(soFar);
}
sb.append(hex[0xc0 | (ch >> 6)]);
sb.append(hex[0x80 | (ch & 0x3F)]);
}
else // 0x7FF < ch <= 0xFFFF
{
if (sb == null)
{
final String soFar = s.substring(0, i);
sb = new StringBuilder(len + 16);
sb.append(soFar);
}
sb.append(hex[0xe0 | (ch >> 12)]);
sb.append(hex[0x80 | ((ch >> 6) & 0x3F)]);
sb.append(hex[0x80 | (ch & 0x3F)]);
}
}
return (sb != null ? sb.toString() : s);
}
private final static String[] hex = {
"%00", "%01", "%02", "%03", "%04", "%05", "%06", "%07",
"%08", "%09", "%0a", "%0b", "%0c", "%0d", "%0e", "%0f",
"%10", "%11", "%12", "%13", "%14", "%15", "%16", "%17",
"%18", "%19", "%1a", "%1b", "%1c", "%1d", "%1e", "%1f",
"%20", "%21", "%22", "%23", "%24", "%25", "%26", "%27",
"%28", "%29", "%2a", "%2b", "%2c", "%2d", "%2e", "%2f",
"%30", "%31", "%32", "%33", "%34", "%35", "%36", "%37",
"%38", "%39", "%3a", "%3b", "%3c", "%3d", "%3e", "%3f",
"%40", "%41", "%42", "%43", "%44", "%45", "%46", "%47",
"%48", "%49", "%4a", "%4b", "%4c", "%4d", "%4e", "%4f",
"%50", "%51", "%52", "%53", "%54", "%55", "%56", "%57",
"%58", "%59", "%5a", "%5b", "%5c", "%5d", "%5e", "%5f",
"%60", "%61", "%62", "%63", "%64", "%65", "%66", "%67",
"%68", "%69", "%6a", "%6b", "%6c", "%6d", "%6e", "%6f",
"%70", "%71", "%72", "%73", "%74", "%75", "%76", "%77",
"%78", "%79", "%7a", "%7b", "%7c", "%7d", "%7e", "%7f",
"%80", "%81", "%82", "%83", "%84", "%85", "%86", "%87",
"%88", "%89", "%8a", "%8b", "%8c", "%8d", "%8e", "%8f",
"%90", "%91", "%92", "%93", "%94", "%95", "%96", "%97",
"%98", "%99", "%9a", "%9b", "%9c", "%9d", "%9e", "%9f",
"%a0", "%a1", "%a2", "%a3", "%a4", "%a5", "%a6", "%a7",
"%a8", "%a9", "%aa", "%ab", "%ac", "%ad", "%ae", "%af",
"%b0", "%b1", "%b2", "%b3", "%b4", "%b5", "%b6", "%b7",
"%b8", "%b9", "%ba", "%bb", "%bc", "%bd", "%be", "%bf",
"%c0", "%c1", "%c2", "%c3", "%c4", "%c5", "%c6", "%c7",
"%c8", "%c9", "%ca", "%cb", "%cc", "%cd", "%ce", "%cf",
"%d0", "%d1", "%d2", "%d3", "%d4", "%d5", "%d6", "%d7",
"%d8", "%d9", "%da", "%db", "%dc", "%dd", "%de", "%df",
"%e0", "%e1", "%e2", "%e3", "%e4", "%e5", "%e6", "%e7",
"%e8", "%e9", "%ea", "%eb", "%ec", "%ed", "%ee", "%ef",
"%f0", "%f1", "%f2", "%f3", "%f4", "%f5", "%f6", "%f7",
"%f8", "%f9", "%fa", "%fb", "%fc", "%fd", "%fe", "%ff"
};
} }

View File

@@ -38,47 +38,54 @@ import org.apache.commons.logging.LogFactory;
/** /**
* Remote Store service. * Remote Store service.
* * <p>
* Responsible for providing remote HTTP based access to a store. Designed to be accessed * Responsible for providing remote HTTP based access to a store. Designed to be accessed
* from a web-tier application to remotely mirror a WebScript Store instance. * from a web-tier application to remotely mirror a WebScript Store instance.
* * <p>
* Request format: * Request format:
* <pre>
* <servicepath>/<method>/<path>[?<args>] * <servicepath>/<method>/<path>[?<args>]
* <servicepath>/<method>/s/<store>/<path>[?<args>] * <servicepath>/<method>/s/<store>/<path>[?<args>]
* <servicepath>/<method>/s/<store>/w/<webapp>/<path>[?<args>] * <servicepath>/<method>/s/<store>/w/<webapp>/<path>[?<args>]
* * </pre><p>
* Example: * Example:
* <pre>
* /service/remotestore/lastmodified/sites/xyz/pages/page.xml * /service/remotestore/lastmodified/sites/xyz/pages/page.xml
* * </pre><p>
* where: * where:
* <pre>
* /service/remotestore -> service path * /service/remotestore -> service path
* /lastmodified -> method name * /lastmodified -> method name
* /sites/../page.xml -> document path * /sites/../page.xml -> document path
* * </pre><p>
* optional request parameters: * optional request parameters:
* * <pre>
* s -> the avm store id * s -> the avm store id
* w -> the wcm web application id * w -> the wcm web application id
* * </pre><p>
* Note: path is relative to the root path as configured for this webscript bean * Note: path is relative to the root path as configured for this webscript bean
* * <p>
* Further URL arguments may be provided if required by specific API methods. * Further URL arguments may be provided if required by specific API methods.
* * <p>
* For content create and update the request should be POSTed and the content sent as the * For content create and update the request should be POSTed and the content sent as the
* payload of the request content. * payload of the request content.
* * <p>
* Supported API methods: * Supported API methods:
* GET lastmodified -> return long timestamp of a document * <pre>
* GET has -> return true/false of existence for a document * GET lastmodified -> return timestamp of a document in ms since 1970 as a long string value
* GET get -> return document content - in addition the usual HTTP headers for the * GET has -> return true or false string as existence for a document
* character encoding, content type, length and modified date will be supplied * GET get -> return raw document content - in addition the appropriate HTTP headers for the
* GET list -> return the list of available document paths under a path * character encoding, content type, length and modified date will be set
* GET list -> return the list of available document paths under a path - UTF-8 response text
* GET listall -> return the list of available document paths (recursively) under a given path * GET listall -> return the list of available document paths (recursively) under a given path
* - UTF-8 response text
* GET listpattern -> return the list of document paths matching a file pattern under a given path * GET listpattern -> return the list of document paths matching a file pattern under a given path
* - UTF-8 response text
* POST create -> create a new document with request content payload * POST create -> create a new document with request content payload
* POST createmulti -> create multiple new documents with request content payload
* POST update -> update an existing document with request content payload * POST update -> update an existing document with request content payload
* DELETE delete -> delete an existing document * DELETE delete -> delete an existing document
* * </pre>
* @author Kevin Roast * @author Kevin Roast
*/ */
public abstract class BaseRemoteStore extends AbstractWebScript public abstract class BaseRemoteStore extends AbstractWebScript
@@ -92,7 +99,6 @@ public abstract class BaseRemoteStore extends AbstractWebScript
private static final Log logger = LogFactory.getLog(BaseRemoteStore.class); private static final Log logger = LogFactory.getLog(BaseRemoteStore.class);
protected String defaultStore; protected String defaultStore;
protected ContentService contentService;
protected MimetypeService mimetypeService; protected MimetypeService mimetypeService;
@@ -104,14 +110,6 @@ public abstract class BaseRemoteStore extends AbstractWebScript
this.defaultStore = defaultStore; this.defaultStore = defaultStore;
} }
/**
* @param contentService the ContentService to set
*/
public void setContentService(ContentService contentService)
{
this.contentService = contentService;
}
/** /**
* @param mimetypeService the MimetypeService to set * @param mimetypeService the MimetypeService to set
*/ */
@@ -271,7 +269,7 @@ public abstract class BaseRemoteStore extends AbstractWebScript
case UPDATE: case UPDATE:
validatePath(path); validatePath(path);
if (logger.isDebugEnabled()) if (logger.isDebugEnabled())
logger.debug("CREATE: content length=" + httpReq.getContentLength()); logger.debug("UPDATE: content length=" + httpReq.getContentLength());
updateDocument(res, store, path, httpReq.getInputStream()); updateDocument(res, store, path, httpReq.getInputStream());
break; break;
@@ -447,4 +445,103 @@ public abstract class BaseRemoteStore extends AbstractWebScript
UPDATE, UPDATE,
DELETE DELETE
}; };
protected static String encodePath(final String s)
{
StringBuilder sb = null; //create on demand
char ch;
final int len = s.length();
for (int i = 0; i < len; i++)
{
ch = s.charAt(i);
if (('A' <= ch && ch <= 'Z') || // 'A'..'Z'
('a' <= ch && ch <= 'z') || // 'a'..'z'
('0' <= ch && ch <= '9') || // '0'..'9'
ch == '/' ||
ch == '\'' || ch == ' ' ||
ch == '.' || ch == '~' ||
ch == '-' || ch == '_' ||
ch == '@' || ch == '!' ||
ch == '(' || ch == ')' ||
ch == ';' || ch == ',' ||
ch == '+' || ch == '$')
{
if (sb != null)
{
sb.append(ch);
}
}
else if ((int)ch <= 0x007f) // other ASCII
{
if (sb == null)
{
final String soFar = s.substring(0, i);
sb = new StringBuilder(len + 16);
sb.append(soFar);
}
sb.append(hex[ch]);
}
else if ((int)ch <= 0x07FF) // non-ASCII <= 0x7FF
{
if (sb == null)
{
final String soFar = s.substring(0, i);
sb = new StringBuilder(len + 16);
sb.append(soFar);
}
sb.append(hex[0xc0 | (ch >> 6)]);
sb.append(hex[0x80 | (ch & 0x3F)]);
}
else // 0x7FF < ch <= 0xFFFF
{
if (sb == null)
{
final String soFar = s.substring(0, i);
sb = new StringBuilder(len + 16);
sb.append(soFar);
}
sb.append(hex[0xe0 | (ch >> 12)]);
sb.append(hex[0x80 | ((ch >> 6) & 0x3F)]);
sb.append(hex[0x80 | (ch & 0x3F)]);
}
}
return (sb != null ? sb.toString() : s);
}
private final static String[] hex = {
"%00", "%01", "%02", "%03", "%04", "%05", "%06", "%07",
"%08", "%09", "%0a", "%0b", "%0c", "%0d", "%0e", "%0f",
"%10", "%11", "%12", "%13", "%14", "%15", "%16", "%17",
"%18", "%19", "%1a", "%1b", "%1c", "%1d", "%1e", "%1f",
"%20", "%21", "%22", "%23", "%24", "%25", "%26", "%27",
"%28", "%29", "%2a", "%2b", "%2c", "%2d", "%2e", "%2f",
"%30", "%31", "%32", "%33", "%34", "%35", "%36", "%37",
"%38", "%39", "%3a", "%3b", "%3c", "%3d", "%3e", "%3f",
"%40", "%41", "%42", "%43", "%44", "%45", "%46", "%47",
"%48", "%49", "%4a", "%4b", "%4c", "%4d", "%4e", "%4f",
"%50", "%51", "%52", "%53", "%54", "%55", "%56", "%57",
"%58", "%59", "%5a", "%5b", "%5c", "%5d", "%5e", "%5f",
"%60", "%61", "%62", "%63", "%64", "%65", "%66", "%67",
"%68", "%69", "%6a", "%6b", "%6c", "%6d", "%6e", "%6f",
"%70", "%71", "%72", "%73", "%74", "%75", "%76", "%77",
"%78", "%79", "%7a", "%7b", "%7c", "%7d", "%7e", "%7f",
"%80", "%81", "%82", "%83", "%84", "%85", "%86", "%87",
"%88", "%89", "%8a", "%8b", "%8c", "%8d", "%8e", "%8f",
"%90", "%91", "%92", "%93", "%94", "%95", "%96", "%97",
"%98", "%99", "%9a", "%9b", "%9c", "%9d", "%9e", "%9f",
"%a0", "%a1", "%a2", "%a3", "%a4", "%a5", "%a6", "%a7",
"%a8", "%a9", "%aa", "%ab", "%ac", "%ad", "%ae", "%af",
"%b0", "%b1", "%b2", "%b3", "%b4", "%b5", "%b6", "%b7",
"%b8", "%b9", "%ba", "%bb", "%bc", "%bd", "%be", "%bf",
"%c0", "%c1", "%c2", "%c3", "%c4", "%c5", "%c6", "%c7",
"%c8", "%c9", "%ca", "%cb", "%cc", "%cd", "%ce", "%cf",
"%d0", "%d1", "%d2", "%d3", "%d4", "%d5", "%d6", "%d7",
"%d8", "%d9", "%da", "%db", "%dc", "%dd", "%de", "%df",
"%e0", "%e1", "%e2", "%e3", "%e4", "%e5", "%e6", "%e7",
"%e8", "%e9", "%ea", "%eb", "%ec", "%ed", "%ee", "%ef",
"%f0", "%f1", "%f2", "%f3", "%f4", "%f5", "%f6", "%f7",
"%f8", "%f9", "%fa", "%fb", "%fc", "%fd", "%fe", "%ff"
};
} }