diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/solr/getNodes.get.desc.xml b/config/alfresco/templates/webscripts/org/alfresco/repository/solr/getNodes.get.desc.xml new file mode 100644 index 0000000000..e13345fe5d --- /dev/null +++ b/config/alfresco/templates/webscripts/org/alfresco/repository/solr/getNodes.get.desc.xml @@ -0,0 +1,8 @@ + + Get the nodes in the given transactions + Get the nodes updated/deleted in the given transactions. + /api/solr/nodes?txnIds={txnIds}&{fromNodeId?}&{toNodeId?}&{count?} + argument + admin + required + \ No newline at end of file diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/solr/getNodes.get.json.ftl b/config/alfresco/templates/webscripts/org/alfresco/repository/solr/getNodes.get.json.ftl new file mode 100644 index 0000000000..326c334e6c --- /dev/null +++ b/config/alfresco/templates/webscripts/org/alfresco/repository/solr/getNodes.get.json.ftl @@ -0,0 +1,11 @@ +<#import "solr.lib.ftl" as solrLib/> +{ + "nodes" : + [ + <#list nodes as node> + <@solrLib.nodeJSON node=node/> + <#if node_has_next>, + + ], + "count": ${count} +} \ No newline at end of file diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/solr/getTransactions.get.desc.xml b/config/alfresco/templates/webscripts/org/alfresco/repository/solr/getTransactions.get.desc.xml new file mode 100644 index 0000000000..571da798db --- /dev/null +++ b/config/alfresco/templates/webscripts/org/alfresco/repository/solr/getTransactions.get.desc.xml @@ -0,0 +1,8 @@ + + Get transactions + Get the transactions from the given commit time. + /api/solr/transactions?fromCommitTime={fromCommitTime} + argument + admin + required + \ No newline at end of file diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/solr/getTransactions.get.json.ftl b/config/alfresco/templates/webscripts/org/alfresco/repository/solr/getTransactions.get.json.ftl new file mode 100644 index 0000000000..8adfc6aa3d --- /dev/null +++ b/config/alfresco/templates/webscripts/org/alfresco/repository/solr/getTransactions.get.json.ftl @@ -0,0 +1,10 @@ +<#import "solr.lib.ftl" as solrLib/> +{ + "transactions" : + [ + <#list transactions as txn> + <@solrLib.transactionJSON txn=txn/> + <#if txn_has_next>, + + ] +} \ No newline at end of file diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/solr/solr.lib.ftl b/config/alfresco/templates/webscripts/org/alfresco/repository/solr/solr.lib.ftl new file mode 100644 index 0000000000..552aa77c9a --- /dev/null +++ b/config/alfresco/templates/webscripts/org/alfresco/repository/solr/solr.lib.ftl @@ -0,0 +1,16 @@ +<#macro transactionJSON txn> +{ + "id": "${txn.id?c}", + "commitTimeMs": "${txn.commitTimeMs?c}", + "updates": "${txn.updates?c}", + "deletes": "${txn.deletes?c}" +} + + +<#macro nodeJSON node> +{ + "nodeID": "${node.id?c}", + "txnID": "${node.transaction.id?c}", + "deleted": "${node.deleted?string}" +} + \ No newline at end of file diff --git a/config/alfresco/web-scripts-application-context.xml b/config/alfresco/web-scripts-application-context.xml index 2fce714baa..329290e6e3 100644 --- a/config/alfresco/web-scripts-application-context.xml +++ b/config/alfresco/web-scripts-application-context.xml @@ -1176,4 +1176,21 @@ + + + + + + + + + + + + + diff --git a/source/java/org/alfresco/repo/web/scripts/solr/GetNodes.java b/source/java/org/alfresco/repo/web/scripts/solr/GetNodes.java new file mode 100644 index 0000000000..da1b403a1f --- /dev/null +++ b/source/java/org/alfresco/repo/web/scripts/solr/GetNodes.java @@ -0,0 +1,155 @@ +package org.alfresco.repo.web.scripts.solr; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.repo.domain.node.Node; +import org.alfresco.repo.domain.solr.NodeParameters; +import org.alfresco.repo.domain.solr.SOLRDAO; +import org.alfresco.repo.domain.solr.SOLRDAO.NodeQueryCallback; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Support for SOLR. Get a list of nodes in the given transactions. + * + * Supports fromNodeId, toNodeId, count (all optional) to control the number of nodes returned + * e.g. (null, null, 1000) will return at most 1000 nodes starting from the first node in the first transaction. + * e.g. (1234, null, 1000) will return at most 1000 nodes starting from the node id 1234. + * + * @since 4.0 + */ +public class GetNodes extends DeclarativeWebScript +{ + protected static final Log logger = LogFactory.getLog(GetNodes.class); + + private SOLRDAO solrDAO; + + /** + * @param solrDAO the solrDAO to set + */ + public void setSolrDAO(SOLRDAO solrDAO) + { + this.solrDAO = solrDAO; + } + + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.Status) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status) + { + String txnIdsString = req.getParameter("txnIds"); + if(txnIdsString == null) + { + throw new WebScriptException("txnIds parameter is required for GetNodes"); + } + + String param = req.getParameter("fromNodeId"); + Long fromNodeId = (param == null ? null : Long.valueOf(param)); + + param = req.getParameter("toNodeId"); + Long toNodeId = (param == null ? null : Long.valueOf(param)); + + param = req.getParameter("excludeAspects"); + Set excludeAspects = null; + if(param != null) + { + String[] excludeAspectsStrings = param.split(","); + excludeAspects = new HashSet(excludeAspectsStrings.length); + for(String excludeAspect : excludeAspectsStrings) + { + excludeAspects.add(QName.createQName(excludeAspect.trim())); + } + } + + param = req.getParameter("includeAspects"); + Set includeAspects = null; + if(param != null) + { + String[] includeAspectsStrings = param.split(","); + includeAspects = new HashSet(includeAspectsStrings.length); + for(String includeAspect : includeAspectsStrings) + { + includeAspects.add(QName.createQName(includeAspect.trim())); + } + } + + param = req.getParameter("maxResults"); + int maxResults = (param == null ? 0 : Integer.valueOf(param)); + + param = req.getParameter("storeProtocol"); + String storeProtocol = param; + + param = req.getParameter("storeIdentifier"); + String storeIdentifier = param; + + String[] txnIdStrings = txnIdsString.split(","); + List txnIds = new ArrayList(txnIdStrings.length); + for(String txnIdString : txnIdStrings) + { + txnIds.add(Long.valueOf(txnIdString.trim())); + } + + WebNodeQueryCallback nodeQueryCallback = new WebNodeQueryCallback(maxResults); + NodeParameters nodeParameters = new NodeParameters(); + nodeParameters.setTransactionIds(txnIds); + nodeParameters.setFromNodeId(fromNodeId); + nodeParameters.setToNodeId(toNodeId); + nodeParameters.setExcludeAspects(excludeAspects); + nodeParameters.setIncludeAspects(includeAspects); + nodeParameters.setStoreProtocol(storeProtocol); + nodeParameters.setStoreIdentifier(storeIdentifier); + solrDAO.getNodes(nodeParameters, maxResults, nodeQueryCallback); + + Map model = new HashMap(2, 1.0f); + List nodes = nodeQueryCallback.getNodes(); + model.put("nodes", nodes); + model.put("count", nodes.size()); + + if (logger.isDebugEnabled()) + { + logger.debug("Result: \n\tRequest: " + req + "\n\tModel: " + model); + } + + return model; + } + + /** + * + * + */ + private static class WebNodeQueryCallback implements NodeQueryCallback + { + private ArrayList nodes; + + public WebNodeQueryCallback(int count) { + super(); + nodes = new ArrayList(count == 0 || count == Integer.MAX_VALUE ? 100 : count); + } + + @Override + public boolean handleNode(Node node) { + nodes.add(node); + + // continue - get next node + return true; + } + + public List getNodes() + { + return nodes; + } + } + +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/web/scripts/solr/GetTransactions.java b/source/java/org/alfresco/repo/web/scripts/solr/GetTransactions.java new file mode 100644 index 0000000000..7be5548aa3 --- /dev/null +++ b/source/java/org/alfresco/repo/web/scripts/solr/GetTransactions.java @@ -0,0 +1,63 @@ +package org.alfresco.repo.web.scripts.solr; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.repo.domain.solr.SOLRDAO; +import org.alfresco.repo.domain.solr.Transaction; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Support for SOLR. Get a list of transactions with a commit time greater than or equal to the given parameter. + * + * @since 4.0 + */ +public class GetTransactions extends DeclarativeWebScript +{ + protected static final Log logger = LogFactory.getLog(GetTransactions.class); + + private SOLRDAO solrDAO; + + /** + * @param solrDAO the SOLDAO to set + */ + public void setSolrDAO(SOLRDAO solrDAO) + { + this.solrDAO = solrDAO; + } + + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.Status) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status) + { + String minTxnIdParam = req.getParameter("minTxnId"); + String fromCommitTimeParam = req.getParameter("fromCommitTime"); + String maxResultsParam = req.getParameter("maxResults"); + + Long minTxnId = (minTxnIdParam == null ? null : Long.valueOf(minTxnIdParam)); + Long fromCommitTime = (fromCommitTimeParam == null ? null : Long.valueOf(fromCommitTimeParam)); + int maxResults = (maxResultsParam == null ? 0 : Integer.valueOf(maxResultsParam)); + + List transactions = solrDAO.getTransactions(minTxnId, fromCommitTime, maxResults); + + Map model = new HashMap(1, 1.0f); + model.put("transactions", transactions); + + if (logger.isDebugEnabled()) + { + logger.debug("Result: \n\tRequest: " + req + "\n\tModel: " + model); + } + + return model; + } + +} diff --git a/source/java/org/alfresco/repo/web/scripts/solr/SOLRWebScriptTest.java b/source/java/org/alfresco/repo/web/scripts/solr/SOLRWebScriptTest.java new file mode 100644 index 0000000000..839179cd10 --- /dev/null +++ b/source/java/org/alfresco/repo/web/scripts/solr/SOLRWebScriptTest.java @@ -0,0 +1,1043 @@ +package org.alfresco.repo.web.scripts.solr; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.domain.node.NodeDAO; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.repo.web.scripts.BaseWebScriptTest; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.Pair; +import org.alfresco.util.PropertyMap; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.context.ApplicationContext; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.TestWebScriptServer; +import org.springframework.extensions.webscripts.TestWebScriptServer.Response; + +/** + * Test the SOLR web scripts + * + * @since 4.0 + */ +// TODO check txn ids are correct - how to get txn ids? +// TODO create a node in txn1 and delete it in txn2 - only the delete in txn2 appears. Will SOLR not then see this as deletion of a non-existent node? +// TODO test getTxns: combinations of fromTxnId, fromCommitTime, maxResults +// TODO move/duplicate tests to SOLRDAO tests +public class SOLRWebScriptTest extends BaseWebScriptTest +{ + protected static final Log logger = LogFactory.getLog(SOLRWebScriptTest.class); + + private ApplicationContext ctx; + private NodeDAO nodeDAO; + private TransactionService transactionService; + private NodeService nodeService; + private ContentService contentService; + private FileFolderService fileFolderService; + private RetryingTransactionHelper txnHelper; + + private String admin; + + private StoreRef storeRef; + private StoreRef storeRef1; + private NodeRef rootNodeRef; + private NodeRef rootNodeRef1; + + private NodeRef container1; + private NodeRef container2; + private NodeRef container3; + private NodeRef container4; + private NodeRef container5; + private NodeRef content1; + private NodeRef content2; + private NodeRef content3; + + private long container1NodeID; + private long container2NodeID; + private long container3NodeID; + private long container4NodeID; + private long container5NodeID; + private long content1NodeID; + private long content2NodeID; + private long content3NodeID; + private long content4NodeID; + private long content5NodeID; + + private JSONObject firstNode; + private JSONObject secondNode; + private JSONObject thirdNode; + + private NodeRef[][] contents = new NodeRef[10][100]; + private Long[][] nodeIDs = new Long[10][100]; + + @Override + protected void setUp() throws Exception + { + super.setUp(); + ctx = getServer().getApplicationContext(); + + ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY); + transactionService = serviceRegistry.getTransactionService(); + nodeService = serviceRegistry.getNodeService(); + contentService = serviceRegistry.getContentService(); + fileFolderService = serviceRegistry.getFileFolderService(); + txnHelper = transactionService.getRetryingTransactionHelper(); + nodeDAO = (NodeDAO)ctx.getBean("nodeDAO"); + + admin = AuthenticationUtil.getAdminUserName(); + + AuthenticationUtil.setFullyAuthenticatedUser(admin); + + storeRef = nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, getName() + ".1." + System.currentTimeMillis()); + rootNodeRef = nodeService.getRootNode(storeRef); + + storeRef1 = nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, getName() + ".2." + System.currentTimeMillis()); + rootNodeRef1 = nodeService.getRootNode(storeRef1); + } + + @Override + protected void tearDown() throws Exception + { + super.tearDown(); + } + + private JSONArray getNodes(GetNodesParameters parameters, Integer maxResults) throws Exception + { + StringBuilder url = new StringBuilder("/api/solr/nodes?txnIds="); + url.append(join(parameters.getTransactionIds(), ",")); + if(parameters.getFromNodeId() != null) + { + url.append("&fromNodeId="); + url.append(parameters.getFromNodeId()); + } + if(parameters.getToNodeId() != null) + { + url.append("&toNodeId="); + url.append(parameters.getToNodeId()); + } + if(parameters.getExcludeAspects() != null) + { + url.append("&excludeAspects="); + Set excludeAspects = parameters.getExcludeAspects(); + int i = 0; + for(QName excludeAspect : excludeAspects) + { + url.append(excludeAspect.toString()); + if(i < (excludeAspects.size() - 1)) + { + url.append(","); + } + i++; + } + } + if(parameters.getIncludeAspects() != null) + { + url.append("&includeAspects="); + Set includeAspects = parameters.getIncludeAspects(); + int i = 0; + for(QName includeAspect : includeAspects) + { + url.append(includeAspect.toString()); + if(i < (includeAspects.size() - 1)) + { + url.append(","); + } + i++; + } + } + if(parameters.getStoreProtocol() != null) + { + url.append("&storeProtocol="); + url.append(parameters.getStoreProtocol()); + } + if(parameters.getStoreIdentifier() != null) + { + url.append("&storeIdentifier="); + url.append(parameters.getStoreIdentifier()); + } + if(maxResults != null) + { + url.append("&maxResults="); + url.append(maxResults); + } + + TestWebScriptServer.GetRequest req = new TestWebScriptServer.GetRequest(url.toString()); + Response response = sendRequest(req, Status.STATUS_OK, admin); + +// assertEquals("Expected application/json content type", "application/json[;charset=UTF-8]", response.getContentType()); + + if(logger.isDebugEnabled()) + { + logger.debug(response.getContentAsString()); + } + + JSONObject json = new JSONObject(response.getContentAsString()); + json.write(new PrintWriter(System.out)); + + JSONArray nodes = json.getJSONArray("nodes"); + + assertEquals("Node count is incorrect", nodes.length(), json.getInt("count")); + + return nodes; + } + + private void buildTransactions1() + { + txnHelper.doInTransaction(new RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + PropertyMap props = new PropertyMap(); + props.put(ContentModel.PROP_NAME, "Container1"); + container1 = nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN, + ContentModel.TYPE_FOLDER, + props).getChildRef(); + + System.out.println("container1 = " + container1); + + FileInfo content1Info = fileFolderService.create(container1, "Content1", ContentModel.TYPE_CONTENT); + content1 = content1Info.getNodeRef(); + + container1NodeID = getNodeID(container1); + content1NodeID = getNodeID(content1); + + if(logger.isDebugEnabled()) + { + logger.debug("content1 = " + content1); + } + + return null; + } + }); + + txnHelper.doInTransaction(new RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + FileInfo content2Info = fileFolderService.create(container1, "Content2", ContentModel.TYPE_CONTENT); + content2 = content2Info.getNodeRef(); + content2NodeID = getNodeID(content2); + + if(logger.isDebugEnabled()) + { + logger.debug("content2 = " + content2); + } + + nodeService.addAspect(content1, ContentModel.ASPECT_TEMPORARY, null); + fileFolderService.delete(content1); + + return null; + } + }); + } + + private List getTransactionIds(JSONArray transactions) throws JSONException + { + List txnIds = new ArrayList(transactions.length()); + + int numTxns = transactions.length(); + for(int i = 0; i < numTxns; i++) + { + JSONObject txn = transactions.getJSONObject(i); + txnIds.add(txn.getLong("id")); + } + + return txnIds; + } + + public static String join(Collection s, String delimiter) { + StringBuffer buffer = new StringBuffer(); + Iterator iter = s.iterator(); + while (iter.hasNext()) { + buffer.append(iter.next()); + if (iter.hasNext()) { + buffer.append(delimiter); + } + } + return buffer.toString(); + } + + public void testGetTransactions1() throws Exception + { + long fromCommitTime = System.currentTimeMillis(); + + buildTransactions1(); + + String url = "/api/solr/transactions?fromCommitTime=" + fromCommitTime; + TestWebScriptServer.GetRequest req = new TestWebScriptServer.GetRequest(url); + Response response = sendRequest(req, Status.STATUS_OK, admin); + +// assertEquals("Expected application/json content type", "application/json[;charset=UTF-8]", response.getContentType()); + + System.out.println(response.getContentAsString()); + JSONObject json = new JSONObject(response.getContentAsString()); + + JSONArray transactions = json.getJSONArray("transactions"); + assertEquals("Number of transactions is incorrect", 2, transactions.length()); + + int[] updates = new int[] {1, 1}; + int[] deletes = new int[] {0, 1}; + StringBuilder txnIds = new StringBuilder(); + int numTxns = transactions.length(); + List transactionIds = getTransactionIds(transactions); + for(int i = 0; i < numTxns; i++) + { + JSONObject txn = transactions.getJSONObject(i); + assertEquals("Number of deletes is incorrect", deletes[i], txn.getLong("deletes")); + assertEquals("Number of updates is incorrect", updates[i], txn.getLong("updates")); + +// txnIds.append(txn.getString("id")); +// if(i < (numTxns - 1)) +// { +// txnIds.append(","); +// } + } + + // get all nodes at once + if(logger.isDebugEnabled()) + { + logger.debug("txnIds = " + txnIds.toString()); + } + + GetNodesParameters parameters = new GetNodesParameters(); + parameters.setTransactionIds(transactionIds); + JSONArray nodes = getNodes(parameters, null); + if(logger.isDebugEnabled()) + { + logger.debug("nodes:"); + logger.debug(nodes.toString(3)); + } + assertEquals("Number of nodes is incorrect", 3, nodes.length()); + + JSONObject lastNode = nodes.getJSONObject(2); + assertTrue("nodeID is missing", lastNode.has("nodeID")); + Long fromNodeId = lastNode.getLong("nodeID"); + if(logger.isDebugEnabled()) + { + logger.debug("fromNodeId = " + fromNodeId); + } + assertNotNull("Unexpected null fromNodeId", fromNodeId); + + firstNode = nodes.getJSONObject(0); + secondNode = nodes.getJSONObject(1); + //assertEquals("Expected transaction ids to be the same", firstNode.getLong("txnID") == secondNode.getLong("txnID")); + assertEquals("Expected node update", false, firstNode.getBoolean("deleted")); + assertEquals("Expected node deleted", true, secondNode.getBoolean("deleted")); + assertEquals("Node id is incorrect", container1NodeID, firstNode.getLong("nodeID")); + assertEquals("Node id is incorrect", content1NodeID, secondNode.getLong("nodeID")); + + // get first 2 nodes + parameters = new GetNodesParameters(); + parameters.setTransactionIds(transactionIds); + nodes = getNodes(parameters, 2); + if(logger.isDebugEnabled()) + { + logger.debug("nodes:"); + logger.debug(nodes.toString(3)); + } + assertEquals("Number of nodes is incorrect", 2, nodes.length()); + + lastNode = nodes.getJSONObject(1); + assertTrue("nodeID is missing", lastNode.has("nodeID")); + fromNodeId = lastNode.getLong("nodeID"); + if(logger.isDebugEnabled()) + { + logger.debug("fromNodeId = " + fromNodeId); + } + assertNotNull("Unexpected null fromNodeId", fromNodeId); + + // get 4 nodes starting with fromNodeId, should return only 2 nodes (including fromNodeId) + parameters = new GetNodesParameters(); + parameters.setTransactionIds(transactionIds); + parameters.setFromNodeId(fromNodeId); + nodes = getNodes(parameters, 4); + if(logger.isDebugEnabled()) + { + logger.debug("nodes:"); + logger.debug(nodes.toString(3)); + } + assertEquals("Number of nodes is incorrect", 2, nodes.length()); + + firstNode = nodes.getJSONObject(0); + secondNode = nodes.getJSONObject(1); + assertEquals("Expected node deleted", true, firstNode.getBoolean("deleted")); + assertEquals("Expected node updated", false, secondNode.getBoolean("deleted")); + assertEquals("Node id is incorrect", content1NodeID, firstNode.getLong("nodeID")); + assertEquals("Node id is incorrect", content2NodeID, secondNode.getLong("nodeID")); + + // get 0 (all) nodes starting with fromNodeId, should return 2 nodes + parameters = new GetNodesParameters(); + parameters.setTransactionIds(transactionIds); + parameters.setFromNodeId(fromNodeId); + nodes = getNodes(parameters, 0); + if(logger.isDebugEnabled()) + { + logger.debug("nodes:"); + logger.debug(nodes.toString(3)); + } + assertEquals("Number of nodes is incorrect", 2, nodes.length()); + + // get 2 nodes ending with toNodeId, should return 2 nodes + long toNodeId = content2NodeID; + parameters = new GetNodesParameters(); + parameters.setTransactionIds(transactionIds); + parameters.setToNodeId(toNodeId); + nodes = getNodes(parameters, 2); + if(logger.isDebugEnabled()) + { + logger.debug("nodes:"); + logger.debug(nodes.toString(3)); + } + assertEquals("Number of nodes is incorrect", 2, nodes.length()); + + firstNode = nodes.getJSONObject(0); + secondNode = nodes.getJSONObject(1); + assertEquals("Expected node deleted", false, firstNode.getBoolean("deleted")); + assertEquals("Node id is incorrect", container1NodeID, firstNode.getLong("nodeID")); + assertEquals("Expected node updated", true, secondNode.getBoolean("deleted")); + assertEquals("Node id is incorrect", content1NodeID, secondNode.getLong("nodeID")); + + // get 1 node ending with toNodeId, should return 1 nodes + toNodeId = content2NodeID; + parameters = new GetNodesParameters(); + parameters.setTransactionIds(transactionIds); + parameters.setToNodeId(toNodeId); + nodes = getNodes(parameters, 1); + if(logger.isDebugEnabled()) + { + logger.debug("nodes:"); + logger.debug(nodes.toString(3)); + } + assertEquals("Number of nodes is incorrect", 1, nodes.length()); + + firstNode = nodes.getJSONObject(0); + assertEquals("Expected node updated", false, firstNode.getBoolean("deleted")); + assertEquals("Node id is incorrect", container1NodeID, firstNode.getLong("nodeID")); + + // get 3 nodes ending with toNodeId, should return 3 nodes + toNodeId = content2NodeID; + parameters = new GetNodesParameters(); + parameters.setTransactionIds(transactionIds); + parameters.setToNodeId(toNodeId); + nodes = getNodes(parameters, 3); + if(logger.isDebugEnabled()) + { + logger.debug("nodes:"); + logger.debug(nodes.toString(3)); + } + assertEquals("Number of nodes is incorrect", 3, nodes.length()); + + firstNode = nodes.getJSONObject(0); + assertEquals("Expected node updated", false, firstNode.getBoolean("deleted")); + assertEquals("Node id is incorrect", container1NodeID, firstNode.getLong("nodeID")); + + secondNode = nodes.getJSONObject(1); + assertEquals("Expected node deleted", true, secondNode.getBoolean("deleted")); + assertEquals("Node id is incorrect", content1NodeID, secondNode.getLong("nodeID")); + + thirdNode = nodes.getJSONObject(2); + assertEquals("Expected node updated", false, thirdNode.getBoolean("deleted")); + assertEquals("Node id is incorrect", content2NodeID, thirdNode.getLong("nodeID")); + } + + private void buildTransactions2() + { + txnHelper.doInTransaction(new RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + PropertyMap props = new PropertyMap(); + props.put(ContentModel.PROP_NAME, "Container2"); + container2 = nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN, + ContentModel.TYPE_FOLDER, + props).getChildRef(); + container2NodeID = getNodeID(container2); + if(logger.isDebugEnabled()) + { + logger.debug("container2 = " + container2); + } + + FileInfo content1Info = fileFolderService.create(container2, "Content1", ContentModel.TYPE_CONTENT); + content1 = content1Info.getNodeRef(); + content1NodeID = getNodeID(content1); + + if(logger.isDebugEnabled()) + { + logger.debug("content1 = " + content1); + } + + FileInfo content2Info = fileFolderService.create(container2, "Content2", ContentModel.TYPE_CONTENT); + content2 = content2Info.getNodeRef(); + content2NodeID = getNodeID(content2); + if(logger.isDebugEnabled()) + { + logger.debug("content2 = " + content2); + } + + return null; + } + }); + + txnHelper.doInTransaction(new RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + FileInfo content3Info = fileFolderService.create(container2, "Content3", ContentModel.TYPE_CONTENT); + content3 = content3Info.getNodeRef(); + content3NodeID = getNodeID(content3); + if(logger.isDebugEnabled()) + { + logger.debug("content3 = " + content3); + } + + nodeService.addAspect(content1, ContentModel.ASPECT_TEMPORARY, null); + fileFolderService.delete(content1); + + nodeService.setProperty(content3, ContentModel.PROP_NAME, "Content 3 New Name"); + + return null; + } + }); + } + + public void testGetTransactions2() throws Exception + { + long fromCommitTime = System.currentTimeMillis(); + + buildTransactions2(); + + String url = "/api/solr/transactions?fromCommitTime=" + fromCommitTime; + TestWebScriptServer.GetRequest req = new TestWebScriptServer.GetRequest(url); + Response response = sendRequest(req, Status.STATUS_OK, admin); + + if(logger.isDebugEnabled()) + { + logger.debug(response.getContentAsString()); + } + JSONObject json = new JSONObject(response.getContentAsString()); + + JSONArray transactions = json.getJSONArray("transactions"); + assertEquals("Number of transactions is incorrect", 2, transactions.length()); + + // first txn has 2 updates rather than three because content1 is deleted in txn 2 and therefore + // "belongs" to that txn (because txn2 was the last to alter the node) + int[] updates = new int[] {2, 1}; + int[] deletes = new int[] {0, 1}; + StringBuilder txnIds = new StringBuilder(); + int numTxns = transactions.length(); + for(int i = 0; i < numTxns; i++) + { + JSONObject txn = transactions.getJSONObject(i); + assertEquals("Number of deletes is incorrect", deletes[i], txn.getLong("deletes")); + assertEquals("Number of updates is incorrect", updates[i], txn.getLong("updates")); + + txnIds.append(txn.getString("id")); + if(i < (numTxns - 1)) + { + txnIds.append(","); + } + } + + // get all nodes at once + if(logger.isDebugEnabled()) + { + logger.debug("txnIds = " + txnIds.toString()); + } + + List transactionIds = getTransactionIds(transactions); + + // get all nodes in the txns + GetNodesParameters parameters = new GetNodesParameters(); + parameters.setTransactionIds(transactionIds); + JSONArray nodes = getNodes(parameters, null); + assertEquals("Number of nodes is incorrect", 4, nodes.length()); + JSONObject lastNode = nodes.getJSONObject(nodes.length() - 1); + Long fromNodeId = lastNode.getLong("nodeID"); + assertNotNull("Unexpected null fromNodeId", fromNodeId); + + // get first 2 nodes + parameters = new GetNodesParameters(); + parameters.setTransactionIds(transactionIds); + nodes = getNodes(parameters, 2); + if(logger.isDebugEnabled()) + { + logger.debug("nodes:"); + logger.debug(nodes.toString(3)); + } + assertEquals("Number of nodes is incorrect", 2, nodes.length()); + + firstNode = nodes.getJSONObject(0); + assertTrue("nodeID is missing", firstNode.has("nodeID")); + secondNode = nodes.getJSONObject(1); + assertTrue("nodeID is missing", secondNode.has("nodeID")); + fromNodeId = secondNode.getLong("nodeID"); + if(logger.isDebugEnabled()) + { + logger.debug("fromNodeId = " + fromNodeId); + } + assertNotNull("Unexpected null nodeID", fromNodeId); + + //assertEquals("Expected transaction ids to be the same", firstNode.getLong("txnID"), secondNode.getLong("txnID")); + assertEquals("Expected node update", false, firstNode.getBoolean("deleted")); + assertEquals("Expected node delete", true, secondNode.getBoolean("deleted")); + assertEquals("Incorrect node id", container2NodeID, firstNode.getLong("nodeID")); + assertEquals("Incorrect node id", content1NodeID, secondNode.getLong("nodeID")); + + // get 10 nodes (including fromNodeId) starting with fromNodeId, should return only 3 nodes + parameters = new GetNodesParameters(); + parameters.setTransactionIds(transactionIds); + parameters.setFromNodeId(fromNodeId); + nodes = getNodes(parameters, 10); + assertEquals("Number of nodes is incorrect", 3, nodes.length()); + + firstNode = nodes.getJSONObject(0); + assertTrue("nodeID is missing", firstNode.has("nodeID")); + secondNode = nodes.getJSONObject(1); + assertTrue("nodeID is missing", secondNode.has("nodeID")); + thirdNode = nodes.getJSONObject(2); + assertTrue("nodeID is missing", thirdNode.has("nodeID")); + + assertEquals("Expected node delete", true, firstNode.getBoolean("deleted")); + assertEquals("Expected node update", false, secondNode.getBoolean("deleted")); + assertEquals("Expected node update", false, thirdNode.getBoolean("deleted")); + assertEquals("Incorrect node id", content1NodeID, firstNode.getLong("nodeID")); + assertEquals("Incorrect node id", content2NodeID, secondNode.getLong("nodeID")); + assertEquals("Incorrect node id", content3NodeID, thirdNode.getLong("nodeID")); + + // test with from and to node ids + parameters = new GetNodesParameters(); + parameters.setTransactionIds(transactionIds); + parameters.setFromNodeId(container2NodeID); + parameters.setToNodeId(content3NodeID); + nodes = getNodes(parameters, 2); + assertEquals("Number of nodes is incorrect", 2, nodes.length()); + + firstNode = nodes.getJSONObject(0); + assertTrue("nodeID is missing", firstNode.has("nodeID")); + secondNode = nodes.getJSONObject(1); + assertTrue("nodeID is missing", secondNode.has("nodeID")); + assertEquals("Incorrect node id", container2NodeID, firstNode.getLong("nodeID")); + assertEquals("Incorrect node id", content1NodeID, secondNode.getLong("nodeID")); + + // test right truncation + parameters = new GetNodesParameters(); + parameters.setTransactionIds(transactionIds); + parameters.setToNodeId(content3NodeID); + nodes = getNodes(parameters, 2); + assertEquals("Number of nodes is incorrect", 2, nodes.length()); + + firstNode = nodes.getJSONObject(0); + assertTrue("nodeID is missing", firstNode.has("nodeID")); + secondNode = nodes.getJSONObject(1); + assertTrue("nodeID is missing", secondNode.has("nodeID")); + assertEquals("Incorrect node id", container2NodeID, firstNode.getLong("nodeID")); + assertEquals("Incorrect node id", content1NodeID, secondNode.getLong("nodeID")); + + // test left truncation, specifying from node only + parameters = new GetNodesParameters(); + parameters.setTransactionIds(transactionIds); + parameters.setFromNodeId(container2NodeID); + nodes = getNodes(parameters, 2); + assertEquals("Number of nodes is incorrect", 2, nodes.length()); + + firstNode = nodes.getJSONObject(0); + assertTrue("nodeID is missing", firstNode.has("nodeID")); + secondNode = nodes.getJSONObject(1); + assertTrue("nodeID is missing", secondNode.has("nodeID")); + assertEquals("Incorrect node id", container2NodeID, firstNode.getLong("nodeID")); + assertEquals("Incorrect node id", content1NodeID, secondNode.getLong("nodeID")); + } + + private void buildTransactions3() + { + txnHelper.doInTransaction(new RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + PropertyMap props = new PropertyMap(); + props.put(ContentModel.PROP_NAME, "Container3"); + container3 = nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN, + ContentModel.TYPE_FOLDER, + props).getChildRef(); + container3NodeID = getNodeID(container3); + if(logger.isDebugEnabled()) + { + logger.debug("container3 = " + container3); + } + + for(int i = 0; i < 100; i++) + { + FileInfo content1Info = fileFolderService.create(container3, "Content" + i, ContentModel.TYPE_CONTENT); + contents[3][i] = content1Info.getNodeRef(); + nodeIDs[3][i] = getNodeID(contents[3][i]); + + if(i % 2 == 1) + { + nodeService.addAspect(contents[3][i], ContentModel.ASPECT_TEMPORARY, null); + } + } + + return null; + } + }); + } + + public void testGetNodesExcludeAspects() throws Exception + { + long fromCommitTime = System.currentTimeMillis(); + + buildTransactions3(); + + String url = "/api/solr/transactions?fromCommitTime=" + fromCommitTime; + TestWebScriptServer.GetRequest req = new TestWebScriptServer.GetRequest(url); + Response response = sendRequest(req, Status.STATUS_OK, admin); + + if(logger.isDebugEnabled()) + { + logger.debug(response.getContentAsString()); + } + JSONObject json = new JSONObject(response.getContentAsString()); + + JSONArray transactions = json.getJSONArray("transactions"); + assertEquals("Number of transactions is incorrect", 1, transactions.length()); + + // first txn has 2 updates rather than three because content1 is deleted in txn 2 and therefore + // "belongs" to that txn (because txn2 was the last to alter the node) + + List transactionIds = getTransactionIds(transactions); + + Set excludeAspects = new HashSet(1); + excludeAspects.add(ContentModel.ASPECT_TEMPORARY); + + // get all nodes, exclude nodes with temporary aspect + GetNodesParameters parameters = new GetNodesParameters(); + parameters.setTransactionIds(transactionIds); + parameters.setExcludeAspects(excludeAspects); + JSONArray nodes = getNodes(parameters, 0); + assertEquals("Number of nodes is incorrect", 51, nodes.length()); + } + + private void buildTransactions4() + { + txnHelper.doInTransaction(new RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + PropertyMap props = new PropertyMap(); + props.put(ContentModel.PROP_NAME, "Container4"); + container4 = nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN, + ContentModel.TYPE_FOLDER, + props).getChildRef(); + container4NodeID = getNodeID(container4); + if(logger.isDebugEnabled()) + { + logger.debug("container4 = " + container4); + } + + for(int i = 0; i < 100; i++) + { + FileInfo content1Info = fileFolderService.create(container4, "Content" + i, ContentModel.TYPE_CONTENT); + contents[4][i] = content1Info.getNodeRef(); + nodeIDs[4][i] = getNodeID(contents[4][i]); + + if(i % 2 == 1) + { + nodeService.addAspect(contents[4][i], ContentModel.ASPECT_TEMPORARY, null); + } + } + + return null; + } + }); + + txnHelper.doInTransaction(new RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + PropertyMap props = new PropertyMap(); + props.put(ContentModel.PROP_NAME, "Container5"); + container5 = nodeService.createNode( + rootNodeRef1, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN, + ContentModel.TYPE_FOLDER, + props).getChildRef(); + container5NodeID = getNodeID(container5); + if(logger.isDebugEnabled()) + { + logger.debug("container5 = " + container5); + } + + for(int i = 0; i < 100; i++) + { + FileInfo content1Info = fileFolderService.create(container5, "Content" + i, ContentModel.TYPE_CONTENT); + contents[5][i] = content1Info.getNodeRef(); + nodeIDs[5][i] = getNodeID(contents[5][i]); + + if(i % 2 == 1) + { + nodeService.addAspect(contents[5][i], ContentModel.ASPECT_TEMPORARY, null); + } + } + + return null; + } + }); + } + + public void testGetNodesStoreName() throws Exception + { + long fromCommitTime = System.currentTimeMillis(); + + buildTransactions4(); + + String url = "/api/solr/transactions?fromCommitTime=" + fromCommitTime; + TestWebScriptServer.GetRequest req = new TestWebScriptServer.GetRequest(url); + Response response = sendRequest(req, Status.STATUS_OK, admin); + + if(logger.isDebugEnabled()) + { + logger.debug(response.getContentAsString()); + } + JSONObject json = new JSONObject(response.getContentAsString()); + + JSONArray transactions = json.getJSONArray("transactions"); + assertEquals("Number of transactions is incorrect", 2, transactions.length()); + + // first txn has 2 updates rather than three because content1 is deleted in txn 2 and therefore + // "belongs" to that txn (because txn2 was the last to alter the node) + + List transactionIds = getTransactionIds(transactions); + + // exact store name + GetNodesParameters parameters = new GetNodesParameters(); + parameters.setTransactionIds(transactionIds); + parameters.setStoreProtocol(storeRef.getProtocol()); + parameters.setStoreIdentifier(storeRef.getIdentifier()); + JSONArray nodes = getNodes(parameters, 0); + assertEquals("Number of nodes is incorrect", 101, nodes.length()); + + nodes = getNodes(parameters, 50); + assertEquals("Number of nodes is incorrect", 50, nodes.length()); + + // store protocol + parameters = new GetNodesParameters(); + parameters.setTransactionIds(transactionIds); + parameters.setStoreProtocol(storeRef.getProtocol()); + nodes = getNodes(parameters, 0); + assertEquals("Number of nodes is incorrect", 202, nodes.length()); + + // store identifier + parameters = new GetNodesParameters(); + parameters.setTransactionIds(transactionIds); + parameters.setStoreIdentifier(storeRef.getIdentifier()); + nodes = getNodes(parameters, 0); + assertEquals("Number of nodes is incorrect", 101, nodes.length()); + } + +/* private void buildTransactions3() + { + txnHelper.doInTransaction(new RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + PropertyMap props = new PropertyMap(); + props.put(ContentModel.PROP_NAME, "Container1"); + container3 = nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN, + ContentModel.TYPE_FOLDER, + props).getChildRef(); + + System.out.println("container1 = " + container1); + + FileInfo content1Info = fileFolderService.create(container3, "Content1", ContentModel.TYPE_CONTENT); + content1 = content1Info.getNodeRef(); + + container3NodeID = getNodeID(container3); + content1NodeID = getNodeID(content1); + + ContentWriter writer = contentService.getWriter(content1Info.getNodeRef(), ContentModel.PROP_CONTENT, true); + writer.putContent("test content"); + + if(logger.isDebugEnabled()) + { + logger.debug("content1 = " + content1); + } + + return null; + } + }); + } + + public void testGetContent() throws Exception + { + long nodeId = -1l; + String propertyName = ContentModel.PROP_CONTENT.toString(); + + buildTransactions3(); + + String url = "/api/solr/content?nodeId=" + nodeId + "&propertyName=" + propertyName; + TestWebScriptServer.GetRequest req = new TestWebScriptServer.GetRequest(url); + Response response = sendRequest(req, Status.STATUS_OK, admin); + if(logger.isDebugEnabled()) + { + logger.debug("content1 = " + response.getContentAsString()); + } + + assertEquals("Content length is incorrect", "test content".length(), response.getContentLength()); + + }*/ + + private long getNodeID(NodeRef nodeRef) + { + Pair pair = nodeDAO.getNodePair(nodeRef); + assertNotNull("Can't find node " + nodeRef, pair); + return pair.getFirst(); + } + + private static class GetNodesParameters + { + private List transactionIds; + private Long fromNodeId; + private Long toNodeId; + + private String storeProtocol; + private String storeIdentifier; + + private Set includeNodeTypes; + private Set excludeNodeTypes; + + private Set includeAspects; + private Set excludeAspects; + + public boolean getStoreFilter() + { + return (storeProtocol != null || storeIdentifier != null); + } + + public void setStoreProtocol(String storeProtocol) + { + this.storeProtocol = storeProtocol; + } + + public String getStoreProtocol() + { + return storeProtocol; + } + + public void setStoreIdentifier(String storeIdentifier) + { + this.storeIdentifier = storeIdentifier; + } + + public String getStoreIdentifier() + { + return storeIdentifier; + } + + public void setTransactionIds(List txnIds) + { + this.transactionIds = txnIds; + } + + public List getTransactionIds() + { + return transactionIds; + } + + public Long getFromNodeId() + { + return fromNodeId; + } + + public void setFromNodeId(Long fromNodeId) + { + this.fromNodeId = fromNodeId; + } + + public Long getToNodeId() + { + return toNodeId; + } + + public void setToNodeId(Long toNodeId) + { + this.toNodeId = toNodeId; + } + + public Set getIncludeNodeTypes() + { + return includeNodeTypes; + } + + public Set getExcludeNodeTypes() + { + return excludeNodeTypes; + } + + public Set getIncludeAspects() + { + return includeAspects; + } + + public Set getExcludeAspects() + { + return excludeAspects; + } + + public void setIncludeNodeTypes(Set includeNodeTypes) + { + this.includeNodeTypes = includeNodeTypes; + } + + public void setExcludeNodeTypes(Set excludeNodeTypes) + { + this.excludeNodeTypes = excludeNodeTypes; + } + + public void setIncludeAspects(Set includeAspects) + { + this.includeAspects = includeAspects; + } + + public void setExcludeAspects(Set excludeAspects) + { + this.excludeAspects = excludeAspects; + } + + } +} \ No newline at end of file