selectNodeAssocsBySource(Long sourceNodeId, Long typeQNameId)
diff --git a/source/java/org/alfresco/repo/domain/query/CannedQueryDAO.java b/source/java/org/alfresco/repo/domain/query/CannedQueryDAO.java
index 4ae6d0d774..77bf9d8cbd 100644
--- a/source/java/org/alfresco/repo/domain/query/CannedQueryDAO.java
+++ b/source/java/org/alfresco/repo/domain/query/CannedQueryDAO.java
@@ -1,4 +1,4 @@
-/*
+/*
* Copyright (C) 2005-2010 Alfresco Software Limited.
*
* This file is part of Alfresco
@@ -14,30 +14,108 @@
* 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 .
- */
-package org.alfresco.repo.domain.query;
-
-/**
- * DAO services for general-use canned queries
- *
- * @author Derek Hulley
- * @since 3.5
- */
-public interface CannedQueryDAO
-{
- /**
- * Execute a count(*)-style query returning a count value. The implementation
- * will ensure that null is substituted with 0, if required.
- *
- * All exceptions can be safely caught and handled as required.
- *
- * @param sqlNamespace the query namespace (defined by config file) e.g. alfresco.query.usage
- * @param queryName the name of the query e.g. select_userCount
- * @param parameterObj the values to drive the selection
- * @return a non-null count
- *
- * @throws QueryException if the query returned multiple results
- */
- Long executeCountQuery(String sqlNamespace, String queryName, Object parameterObj);
-}
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.repo.domain.query;
+
+import java.util.List;
+
+/**
+ * DAO services for general-use database canned queries. Note that this is specifically targeted
+ * at low-level queries to the persistance layer.
+ *
+ * @author Derek Hulley
+ * @since 3.5
+ */
+public interface CannedQueryDAO
+{
+ /*
+ * This interface looks very much like SqlSessionTemplate; this is not accidental.
+ * However, the use of generics and the ability to trace all general SQL queries through
+ * an Alfresco API make it useful. Additionally, there are specific checks done in the
+ * implementation to insulate the client from some common problems.
+ */
+
+ /**
+ * Execute a count(*)-style query returning a count value. The implementation
+ * will ensure that null is substituted with 0, if required.
+ *
+ * All exceptions can be safely caught and handled as required.
+ *
+ * @param sqlNamespace the query namespace (defined by config file) e.g. alfresco.query.usage
+ * @param queryName the name of the query e.g. select_userCount
+ * @param parameterObj the values to drive the selection (may be null if not required)
+ * @return a non-null count
+ *
+ * @throws QueryException if the query returned multiple results
+ */
+ Long executeCountQuery(String sqlNamespace, String queryName, Object parameterObj);
+
+ /**
+ * Execute a query that returns exactly one result. The assumption is that the parameters provided
+ * uniquely identify the object.
+ *
+ * @param the return value's type
+ * @param sqlNamespace the query namespace (defined by config file) e.g. alfresco.query.usage
+ * @param queryName the name of the query e.g. select_userCount
+ * @param parameterObj the values to drive the selection (may be null if not required)
+ * @return the unique result (never null)
+ * @throws concurrency-related exception if a single object was not found
+ */
+ R executeQueryUnique(String sqlNamespace, String queryName, Object parameterObj);
+
+ /**
+ * Execute a query that returns one or more results.
+ *
+ * @param the return value's type
+ * @param sqlNamespace the query namespace (defined by config file) e.g. alfresco.query.usage
+ * @param queryName the name of the query e.g. select_userCount
+ * @param parameterObj the values to drive the selection (may be null if not required)
+ * @param offset the number of results to skip
+ * @param limit the maximum number of results to retrieve
+ * @return
+ */
+ List executeQuery(
+ String sqlNamespace, String queryName, Object parameterObj,
+ int offset, int limit);
+
+ /**
+ * Execute a query that returns one or more results, processing the results using a handler.
+ *
+ * @param the return value's type
+ * @param sqlNamespace the query namespace (defined by config file) e.g. alfresco.query.usage
+ * @param queryName the name of the query e.g. select_userCount
+ * @param parameterObj the values to drive the selection (may be null if not required)
+ * @param offset the number of results to skip
+ * @param limit the maximum number of results to retrieve
+ * @return
+ */
+ void executeQuery(
+ String sqlNamespace,
+ String queryName,
+ Object parameterObj,
+ int offset,
+ int limit,
+ ResultHandler handler);
+
+ /**
+ * A simple, typed results handler.
+ *
+ * @author Derek Hulley
+ * @since 4.0
+ *
+ * @param the type of the result
+ */
+ public interface ResultHandler
+ {
+ /**
+ * Allow implementations to process a result. Note that the interface contract will
+ * be met, but internally the querying mechanism might not be able to optimise out
+ * all result fetching.
+ *
+ * @return true if more results are required or false to
+ * terminate result fetching.
+ */
+ boolean handleResult(R result);
+ }
+}
diff --git a/source/java/org/alfresco/repo/domain/query/CannedQueryDAOTest.java b/source/java/org/alfresco/repo/domain/query/CannedQueryDAOTest.java
index b6780dca95..994e1a68e8 100644
--- a/source/java/org/alfresco/repo/domain/query/CannedQueryDAOTest.java
+++ b/source/java/org/alfresco/repo/domain/query/CannedQueryDAOTest.java
@@ -1,5 +1,5 @@
-/*
- * Copyright (C) 2005-2010 Alfresco Software Limited.
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
*
* This file is part of Alfresco
*
@@ -14,196 +14,317 @@
* 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 .
- */
-package org.alfresco.repo.domain.query;
-
-import junit.framework.TestCase;
-
-import org.alfresco.repo.domain.mimetype.MimetypeDAO;
-import org.alfresco.repo.transaction.RetryingTransactionHelper;
-import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
-import org.alfresco.service.ServiceRegistry;
-import org.alfresco.service.transaction.TransactionService;
-import org.alfresco.util.ApplicationContextHelper;
-import org.alfresco.util.GUID;
-import org.springframework.context.ApplicationContext;
-
-/**
- * @see CannedQueryDAO
- *
- * @author Derek Hulley
- * @since 3.2
- */
-public class CannedQueryDAOTest extends TestCase
-{
- private static final String QUERY_NS = "alfresco.query.test";
- private static final String QUERY_SELECT_MIMETYPE_COUNT = "select_CountMimetypes";
-
- private ApplicationContext ctx = ApplicationContextHelper.getApplicationContext();
-
- private TransactionService transactionService;
- private RetryingTransactionHelper txnHelper;
- private CannedQueryDAO cannedQueryDAO;
- private MimetypeDAO mimetypeDAO;
-
- private String mimetypePrefix;
-
- @Override
- public void setUp() throws Exception
- {
- ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY);
- transactionService = serviceRegistry.getTransactionService();
- txnHelper = transactionService.getRetryingTransactionHelper();
-
- cannedQueryDAO = (CannedQueryDAO) ctx.getBean("cannedQueryDAO");
- mimetypeDAO = (MimetypeDAO) ctx.getBean("mimetypeDAO");
-
- RetryingTransactionCallback createMimetypeCallback = new RetryingTransactionCallback()
- {
- @Override
- public String execute() throws Throwable
- {
- String mimetypePrefix = GUID.generate();
- mimetypeDAO.getOrCreateMimetype(mimetypePrefix + "-aaa");
- mimetypeDAO.getOrCreateMimetype(mimetypePrefix + "-bbb");
- return mimetypePrefix;
- }
- };
- mimetypePrefix = txnHelper.doInTransaction(createMimetypeCallback);
- }
-
- /**
- * Helper parameter class for testing
- * @author Derek Hulley
- */
- public static class TestOneParams
- {
- private final String mimetypeMatch;
- private final boolean exact;
- private boolean forceFail; // Trigger a SQL exception
- public TestOneParams(String mimetypeMatch, boolean exact)
- {
- this.mimetypeMatch = mimetypeMatch;
- this.exact = exact;
- this.forceFail = false;
- }
- @Override
- public String toString()
- {
- return "TestOneParams [mimetypeMatch=" + mimetypeMatch + ", exact=" + exact + "]";
- }
- public String getMimetypeMatch()
- {
- return mimetypeMatch;
- }
- public boolean isExact()
- {
- return exact;
- }
- public boolean isForceFail()
- {
- return forceFail;
- }
- public void setForceFail(boolean forceFail)
- {
- this.forceFail = forceFail;
- }
- }
-
- /**
- * Force a failure and ensure that the connection is not tarnished
- */
- public void testExecute_FailureRecovery() throws Throwable
- {
- RetryingTransactionCallback failCallback = new RetryingTransactionCallback()
- {
- @Override
- public Void execute() throws Throwable
- {
- TestOneParams params = new TestOneParams(null, true);
- params.setForceFail(true);
- try
- {
- cannedQueryDAO.executeCountQuery(QUERY_NS, QUERY_SELECT_MIMETYPE_COUNT, params);
- fail("Expected bad SQL");
- }
- catch (Throwable e)
- {
- // Expected
- }
- // Now attempt to write to the connection
- mimetypeDAO.getOrCreateMimetype(mimetypePrefix + "-postfail");
- return null;
- }
- };
- txnHelper.doInTransaction(failCallback, false);
- }
-
- public void testExecute_CountAllMimetypes() throws Throwable
- {
- RetryingTransactionCallback selectCallback = new RetryingTransactionCallback()
- {
- @Override
- public Long execute() throws Throwable
- {
- TestOneParams params = new TestOneParams(null, true);
- return cannedQueryDAO.executeCountQuery(QUERY_NS, QUERY_SELECT_MIMETYPE_COUNT, params);
- }
- };
- Long count = txnHelper.doInTransaction(selectCallback, true);
- assertNotNull(count);
- assertTrue(count.longValue() > 0L);
- }
-
- /**
- * Ensures that no results returns 0 since SQL will return a null count.
- */
- public void testExecute_CountNoResults() throws Throwable
- {
- RetryingTransactionCallback selectCallback = new RetryingTransactionCallback()
- {
- @Override
- public Long execute() throws Throwable
- {
- TestOneParams params = new TestOneParams(GUID.generate(), true);
- return cannedQueryDAO.executeCountQuery(QUERY_NS, QUERY_SELECT_MIMETYPE_COUNT, params);
- }
- };
- Long count = txnHelper.doInTransaction(selectCallback, true);
- assertNotNull(count);
- assertEquals("Incorrect result count.", 0L, count.longValue());
- }
-
- public void testExecute_CountMimetypeExact() throws Throwable
- {
- RetryingTransactionCallback selectCallback = new RetryingTransactionCallback()
- {
- @Override
- public Long execute() throws Throwable
- {
- TestOneParams params = new TestOneParams(mimetypePrefix + "-aaa", true);
- return cannedQueryDAO.executeCountQuery(QUERY_NS, QUERY_SELECT_MIMETYPE_COUNT, params);
- }
- };
- Long count = txnHelper.doInTransaction(selectCallback, true);
- assertNotNull(count);
- assertEquals("Incorrect result count.", 1L, count.longValue());
- }
-
- public void testExecute_CountMimetypeWildcard() throws Throwable
- {
- RetryingTransactionCallback selectCallback = new RetryingTransactionCallback()
- {
- @Override
- public Long execute() throws Throwable
- {
- TestOneParams params = new TestOneParams(mimetypePrefix + "%", false);
- return cannedQueryDAO.executeCountQuery(QUERY_NS, QUERY_SELECT_MIMETYPE_COUNT, params);
- }
- };
- Long count = txnHelper.doInTransaction(selectCallback, true);
- assertNotNull(count);
- assertEquals("Incorrect result count.", 2L, count.longValue());
- }
-}
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.repo.domain.query;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import junit.framework.TestCase;
+
+import org.alfresco.error.ExceptionStackUtil;
+import org.alfresco.repo.domain.mimetype.MimetypeDAO;
+import org.alfresco.repo.domain.query.CannedQueryDAO.ResultHandler;
+import org.alfresco.repo.transaction.RetryingTransactionHelper;
+import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
+import org.alfresco.service.ServiceRegistry;
+import org.alfresco.service.transaction.TransactionService;
+import org.alfresco.util.ApplicationContextHelper;
+import org.alfresco.util.GUID;
+import org.springframework.context.ApplicationContext;
+
+/**
+ * @see CannedQueryDAO
+ *
+ * @author Derek Hulley
+ * @since 3.2
+ */
+public class CannedQueryDAOTest extends TestCase
+{
+ private static final String QUERY_NS = "alfresco.query.test";
+ private static final String QUERY_SELECT_MIMETYPE_COUNT = "select_CountMimetypes";
+ private static final String QUERY_SELECT_MIMETYPES = "select_Mimetypes";
+
+ private ApplicationContext ctx = ApplicationContextHelper.getApplicationContext();
+
+ private TransactionService transactionService;
+ private RetryingTransactionHelper txnHelper;
+ private CannedQueryDAO cannedQueryDAO;
+ private MimetypeDAO mimetypeDAO;
+
+ private String mimetypePrefix;
+
+ @Override
+ public void setUp() throws Exception
+ {
+ ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY);
+ transactionService = serviceRegistry.getTransactionService();
+ txnHelper = transactionService.getRetryingTransactionHelper();
+
+ cannedQueryDAO = (CannedQueryDAO) ctx.getBean("cannedQueryDAO");
+ mimetypeDAO = (MimetypeDAO) ctx.getBean("mimetypeDAO");
+
+ RetryingTransactionCallback createMimetypeCallback = new RetryingTransactionCallback()
+ {
+ @Override
+ public String execute() throws Throwable
+ {
+ String mimetypePrefix = GUID.generate();
+ mimetypeDAO.getOrCreateMimetype(mimetypePrefix + "-aaa");
+ mimetypeDAO.getOrCreateMimetype(mimetypePrefix + "-bbb");
+ return mimetypePrefix;
+ }
+ };
+ mimetypePrefix = txnHelper.doInTransaction(createMimetypeCallback);
+ }
+
+ /**
+ * Helper parameter class for testing
+ * @author Derek Hulley
+ */
+ public static class TestOneParams
+ {
+ private final String mimetypeMatch;
+ private final boolean exact;
+ private boolean forceFail; // Trigger a SQL exception
+ public TestOneParams(String mimetypeMatch, boolean exact)
+ {
+ this.mimetypeMatch = mimetypeMatch;
+ this.exact = exact;
+ this.forceFail = false;
+ }
+ @Override
+ public String toString()
+ {
+ return "TestOneParams [mimetypeMatch=" + mimetypeMatch + ", exact=" + exact + "]";
+ }
+ public String getMimetypeMatch()
+ {
+ return mimetypeMatch;
+ }
+ public boolean isExact()
+ {
+ return exact;
+ }
+ public boolean isForceFail()
+ {
+ return forceFail;
+ }
+ public void setForceFail(boolean forceFail)
+ {
+ this.forceFail = forceFail;
+ }
+ }
+
+ /**
+ * Force a failure and ensure that the connection is not tarnished
+ */
+ public void testExecute_FailureRecovery() throws Throwable
+ {
+ RetryingTransactionCallback failCallback = new RetryingTransactionCallback()
+ {
+ @Override
+ public Void execute() throws Throwable
+ {
+ TestOneParams params = new TestOneParams(null, true);
+ params.setForceFail(true);
+ try
+ {
+ cannedQueryDAO.executeCountQuery(QUERY_NS, QUERY_SELECT_MIMETYPE_COUNT, params);
+ fail("Expected bad SQL");
+ }
+ catch (Throwable e)
+ {
+ // Expected
+ }
+ // Now attempt to write to the connection
+ mimetypeDAO.getOrCreateMimetype(mimetypePrefix + "-postfail");
+ return null;
+ }
+ };
+ txnHelper.doInTransaction(failCallback, false);
+ }
+
+ public void testExecute_CountAllMimetypes() throws Throwable
+ {
+ RetryingTransactionCallback selectCallback = new RetryingTransactionCallback()
+ {
+ @Override
+ public Long execute() throws Throwable
+ {
+ TestOneParams params = new TestOneParams(null, true);
+ return cannedQueryDAO.executeCountQuery(QUERY_NS, QUERY_SELECT_MIMETYPE_COUNT, params);
+ }
+ };
+ Long count = txnHelper.doInTransaction(selectCallback, true);
+ assertNotNull(count);
+ assertTrue(count.longValue() > 0L);
+ }
+
+ /**
+ * Ensures that no results returns 0 since SQL will return a null count.
+ */
+ public void testExecute_CountNoResults() throws Throwable
+ {
+ RetryingTransactionCallback selectCallback = new RetryingTransactionCallback()
+ {
+ @Override
+ public Long execute() throws Throwable
+ {
+ TestOneParams params = new TestOneParams(GUID.generate(), true);
+ return cannedQueryDAO.executeCountQuery(QUERY_NS, QUERY_SELECT_MIMETYPE_COUNT, params);
+ }
+ };
+ Long count = txnHelper.doInTransaction(selectCallback, true);
+ assertNotNull(count);
+ assertEquals("Incorrect result count.", 0L, count.longValue());
+ }
+
+ public void testExecute_CountMimetypeExact() throws Throwable
+ {
+ RetryingTransactionCallback selectCallback = new RetryingTransactionCallback()
+ {
+ @Override
+ public Long execute() throws Throwable
+ {
+ TestOneParams params = new TestOneParams(mimetypePrefix + "-aaa", true);
+ return cannedQueryDAO.executeCountQuery(QUERY_NS, QUERY_SELECT_MIMETYPE_COUNT, params);
+ }
+ };
+ Long count = txnHelper.doInTransaction(selectCallback, true);
+ assertNotNull(count);
+ assertEquals("Incorrect result count.", 1L, count.longValue());
+ }
+
+ public void testExecute_CountMimetypeWildcard() throws Throwable
+ {
+ RetryingTransactionCallback selectCallback = new RetryingTransactionCallback()
+ {
+ @Override
+ public Long execute() throws Throwable
+ {
+ TestOneParams params = new TestOneParams(mimetypePrefix + "%", false);
+ return cannedQueryDAO.executeCountQuery(QUERY_NS, QUERY_SELECT_MIMETYPE_COUNT, params);
+ }
+ };
+ Long count = txnHelper.doInTransaction(selectCallback, true);
+ assertNotNull(count);
+ assertEquals("Incorrect result count.", 2L, count.longValue());
+ }
+
+ public void testExecute_BadBounds() throws Throwable
+ {
+ try
+ {
+ cannedQueryDAO.executeQuery(QUERY_NS, QUERY_SELECT_MIMETYPES, null, -1, 10);
+ fail("Illegal parameter not detected");
+ }
+ catch (IllegalArgumentException e)
+ {
+ // Expected
+ }
+ try
+ {
+ cannedQueryDAO.executeQuery(QUERY_NS, QUERY_SELECT_MIMETYPES, null, 0, -1);
+ fail("Illegal parameter not detected");
+ }
+ catch (IllegalArgumentException e)
+ {
+ // Expected
+ }
+ try
+ {
+ cannedQueryDAO.executeQuery(QUERY_NS, QUERY_SELECT_MIMETYPES, null, 0, Integer.MAX_VALUE);
+ fail("Illegal parameter not detected");
+ }
+ catch (IllegalArgumentException e)
+ {
+ // Expected
+ }
+ }
+
+ public void testExecute_ListMimetypes() throws Throwable
+ {
+ RetryingTransactionCallback> selectCallback = new RetryingTransactionCallback>()
+ {
+ @Override
+ public List execute() throws Throwable
+ {
+ TestOneParams params = new TestOneParams(null, false);
+ return cannedQueryDAO.executeQuery(QUERY_NS, QUERY_SELECT_MIMETYPES, params, 0, 2);
+ }
+ };
+ List mimetypes = txnHelper.doInTransaction(selectCallback, true);
+ assertNotNull(mimetypes);
+ assertTrue("Too many results", mimetypes.size() <= 2);
+ }
+
+ public void testExecute_ResultHandlerWithError() throws Throwable
+ {
+ final ResultHandler resultHandler = new ResultHandler()
+ {
+ @Override
+ public boolean handleResult(String result)
+ {
+ throw new UnsupportedOperationException();
+ }
+ };
+
+ RetryingTransactionCallback> selectCallback = new RetryingTransactionCallback>()
+ {
+ @Override
+ public List execute() throws Throwable
+ {
+ TestOneParams params = new TestOneParams(null, false);
+ try
+ {
+ cannedQueryDAO.executeQuery(QUERY_NS, QUERY_SELECT_MIMETYPES, params, 0, 2, resultHandler);
+ fail("Expected UnsupportedOperationException");
+ }
+ catch (Exception e)
+ {
+ // Expected, but make sure that our exception is the cause
+ Throwable ee = ExceptionStackUtil.getCause(e, UnsupportedOperationException.class);
+ if (ee == null)
+ {
+ throw e;
+ }
+ }
+ // Now query again with success
+ return cannedQueryDAO.executeQuery(QUERY_NS, QUERY_SELECT_MIMETYPES, params, 0, 2);
+ }
+ };
+ List mimetypes = txnHelper.doInTransaction(selectCallback, true);
+ assertNotNull(mimetypes);
+ assertTrue("Too many results", mimetypes.size() <= 2);
+ }
+
+ public void testExecute_ResultHandlerWithEarlyTermination() throws Throwable
+ {
+ final List results = new ArrayList();
+ final ResultHandler resultHandler = new ResultHandler()
+ {
+ @Override
+ public boolean handleResult(String result)
+ {
+ // Only one result then stop
+ results.add(result);
+ return false;
+ }
+ };
+
+ RetryingTransactionCallback selectCallback = new RetryingTransactionCallback()
+ {
+ @Override
+ public Void execute() throws Throwable
+ {
+ TestOneParams params = new TestOneParams(null, false);
+ cannedQueryDAO.executeQuery(QUERY_NS, QUERY_SELECT_MIMETYPES, params, 0, 2, resultHandler);
+ return null;
+ }
+ };
+ txnHelper.doInTransaction(selectCallback, true);
+ assertEquals("ResultHandler did not terminate early", 1, results.size());
+ }
+}
diff --git a/source/java/org/alfresco/repo/domain/query/ibatis/CannedQueryDAOImpl.java b/source/java/org/alfresco/repo/domain/query/ibatis/CannedQueryDAOImpl.java
index 0a93450cae..3a671f6171 100644
--- a/source/java/org/alfresco/repo/domain/query/ibatis/CannedQueryDAOImpl.java
+++ b/source/java/org/alfresco/repo/domain/query/ibatis/CannedQueryDAOImpl.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2005-2010 Alfresco Software Limited.
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
*
* This file is part of Alfresco
*
@@ -18,9 +18,13 @@
*/
package org.alfresco.repo.domain.query.ibatis;
+import java.util.List;
+
import org.alfresco.repo.domain.query.AbstractCannedQueryDAOImpl;
import org.alfresco.repo.domain.query.QueryException;
import org.alfresco.util.PropertyCheck;
+import org.apache.ibatis.session.ResultContext;
+import org.apache.ibatis.session.RowBounds;
import org.mybatis.spring.SqlSessionTemplate;
/**
@@ -44,6 +48,15 @@ public class CannedQueryDAOImpl extends AbstractCannedQueryDAOImpl
super.init();
PropertyCheck.mandatory(this, "template", template);
}
+
+ /**
+ * @return the compound query name
+ */
+ private final String makeQueryName(final String sqlNamespace, final String queryName)
+ {
+ return new StringBuilder(sqlNamespace.length() + queryName.length() + 1)
+ .append(sqlNamespace).append(".").append(queryName).toString();
+ }
/**
* {@inheritDoc}
@@ -53,9 +66,7 @@ public class CannedQueryDAOImpl extends AbstractCannedQueryDAOImpl
@Override
public Long executeCountQuery(String sqlNamespace, String queryName, Object parameterObj)
{
- String query = new StringBuilder(sqlNamespace.length() + queryName.length() + 1)
- .append(sqlNamespace).append(".").append(queryName).toString();
-
+ String query = makeQueryName(sqlNamespace, queryName);
try
{
Long result = (Long) template.selectOne(query, parameterObj);
@@ -92,4 +103,145 @@ public class CannedQueryDAOImpl extends AbstractCannedQueryDAOImpl
e);
}
}
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public R executeQueryUnique(String sqlNamespace, String queryName, Object parameterObj)
+ {
+ String query = makeQueryName(sqlNamespace, queryName);
+ Object obj = template.selectOne(query, parameterObj);
+ try
+ {
+ return (R) obj;
+ }
+ catch (ClassCastException e)
+ {
+ throw new IllegalArgumentException("Return type of query does not match expected type.", e);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public List executeQuery(
+ String sqlNamespace, String queryName, Object parameterObj,
+ int offset, int limit)
+ {
+ if (offset < 0 || offset == Integer.MAX_VALUE)
+ {
+ throw new IllegalArgumentException("Query result offset must be zero or greater.");
+ }
+ if (limit <= 0 || limit == Integer.MAX_VALUE)
+ {
+ throw new IllegalArgumentException("Query results must be constrained by a limit.");
+ }
+ String query = makeQueryName(sqlNamespace, queryName);
+ try
+ {
+ RowBounds bounds = new RowBounds(offset, limit);
+ return (List) template.selectList(query, parameterObj, bounds);
+ }
+ catch (ClassCastException e)
+ {
+ throw new IllegalArgumentException("Return type of query does not match expected type.", e);
+ }
+ catch (Throwable e)
+ {
+ throw new QueryException(
+ "Failed to execute query: \n" +
+ " Namespace: " + sqlNamespace + "\n" +
+ " queryName: " + queryName + "\n" +
+ " Parameter: " + parameterObj + "\n" +
+ " Offset: " + offset + "\n" +
+ " Limit: " + limit,
+ e);
+ }
+ }
+
+ @Override
+ public void executeQuery(
+ String sqlNamespace, String queryName, Object parameterObj,
+ int offset, int limit,
+ ResultHandler handler)
+ {
+ if (offset < 0 || offset == Integer.MAX_VALUE)
+ {
+ throw new IllegalArgumentException("Query result offset must be zero or greater.");
+ }
+
+ // TODO MyBatis workaround - temporarily support unlimited for nested result maps (see also below)
+ /*
+ if (limit <= 0 || limit == Integer.MAX_VALUE)
+ {
+ throw new IllegalArgumentException("Query results must be constrained by a limit.");
+ }
+ */
+
+ String query = makeQueryName(sqlNamespace, queryName);
+ ResultHandlerTranslator resultHandler = new ResultHandlerTranslator(handler);
+ try
+ {
+ // TODO MyBatis workaround
+ // http://code.google.com/p/mybatis/issues/detail?id=58 (and #139, #234, ...)
+ template.clearCache();
+
+ if ((offset == 0) && (limit == Integer.MAX_VALUE))
+ {
+ // TODO MyBatis workaround - temporarily support unlimited for nested result maps (see also above)
+ // http://code.google.com/p/mybatis/issues/detail?id=129
+ template.select(query, parameterObj, resultHandler);
+ }
+ else
+ {
+ RowBounds bounds = new RowBounds(offset, limit);
+ template.select(query, parameterObj, bounds, resultHandler);
+ }
+ }
+ catch (ClassCastException e)
+ {
+ throw new IllegalArgumentException("Return type of query does not match expected type.", e);
+ }
+ catch (Throwable e)
+ {
+ throw new QueryException(
+ "Failed to execute query: \n" +
+ " Namespace: " + sqlNamespace + "\n" +
+ " queryName: " + queryName + "\n" +
+ " Parameter: " + parameterObj + "\n" +
+ " Offset: " + offset + "\n" +
+ " Limit: " + limit,
+ e);
+ }
+ }
+
+ /**
+ * Helper class to translate MyBatis ResultHandler to Alfresco ResultHandler.
+ *
+ * @author Derek Hulley
+ *
+ * @param
+ */
+ private static class ResultHandlerTranslator implements org.apache.ibatis.session.ResultHandler
+ {
+ private final ResultHandler target;
+ boolean stopped = false;
+ private ResultHandlerTranslator(ResultHandler target)
+ {
+ this.target = target;
+ }
+ @SuppressWarnings("unchecked")
+ @Override
+ public void handleResult(ResultContext ctx)
+ {
+ if (stopped || ctx.isStopped())
+ {
+ return; // Fly through results without further callbacks
+ }
+ boolean more = this.target.handleResult((R)ctx.getResultObject());
+ if (!more)
+ {
+ ctx.stop();
+ stopped = true;
+ }
+ }
+ }
}
diff --git a/source/java/org/alfresco/repo/domain/schema/SchemaBootstrap.java b/source/java/org/alfresco/repo/domain/schema/SchemaBootstrap.java
index 1af4955829..eef4265fd5 100644
--- a/source/java/org/alfresco/repo/domain/schema/SchemaBootstrap.java
+++ b/source/java/org/alfresco/repo/domain/schema/SchemaBootstrap.java
@@ -123,6 +123,7 @@ public class SchemaBootstrap extends AbstractLifecycleBean
private static final String ERR_VALIDATION_FAILED = "schema.update.err.validation_failed";
private static final String ERR_SCRIPT_NOT_RUN = "schema.update.err.update_script_not_run";
private static final String ERR_SCRIPT_NOT_FOUND = "schema.update.err.script_not_found";
+ private static final String ERR_STATEMENT_INCLUDE_BEFORE_SQL = "schema.update.err.statement_include_before_sql";
private static final String ERR_STATEMENT_VAR_ASSIGNMENT_BEFORE_SQL = "schema.update.err.statement_var_assignment_before_sql";
private static final String ERR_STATEMENT_VAR_ASSIGNMENT_FORMAT = "schema.update.err.statement_var_assignment_format";
private static final String ERR_STATEMENT_TERMINATOR = "schema.update.err.statement_terminator";
@@ -1000,8 +1001,20 @@ public class SchemaBootstrap extends AbstractLifecycleBean
// trim it
String sql = sqlOriginal.trim();
+ // Check of includes
+ if (sql.startsWith("--INCLUDE:"))
+ {
+ if (sb.length() > 0)
+ {
+ // This can only be set before a new SQL statement
+ throw AlfrescoRuntimeException.create(ERR_STATEMENT_INCLUDE_BEFORE_SQL, (line - 1), scriptUrl);
+ }
+ String includedScriptUrl = sql.substring(10, sql.length());
+ // Execute the script in line
+ executeScriptUrl(cfg, connection, includedScriptUrl);
+ }
// Check for variable assignment
- if (sql.startsWith("--ASSIGN:"))
+ else if (sql.startsWith("--ASSIGN:"))
{
if (sb.length() > 0)
{
diff --git a/source/java/org/alfresco/repo/domain/solr/AclEntity.java b/source/java/org/alfresco/repo/domain/solr/AclEntity.java
index 4e2b4a6e9c..844bdefe7d 100644
--- a/source/java/org/alfresco/repo/domain/solr/AclEntity.java
+++ b/source/java/org/alfresco/repo/domain/solr/AclEntity.java
@@ -18,10 +18,7 @@
*/
package org.alfresco.repo.domain.solr;
-import java.util.List;
-
import org.alfresco.repo.solr.Acl;
-import org.alfresco.repo.solr.AclEntry;
/**
* Interface for SOLR changeset objects.
@@ -33,7 +30,6 @@ public class AclEntity implements Acl
{
private Long id;
private Long aclChangeSetId;
- private List entries;
@Override
public String toString()
@@ -41,7 +37,6 @@ public class AclEntity implements Acl
return "AclEntity " +
"[id=" + id +
", aclChangeSetId=" + aclChangeSetId +
- ", entries=" + entries +
"]";
}
@@ -64,14 +59,4 @@ public class AclEntity implements Acl
{
this.aclChangeSetId = aclChangeSetId;
}
-
- @Override
- public List getEntries()
- {
- return entries;
- }
- public void setEntries(List entries)
- {
- this.entries = entries;
- }
}
diff --git a/source/java/org/alfresco/repo/domain/solr/AclEntryEntity.java b/source/java/org/alfresco/repo/domain/solr/AclEntryEntity.java
deleted file mode 100644
index a33cde4313..0000000000
--- a/source/java/org/alfresco/repo/domain/solr/AclEntryEntity.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright (C) 2005-2011 Alfresco Software Limited.
- *
- * This file is part of Alfresco
- *
- * 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 .
- */
-package org.alfresco.repo.domain.solr;
-
-import org.alfresco.repo.solr.AclEntry;
-
-/**
- * Interface for SOLR changeset objects.
- *
- * @author Derek Hulley
- * @since 4.0
- */
-public class AclEntryEntity implements AclEntry
-{
- private Long id;
- private Long aclId;
- private Long aclPermissionId;
- private String aclAuthority;
-
- @Override
- public String toString()
- {
- return "AclEntryEntity " +
- "[id=" + id +
- ", aclId=" + aclId +
- ", aclPermissionId=" + aclPermissionId +
- ", aclAuthority=" + aclAuthority +
- "]";
- }
-
- @Override
- public Long getId()
- {
- return id;
- }
- public void setId(Long id)
- {
- this.id = id;
- }
-
- @Override
- public Long getAclId()
- {
- return aclId;
- }
-
- public void setAclId(Long aclId)
- {
- this.aclId = aclId;
- }
-
- @Override
- public Long getAclPermissionId()
- {
- return aclPermissionId;
- }
- public void setAclPermissionId(Long aclPermissionId)
- {
- this.aclPermissionId = aclPermissionId;
- }
-
- @Override
- public String getAclAuthority()
- {
- return aclAuthority;
- }
- public void setAclAuthority(String aclAuthority)
- {
- this.aclAuthority = aclAuthority;
- }
-}
diff --git a/source/java/org/alfresco/repo/domain/solr/SOLRDAO.java b/source/java/org/alfresco/repo/domain/solr/SOLRDAO.java
index 56eb07de55..9788bff398 100644
--- a/source/java/org/alfresco/repo/domain/solr/SOLRDAO.java
+++ b/source/java/org/alfresco/repo/domain/solr/SOLRDAO.java
@@ -23,7 +23,6 @@ import java.util.List;
import org.alfresco.repo.domain.node.Node;
import org.alfresco.repo.solr.Acl;
import org.alfresco.repo.solr.AclChangeSet;
-import org.alfresco.repo.solr.AclEntry;
import org.alfresco.repo.solr.NodeParameters;
import org.alfresco.repo.solr.Transaction;
@@ -45,24 +44,15 @@ public interface SOLRDAO
public List getAclChangeSets(Long minAclChangeSetId, Long fromCommitTime, int maxResults);
/**
- * Get the ACLs (no rollup count) with paging options for a specific change set
+ * Get the ACLs (no rollup count) for the given ACL ChangeSets
*
* @param aclChangeSetIds the ACL ChangeSet IDs
* @param minAclId the minimum ACL ID - (inclusive and optional).
* @param maxResults the maximum number of results (must be greater than zero and less than MAX)
- * @return list of detailed ACL ChangeSets (details included, no roll-up)
+ * @return list of ACLs
*/
public List getAcls(List aclChangeSetIds, Long minAclId, int maxResults);
- /**
- * Get the ACL entries for specific ACLs, optionally pulling back authority details.
- *
- * @param aclIds the ACL IDs
- * @param includeAuthorities true to pull back authorities with read permission
- * @return
- */
- public List getAclEntries(List aclIds, boolean includeAuthorities);
-
/**
* Get the transactions from either minTxnId or fromCommitTime, optionally limited to maxResults
*
@@ -81,32 +71,4 @@ public interface SOLRDAO
* @return list of matching nodes
*/
public List getNodes(NodeParameters nodeParameters);
-
-// /**
-// * The interface that will be used to give query results to the calling code.
-// */
-// public static interface NodeQueryCallback
-// {
-// /**
-// * Handle a node.
-// *
-// * @param node the node
-// * @return Return true to continue processing rows or false to stop
-// */
-// boolean handleNode(Node node);
-// }
-//
-// /**
-// * The interface that will be used to give query results to the calling code.
-// */
-// public static interface NodeMetaDataQueryCallback
-// {
-// /**
-// * Handle a node.
-// *
-// * @param node the node meta data
-// * @return Return true to continue processing rows or false to stop
-// */
-// boolean handleNodeMetaData(NodeMetaData nodeMetaData);
-// }
}
diff --git a/source/java/org/alfresco/repo/domain/solr/SOLRDAOTest.java b/source/java/org/alfresco/repo/domain/solr/SOLRDAOTest.java
index 6738e64817..023a799979 100644
--- a/source/java/org/alfresco/repo/domain/solr/SOLRDAOTest.java
+++ b/source/java/org/alfresco/repo/domain/solr/SOLRDAOTest.java
@@ -104,7 +104,7 @@ public class SOLRDAOTest extends TestCase
try
{
// No limit on results
- solrDAO.getAcls(Collections.singletonList(1L), null, 0);
+ solrDAO.getAcls(Collections.singletonList(1L), null, 0);
fail("Expected IllegalArgumentException");
}
catch (IllegalArgumentException e)
@@ -142,13 +142,8 @@ public class SOLRDAOTest extends TestCase
List aclChangeSetIds = toIds(aclChangeSets);
// Now use those to query for details
- List acls = solrDAO.getAcls(aclChangeSetIds, null, 10);
- assertTrue("Results not limited", acls.size() <= 10);
+ List acls = solrDAO.getAcls(aclChangeSetIds, null, 1000);
- // Get again, but allow all results
- acls = solrDAO.getAcls(aclChangeSetIds, null, aclTotal);
- assertEquals("Expected exact results", aclTotal, acls.size());
-
// Check that the ACL ChangeSet IDs are correct
Set aclChangeSetIdsSet = new HashSet(aclChangeSetIds);
for (Acl acl : acls)
@@ -158,6 +153,45 @@ public class SOLRDAOTest extends TestCase
}
}
+ public void testQueryAcls_Single()
+ {
+ List aclChangeSets = solrDAO.getAclChangeSets(null, 0L, 1000);
+ // Find one with multiple ALCs
+ AclChangeSet aclChangeSet = null;
+ for (AclChangeSet aclChangeSetLoop : aclChangeSets)
+ {
+ if (aclChangeSetLoop.getAclCount() > 1)
+ {
+ aclChangeSet = aclChangeSetLoop;
+ break;
+ }
+ }
+ if (aclChangeSet == null)
+ {
+ // Nothing to test: Very unlikely
+ return;
+ }
+
+ // Loop a few times and check that the count is correct
+ Long aclChangeSetId = aclChangeSet.getId();
+ List aclChangeSetIds = Collections.singletonList(aclChangeSetId);
+ int aclCount = aclChangeSet.getAclCount();
+ int totalAclCount = 0;
+ Long minAclId = null;
+ while (true)
+ {
+ List acls = solrDAO.getAcls(aclChangeSetIds, minAclId, 1);
+ if (acls.size() == 0)
+ {
+ break;
+ }
+ assertEquals("Expected exactly one result", 1, acls.size());
+ totalAclCount++;
+ minAclId = acls.get(0).getId() + 1;
+ }
+ assertEquals("Expected to page to exact number of results", aclCount, totalAclCount);
+ }
+
private List toIds(List aclChangeSets)
{
List ids = new ArrayList(aclChangeSets.size());
diff --git a/source/java/org/alfresco/repo/domain/solr/ibatis/SOLRDAOImpl.java b/source/java/org/alfresco/repo/domain/solr/ibatis/SOLRDAOImpl.java
index 70d4d1a775..091049c048 100644
--- a/source/java/org/alfresco/repo/domain/solr/ibatis/SOLRDAOImpl.java
+++ b/source/java/org/alfresco/repo/domain/solr/ibatis/SOLRDAOImpl.java
@@ -27,7 +27,6 @@ import org.alfresco.repo.domain.solr.SOLRDAO;
import org.alfresco.repo.domain.solr.SOLRTrackingParameters;
import org.alfresco.repo.solr.Acl;
import org.alfresco.repo.solr.AclChangeSet;
-import org.alfresco.repo.solr.AclEntry;
import org.alfresco.repo.solr.NodeParameters;
import org.alfresco.repo.solr.Transaction;
import org.alfresco.util.PropertyCheck;
@@ -43,7 +42,6 @@ public class SOLRDAOImpl implements SOLRDAO
{
private static final String SELECT_CHANGESETS_SUMMARY = "alfresco.solr.select_ChangeSets_Summary";
private static final String SELECT_ACLS_BY_CHANGESET_IDS = "alfresco.solr.select_AclsByChangeSetIds";
- private static final String SELECT_ACLENTRIESS_BY_ACL_IDS = "alfresco.solr.select_AclEntriessByAclIds";
private static final String SELECT_TRANSACTIONS = "alfresco.solr.select_Txns";
private static final String SELECT_NODES = "alfresco.solr.select_Txn_Nodes";
@@ -107,10 +105,6 @@ public class SOLRDAOImpl implements SOLRDAO
{
throw new IllegalArgumentException("'aclChangeSetIds' cannot have more than 512 entries.");
}
- if (minAclId != null)
- {
- throw new IllegalArgumentException("When 'minAclId' is specified then there should be only one 'aclChangeSetIds'.");
- }
SOLRTrackingParameters params = new SOLRTrackingParameters();
params.setIds(aclChangeSetIds);
@@ -119,28 +113,6 @@ public class SOLRDAOImpl implements SOLRDAO
return (List) template.selectList(SELECT_ACLS_BY_CHANGESET_IDS, params, new RowBounds(0, maxResults));
}
- /**
- * {@inheritDoc}
- */
- @Override
- @SuppressWarnings("unchecked")
- public List getAclEntries(List aclIds, boolean includeAuthorities)
- {
- if (aclIds == null || aclIds.size() == 0)
- {
- throw new IllegalArgumentException("'aclIds' must contain IDs.");
- }
- if (aclIds.size() > 512)
- {
- throw new IllegalArgumentException("'aclIds' cannot have more than 512 entries.");
- }
- SOLRTrackingParameters params = new SOLRTrackingParameters();
- params.setIds(aclIds);
- params.setTrueOrFalse(includeAuthorities);
-
- return (List) template.selectList(SELECT_ACLENTRIESS_BY_ACL_IDS, params);
- }
-
/**
* {@inheritDoc}
*/
diff --git a/source/java/org/alfresco/repo/forms/FormServiceImplTest.java b/source/java/org/alfresco/repo/forms/FormServiceImplTest.java
index 9ba8bb392b..1fb8e66c91 100644
--- a/source/java/org/alfresco/repo/forms/FormServiceImplTest.java
+++ b/source/java/org/alfresco/repo/forms/FormServiceImplTest.java
@@ -952,7 +952,7 @@ public class FormServiceImplTest extends BaseAlfrescoSpringTest
// test different forms of itemId's
data.addFieldData(TypeFormProcessor.DESTINATION, this.folder.toString());
- newNode = (NodeRef)this.formService.saveForm(new Item(TYPE_FORM_ITEM_KIND, "cm_content"), data);
+ newNode = (NodeRef)this.formService.saveForm(new Item(TYPE_FORM_ITEM_KIND, "cm:content"), data);
assertNotNull("Expected new node to be created using itemId cm_content", newNode);
data.addFieldData(TypeFormProcessor.DESTINATION, this.folder.toString());
@@ -1074,7 +1074,7 @@ public class FormServiceImplTest extends BaseAlfrescoSpringTest
assertEquals(expectedContent, content);
}
- @SuppressWarnings({ "deprecation", "null" })
+ @SuppressWarnings({ "deprecation" })
public void disabledTestFDKModel() throws Exception
{
// NOTE: The FDK is not loaded by default, for this test to work you must
@@ -1269,7 +1269,7 @@ public class FormServiceImplTest extends BaseAlfrescoSpringTest
data = new FormData();
data.addFieldData("prop_cm_name", nodeName);
data.addFieldData(TypeFormProcessor.DESTINATION, this.folder.toString());
- NodeRef newNode = (NodeRef)this.formService.saveForm(new Item(TYPE_FORM_ITEM_KIND, "fdk_with_underscore"), data);
+ NodeRef newNode = (NodeRef)this.formService.saveForm(new Item(TYPE_FORM_ITEM_KIND, "fdk:with_underscore"), data);
assertNotNull(newNode);
}
@@ -1595,6 +1595,36 @@ public class FormServiceImplTest extends BaseAlfrescoSpringTest
{
// expected
}
+
+
+ // Tests to make sure that form processors are no longer decoding _ in the itemId
+
+ try
+ {
+ FormData data = new FormData();
+ data.addFieldData(TypeFormProcessor.DESTINATION, this.folder.toString());
+ this.formService.saveForm(new Item(TYPE_FORM_ITEM_KIND, "cm_content"), data);
+ fail("Expecting saveForm for a 'type' item kind containing an underscore to fail");
+ }
+ catch (Exception e)
+ {
+ // expected
+ }
+
+ try
+ {
+ FormData data = new FormData();
+ data.addFieldData("prop_bpm_workflowDescription", "This is a new adhoc task");
+ data.addFieldData("assoc_bpm_assignee_added",
+ this.personManager.get(USER_ONE).toString());
+ data.addFieldData("assoc_packageItems_added", this.document.toString());
+ this.formService.saveForm(new Item(WORKFLOW_FORM_ITEM_KIND, "jbpm$wf_adhoc"), data);
+ fail("Expecting saveForm for a 'workflow' item kind containing an underscore to fail");
+ }
+ catch (Exception e)
+ {
+ // expected
+ }
}
public void testFormData() throws Exception
diff --git a/source/java/org/alfresco/repo/forms/processor/node/PropertyFieldProcessor.java b/source/java/org/alfresco/repo/forms/processor/node/PropertyFieldProcessor.java
index af2b755c8f..286c030d7b 100644
--- a/source/java/org/alfresco/repo/forms/processor/node/PropertyFieldProcessor.java
+++ b/source/java/org/alfresco/repo/forms/processor/node/PropertyFieldProcessor.java
@@ -29,6 +29,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
+import org.alfresco.repo.dictionary.constraint.ListOfValuesConstraint;
import org.alfresco.repo.forms.Field;
import org.alfresco.repo.forms.FieldGroup;
import org.alfresco.repo.forms.PropertyFieldDefinition;
@@ -206,6 +207,26 @@ public class PropertyFieldProcessor extends QNameFieldProcessor params = constraint.getParameters();
+
+ // ListOfValuesConstraints have special handling for localising their allowedValues.
+ if (ListOfValuesConstraint.CONSTRAINT_TYPE.equals(constraint.getType()))
+ {
+ ListOfValuesConstraint lovConstraint = (ListOfValuesConstraint) constraint;
+ List allowedValues = lovConstraint.getAllowedValues();
+ List localisedValues = new ArrayList(allowedValues.size());
+
+ // Look up each localised display-label in turn.
+ for (String value : allowedValues)
+ {
+ String displayLabel = lovConstraint.getDisplayLabel(value);
+ // Change the allowedValue entry to the format the FormsService expects for localised strings: "value|label"
+ // If there is no localisation defined for any value, then this will give us "value|value".
+ localisedValues.add(value + "|" + displayLabel);
+ }
+
+ // Now replace the allowedValues param with our localised version.
+ params.put(ListOfValuesConstraint.ALLOWED_VALUES_PARAM, localisedValues);
+ }
FieldConstraint fieldConstraint = new FieldConstraint(type, params);
fieldConstraints.add(fieldConstraint);
}
diff --git a/source/java/org/alfresco/repo/forms/processor/node/TypeFormProcessor.java b/source/java/org/alfresco/repo/forms/processor/node/TypeFormProcessor.java
index 804490493a..8978af09cd 100644
--- a/source/java/org/alfresco/repo/forms/processor/node/TypeFormProcessor.java
+++ b/source/java/org/alfresco/repo/forms/processor/node/TypeFormProcessor.java
@@ -80,10 +80,8 @@ public class TypeFormProcessor extends ContentModelFormProcessor exten
try
{
ParameterCheck.mandatory("item", item);
- String itemId = decodeId(item.getId());
- return getTypedItemForDecodedId(itemId);
+ return getTypedItemForDecodedId(item.getId());
}
catch (Exception e)
{
@@ -111,22 +110,6 @@ public abstract class AbstractWorkflowFormProcessor exten
}
}
- /**
- * The itemId may be in a URL/Webscript-friendly format. If so it must be converted
- * back to the proper id format.
- *
- * @param itemId
- */
- private String decodeId(String itemId)
- {
- String decodedId = itemId;
- if (itemId.contains("$") == false)
- {
- decodedId = itemId.replaceFirst("_", Matcher.quoteReplacement("$"));
- }
- return decodedId;
- }
-
/* (non-Javadoc)
* @see org.alfresco.repo.forms.processor.FilteredFormProcessor#getDefaultIgnoredFields()
*/
diff --git a/source/java/org/alfresco/repo/forms/processor/workflow/TaskFormProcessorTest.java b/source/java/org/alfresco/repo/forms/processor/workflow/TaskFormProcessorTest.java
index b61151e8f7..75d32355e2 100644
--- a/source/java/org/alfresco/repo/forms/processor/workflow/TaskFormProcessorTest.java
+++ b/source/java/org/alfresco/repo/forms/processor/workflow/TaskFormProcessorTest.java
@@ -135,7 +135,7 @@ public class TaskFormProcessorTest extends TestCase
assertEquals(TASK_ID, result.getId());
// Check URI-encoded id.
- item = new Item("task", TASK_ID.replace('$', '_'));
+ item = new Item("task", TASK_ID);
result = processor.getTypedItem(item);
assertNotNull(result);
assertEquals(TASK_ID, result.getId());
diff --git a/source/java/org/alfresco/repo/forms/processor/workflow/WorkflowFormProcessor.java b/source/java/org/alfresco/repo/forms/processor/workflow/WorkflowFormProcessor.java
index 69a7d52c5f..957a66deee 100644
--- a/source/java/org/alfresco/repo/forms/processor/workflow/WorkflowFormProcessor.java
+++ b/source/java/org/alfresco/repo/forms/processor/workflow/WorkflowFormProcessor.java
@@ -121,8 +121,7 @@ public class WorkflowFormProcessor extends AbstractWorkflowFormProcessorname may be in a URL/Webscript-friendly format. If so it must be converted
- * back to the proper workflow definition name.
- * @param name
- * @return The decoded name
- */
- private String decodeWorkflowDefinitionName(String name)
- {
- if (name.contains(":") == false)
- {
- name = name.replaceFirst("_", ":");
- }
- return name;
- }
-
/* (non-Javadoc)
* @see org.alfresco.repo.forms.processor.workflow.AbstractWorkflowFormProcessor#makeFormPersister(java.lang.Object)
*/
diff --git a/source/java/org/alfresco/repo/forms/processor/workflow/WorkflowFormProcessorTest.java b/source/java/org/alfresco/repo/forms/processor/workflow/WorkflowFormProcessorTest.java
index 73ef6549be..c21a8f6c03 100644
--- a/source/java/org/alfresco/repo/forms/processor/workflow/WorkflowFormProcessorTest.java
+++ b/source/java/org/alfresco/repo/forms/processor/workflow/WorkflowFormProcessorTest.java
@@ -141,12 +141,6 @@ public class WorkflowFormProcessorTest extends TestCase
WorkflowDefinition result = processor.getTypedItem(item);
assertNotNull(result);
assertEquals(WF_DEF_NAME, result.getName());
-
- // Check URI-encoded id.
- Item itemWith_ = new Item("workflow", WF_DEF_NAME.replace('$', '_'));
- result = processor.getTypedItem(itemWith_);
- assertNotNull(result);
- assertEquals(WF_DEF_NAME, result.getName());
}
public void testGenerateSetsItemAndUrl() throws Exception
diff --git a/source/java/org/alfresco/repo/forum/CommentsTest.java b/source/java/org/alfresco/repo/forum/CommentsTest.java
new file mode 100644
index 0000000000..1874b84233
--- /dev/null
+++ b/source/java/org/alfresco/repo/forum/CommentsTest.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * 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 .
+ */
+
+package org.alfresco.repo.forum;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.alfresco.model.ContentModel;
+import org.alfresco.model.ForumModel;
+import org.alfresco.repo.content.MimetypeMap;
+import org.alfresco.repo.model.Repository;
+import org.alfresco.repo.security.authentication.AuthenticationUtil;
+import org.alfresco.repo.transaction.RetryingTransactionHelper;
+import org.alfresco.service.cmr.repository.ChildAssociationRef;
+import org.alfresco.service.cmr.repository.ContentData;
+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.namespace.NamespaceService;
+import org.alfresco.service.namespace.QName;
+import org.alfresco.util.ApplicationContextHelper;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.springframework.context.ApplicationContext;
+
+/**
+ * Test class for some {@link ForumModel forum model}-related functionality, specifically comments.
+ * There is no "CommentService" or "DiscussionService" and the REST API simply creates the appropriate
+ * content structure as required by the forum model.
+ *
+ * @author Neil McErlean
+ * @since 4.0
+ */
+public class CommentsTest
+{
+ private static final ApplicationContext testContext = ApplicationContextHelper.getApplicationContext();
+
+ // Services
+ private static ContentService contentService;
+ private static NodeService nodeService;
+ private static Repository repositoryHelper;
+ private static RetryingTransactionHelper transactionHelper;
+
+ // These NodeRefs are used by the test methods.
+ private NodeRef testFolder;
+ private List testDocs;
+
+ /**
+ * Initialise various services required by the test.
+ */
+ @BeforeClass public static void initTestsContext() throws Exception
+ {
+ contentService = (ContentService)testContext.getBean("ContentService");
+ nodeService = (NodeService)testContext.getBean("NodeService");
+ repositoryHelper = (Repository)testContext.getBean("repositoryHelper");
+ transactionHelper = (RetryingTransactionHelper)testContext.getBean("retryingTransactionHelper");
+
+ // Set the current security context as admin
+ AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName());
+ }
+
+ /**
+ * Create some content that can be commented on.
+ */
+ @Before public void initIndividualTestContext() throws Exception
+ {
+ transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback()
+ {
+ @Override
+ public Void execute() throws Throwable
+ {
+ // Create some content which we will comment on.
+ NodeRef companyHome = repositoryHelper.getCompanyHome();
+
+ testFolder = createNode(companyHome, "testFolder", ContentModel.TYPE_FOLDER);
+ testDocs = new ArrayList(3);
+ for (int i = 0; i < 3; i++)
+ {
+ NodeRef testNode = createNode(testFolder, "testDocInFolder", ContentModel.TYPE_CONTENT);
+ testDocs.add(testNode);
+ }
+
+ return null;
+ }
+ });
+ }
+
+ /**
+ * This method deletes any nodes which were created during test execution.
+ */
+ @After public void tidyUpTestNodes() throws Exception
+ {
+ transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback()
+ {
+ @Override
+ public Void execute() throws Throwable
+ {
+ AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName());
+
+ for (NodeRef nr : testDocs)
+ {
+ if (nodeService.exists(nr)) nodeService.deleteNode(nr);
+ }
+
+ return null;
+ }
+ });
+ }
+
+ /**
+ * Create a node of the specified content type, under the specified parent node with the specified cm:name.
+ */
+ private NodeRef createNode(NodeRef parentNode, String name, QName type)
+ {
+ Map props = new HashMap();
+ String fullName = name + System.currentTimeMillis();
+ props.put(ContentModel.PROP_NAME, fullName);
+ QName docContentQName = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, fullName);
+ NodeRef node = nodeService.createNode(parentNode,
+ ContentModel.ASSOC_CONTAINS,
+ docContentQName,
+ type,
+ props).getChildRef();
+ return node;
+ }
+
+ /**
+ * This test method comments on some nodes asserting that the commentCount rollup property
+ * responds correctly to the changing number of comments.
+ */
+ @Test public void commentOnDocsCheckingCommentCountRollup() throws Exception
+ {
+ transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback()
+ {
+ @Override
+ public Void execute() throws Throwable
+ {
+ // All test nodes are uncommented initially.
+ for (NodeRef nr : testDocs)
+ {
+ assertCommentCountIs(nr, 0);
+ }
+
+ // Comment on each node twice
+ Map> mapDiscussableToComments = new HashMap>();
+
+ for (NodeRef nr : testDocs)
+ {
+ final ArrayList comments = new ArrayList();
+ mapDiscussableToComments.put(nr, comments);
+
+ comments.add(applyComment(nr, "Test comment 1 " + System.currentTimeMillis()));
+ Thread.sleep(50); // 50 ms sleep so comments aren't simultaneous.
+
+ comments.add(applyComment(nr, "Test comment 2 " + System.currentTimeMillis()));
+ Thread.sleep(50);
+ }
+
+ // Check that the rollup comment counts are accurate.
+ for (NodeRef nr : testDocs)
+ {
+ assertCommentCountIs(nr, 2);
+ }
+
+ // Remove comments
+ for (Map.Entry> entry : mapDiscussableToComments.entrySet())
+ {
+ for (NodeRef commentNode : entry.getValue())
+ {
+ nodeService.deleteNode(commentNode);
+ }
+ }
+
+ // All test nodes are uncommented again.
+ for (NodeRef nr : testDocs)
+ {
+ assertCommentCountIs(nr, 0);
+ }
+
+ return null;
+ }
+ });
+ }
+
+ /**
+ * This method asserts that the commentCount (rollup) is as specified for the given node.
+ */
+ private void assertCommentCountIs(NodeRef discussableNode, int expectedCount)
+ {
+ final Serializable commentCount = nodeService.getProperty(discussableNode, ForumModel.PROP_COMMENT_COUNT);
+ if (expectedCount == 0)
+ {
+ assertTrue("Uncommented node should have EITHER no commentsRollup aspect OR commentCount of zero.",
+ !nodeService.hasAspect(discussableNode, ForumModel.ASPECT_COMMENTS_ROLLUP) ||
+ (commentCount != null && commentCount.equals(new Integer(0))
+ ));
+ }
+ else
+ {
+ assertTrue("Commented node should have discussable aspect.", nodeService.hasAspect(discussableNode, ForumModel.ASPECT_COMMENTS_ROLLUP));
+ assertEquals("Wrong comment count", expectedCount, commentCount);
+ }
+ }
+
+ /**
+ * This method applies the specified comment to the specified node.
+ * As there is no CommentService or DiscussionService, we mimic here what the comments REST API does,
+ * by manually creating the correct content structure using the nodeService. Behaviours will do some
+ * of the work for us. See comments.post.json.js for comparison.
+ * @param nr nodeRef to comment on.
+ * @param comment the text of the comment.
+ * @return the NodeRef of the fm:post comment node.
+ */
+ private NodeRef applyComment(NodeRef nr, String comment)
+ {
+ // There is no CommentService, so we have to create the node structure by hand.
+ // This is what happens within e.g. comment.put.json.js when comments are submitted via the REST API.
+ if (!nodeService.hasAspect(nr, ForumModel.ASPECT_DISCUSSABLE))
+ {
+ nodeService.addAspect(nr, ForumModel.ASPECT_DISCUSSABLE, null);
+ }
+ if (!nodeService.hasAspect(nr, ForumModel.ASPECT_COMMENTS_ROLLUP))
+ {
+ nodeService.addAspect(nr, ForumModel.ASPECT_COMMENTS_ROLLUP, null);
+ }
+ // Forum node is created automatically by DiscussableAspect behaviour.
+ NodeRef forumNode = nodeService.getChildAssocs(nr, ForumModel.ASSOC_DISCUSSION, QName.createQName(NamespaceService.FORUMS_MODEL_1_0_URI, "discussion")).get(0).getChildRef();
+
+ final List existingTopics = nodeService.getChildAssocs(forumNode, ContentModel.ASSOC_CONTAINS, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "Comments"));
+ NodeRef topicNode = null;
+ if (existingTopics.isEmpty())
+ {
+ topicNode = nodeService.createNode(forumNode, ContentModel.ASSOC_CONTAINS, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "Comments"), ForumModel.TYPE_TOPIC).getChildRef();
+ }
+ else
+ {
+ topicNode = existingTopics.get(0).getChildRef();
+ }
+
+ NodeRef postNode = nodeService.createNode(topicNode, ContentModel.ASSOC_CONTAINS, QName.createQName("comment" + System.currentTimeMillis()), ForumModel.TYPE_POST).getChildRef();
+ nodeService.setProperty(postNode, ContentModel.PROP_CONTENT, new ContentData(null, MimetypeMap.MIMETYPE_TEXT_PLAIN, 0L, null));
+ ContentWriter writer = contentService.getWriter(postNode, ContentModel.PROP_CONTENT, true);
+ writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN);
+ writer.setEncoding("UTF-8");
+ writer.putContent(comment);
+
+ return postNode;
+ }
+}
diff --git a/source/java/org/alfresco/repo/forum/ForumPostBehaviours.java b/source/java/org/alfresco/repo/forum/ForumPostBehaviours.java
new file mode 100644
index 0000000000..78ef889604
--- /dev/null
+++ b/source/java/org/alfresco/repo/forum/ForumPostBehaviours.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * 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 .
+ */
+package org.alfresco.repo.forum;
+
+import org.alfresco.model.ForumModel;
+import org.alfresco.repo.node.NodeServicePolicies;
+import org.alfresco.repo.node.NodeServicePolicies.OnCreateNodePolicy;
+import org.alfresco.repo.node.NodeServicePolicies.OnDeleteNodePolicy;
+import org.alfresco.repo.policy.JavaBehaviour;
+import org.alfresco.repo.policy.PolicyComponent;
+import org.alfresco.service.cmr.repository.ChildAssociationRef;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.repository.NodeService;
+
+/**
+ * This class registers behaviours for the {@link ForumModel#TYPE_POST fm:post} content type.
+ * These behaviours maintain the correct value for the {@link ForumModel#PROP_COMMENT_COUNT comment count rollup property}.
+ *
+ * @author Neil Mc Erlean
+ * @since 4.0
+ */
+public class ForumPostBehaviours implements NodeServicePolicies.OnCreateNodePolicy,
+ NodeServicePolicies.OnDeleteNodePolicy
+{
+ private PolicyComponent policyComponent;
+ private NodeService nodeService;
+
+ public void setPolicyComponent(PolicyComponent policyComponent)
+ {
+ this.policyComponent = policyComponent;
+ }
+
+ public void setNodeService(NodeService nodeService)
+ {
+ this.nodeService = nodeService;
+ }
+
+ /**
+ * Initialise method
+ */
+ public void init()
+ {
+ this.policyComponent.bindClassBehaviour(
+ OnCreateNodePolicy.QNAME,
+ ForumModel.TYPE_POST,
+ new JavaBehaviour(this, "onCreateNode"));
+ this.policyComponent.bindClassBehaviour(
+ OnDeleteNodePolicy.QNAME,
+ ForumModel.TYPE_POST,
+ new JavaBehaviour(this, "onDeleteNode"));
+ }
+
+ @Override
+ public void onCreateNode(ChildAssociationRef childAssocRef)
+ {
+ // We have a new comment under a discussable node.
+ // We need to find the fm:commentsCount ancestor to this comment node and increment its commentCount
+ NodeRef commentsRollupNode = getCommentsRollupAncestor(childAssocRef.getParentRef());
+
+ if (commentsRollupNode != null)
+ {
+ int existingCommentCount = (Integer) nodeService.getProperty(commentsRollupNode, ForumModel.PROP_COMMENT_COUNT);
+ nodeService.setProperty(commentsRollupNode, ForumModel.PROP_COMMENT_COUNT, existingCommentCount + 1);
+ }
+ }
+
+ @Override
+ public void onDeleteNode(ChildAssociationRef childAssocRef,
+ boolean isNodeArchived)
+ {
+ // We have one less comment under a discussable node.
+ // We need to find the fm:commentsRollup ancestor to this comment node and decrement its commentCount
+
+ NodeRef commentsRollupNode = getCommentsRollupAncestor(childAssocRef.getParentRef());
+
+ if (commentsRollupNode != null)
+ {
+ int existingCommentCount = (Integer) nodeService.getProperty(commentsRollupNode, ForumModel.PROP_COMMENT_COUNT);
+ int newCommentCount = Math.max(0, existingCommentCount - 1); // Negative values should not occur, but we'll stop them anyway.
+ nodeService.setProperty(commentsRollupNode, ForumModel.PROP_COMMENT_COUNT, newCommentCount);
+ }
+ }
+
+ /**
+ * This method navigates up the primary parent containment path to find the ancestor with the
+ * {@link ForumModel#ASPECT_COMMENTS_ROLLUP commentsRollup} aspect.
+ *
+ * @param topicNode
+ * @return the NodeRef of the commentsRollup ancestor if there is one (which there always should be), else null
.
+ */
+ private NodeRef getCommentsRollupAncestor(NodeRef topicNode)
+ {
+ NodeRef forumNode = nodeService.getPrimaryParent(topicNode).getParentRef();
+ NodeRef commentsRollupNode = nodeService.getPrimaryParent(forumNode).getParentRef();
+ if (! nodeService.hasAspect(commentsRollupNode, ForumModel.ASPECT_COMMENTS_ROLLUP))
+ {
+ return null;
+ }
+ else
+ {
+ return commentsRollupNode;
+ }
+ }
+}
diff --git a/source/java/org/alfresco/repo/jscript/ScriptNode.java b/source/java/org/alfresco/repo/jscript/ScriptNode.java
index bd94997694..8ffbeba529 100644
--- a/source/java/org/alfresco/repo/jscript/ScriptNode.java
+++ b/source/java/org/alfresco/repo/jscript/ScriptNode.java
@@ -62,8 +62,8 @@ import org.alfresco.service.cmr.lock.LockStatus;
import org.alfresco.service.cmr.model.FileFolderService;
import org.alfresco.service.cmr.model.FileInfo;
import org.alfresco.service.cmr.model.FileNotFoundException;
+import org.alfresco.service.cmr.model.PagingFileInfoRequest;
import org.alfresco.service.cmr.model.PagingFileInfoResults;
-import org.alfresco.service.cmr.model.PagingSortRequest;
import org.alfresco.service.cmr.repository.AssociationRef;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentData;
@@ -72,7 +72,6 @@ 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.PagingSortProp;
import org.alfresco.service.cmr.repository.Path;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.repository.TemplateImageResolver;
@@ -94,6 +93,7 @@ import org.alfresco.service.namespace.QName;
import org.alfresco.service.namespace.RegexQNamePattern;
import org.alfresco.util.GUID;
import org.alfresco.util.ISO9075;
+import org.alfresco.util.Pair;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.json.JSONException;
@@ -532,6 +532,7 @@ public class ScriptNode implements Serializable, Scopeable, NamespacePrefixResol
/**
* @param files Return files extending from cm:content
* @param folders Return folders extending from cm:folder - ignoring sub-types of cm:systemfolder
+ *
* @return Returns a JavaScript array of child file/folder nodes for this nodes.
* Automatically retrieves all sub-types of cm:content and cm:folder, also removes
* system folder types from the results.
@@ -543,22 +544,67 @@ public class ScriptNode implements Serializable, Scopeable, NamespacePrefixResol
}
/**
- * @param files Return files extending from cm:content
- * @param folders Return folders extending from cm:folder - ignoring sub-types of cm:systemfolder
+ * @param files Return files extending from cm:content
+ * @param folders Return folders extending from cm:folder - ignoring sub-types of cm:systemfolder
+ * @param ignoreTypes Also optionally removes additional type qnames. The additional type can be
+ * specified in short or long qname string form as a single string or an Array e.g. "fm:forum".
+ *
* @return Returns a JavaScript array of child file/folder nodes for this nodes.
* Automatically retrieves all sub-types of cm:content and cm:folder, also removes
* system folder types from the results.
- * Also optionally removes additional type qnames. The additional type can be
- * specified in short or long qname string form as a single string or an Array e.g. "fm:forum".
* This is equivalent to @see FileFolderService.listFiles() and @see FileFolderService.listFolders()
*/
public Scriptable childFileFolders(boolean files, boolean folders, Object ignoreTypes)
{
- return childFileFolders(files, folders, ignoreTypes, -1, -1, null, true).getResult();
+ return childFileFolders(files, folders, ignoreTypes, -1, -1, 0, null, null, null).getPage();
+ }
+
+ /**
+ * @param files Return files extending from cm:content
+ * @param folders Return folders extending from cm:folder - ignoring sub-types of cm:systemfolder
+ * @param ignoreTypes Also optionally removes additional type qnames. The additional type can be
+ * specified in short or long qname string form as a single string or an Array e.g. "fm:forum".
+ * @param maxItems Max number of items
+ *
+ * @return Returns ScriptPagingNodes which includes a JavaScript array of child file/folder nodes for this nodes.
+ * Automatically retrieves all sub-types of cm:content and cm:folder, also removes
+ * system folder types from the results.
+ * This is equivalent to @see FileFolderService.listFiles() and @see FileFolderService.listFolders()
+ *
+ * @deprecated API for review (subject to change prior to release)
+ *
+ * @author janv
+ * @since 4.0
+ */
+ public ScriptPagingNodes childFileFolders(boolean files, boolean folders, Object ignoreTypes, int maxItems)
+ {
+ return childFileFolders(files, folders, ignoreTypes, 0, maxItems, 0, null, null, null);
}
@SuppressWarnings("unchecked")
- public ScriptPagingNodes childFileFolders(boolean files, boolean folders, Object ignoreTypes, int skipOffset, int maxItems, String sortProp, boolean ascending)
+ /**
+ * @param files Return files extending from cm:content
+ * @param folders Return folders extending from cm:folder - ignoring sub-types of cm:systemfolder
+ * @param ignoreTypes Also optionally removes additional type qnames. The additional type can be
+ * specified in short or long qname string form as a single string or an Array e.g. "fm:forum".
+ * @param skipOffset Items to skip (e.g. 0 or (num pages to skip * size of page)
+ * @param maxItems Max number of items (eg. size of page)
+ * @param requestTotalCountMax Request total count (upto a given max total count)
+ * Note: if 0 then total count is not requested and the query may be able to optimise/cutoff for max items)
+ * @param sortProp Optional sort property as a prefix qname string (e.g. "cm:name"). Also supports special
+ * content case (i.e. "cm:content.size" and "cm:content.mimetype")
+ * @param sortAsc Given a sort property, true => ascending, false => descending
+ * @param queryExecutionId If paging then can pass back the previous query execution (as a hint for possible query optimisation)
+ *
+ * @return Returns ScriptPagingNodes which includes a JavaScript array of child file/folder nodes for this nodes.
+ * Automatically retrieves all sub-types of cm:content and cm:folder, also removes
+ * system folder types from the results.
+ * This is equivalent to @see FileFolderService.listFiles() and @see FileFolderService.listFolders()
+ *
+ * @author janv
+ * @since 4.0
+ */
+ public ScriptPagingNodes childFileFolders(boolean files, boolean folders, Object ignoreTypes, int skipOffset, int maxItems, int requestTotalCountMax, String sortProp, Boolean sortAsc, String queryExecutionId)
{
Object[] results;
@@ -585,18 +631,19 @@ public class ScriptNode implements Serializable, Scopeable, NamespacePrefixResol
ignoreTypeQNames.add(createQName(ignoreTypes.toString()));
}
- List sortProps = null; // note: null sortProps => get all in default sort order
+ List> sortProps = null; // note: null sortProps => get all in default sort order
if (sortProp != null)
{
- sortProps = new ArrayList(1);
- sortProps.add(new PagingSortProp(createQName(sortProp), ascending));
+ sortProps = new ArrayList>(1);
+ sortProps.add(new Pair(createQName(sortProp), sortAsc));
}
- PagingSortRequest pageRequest = new PagingSortRequest(skipOffset, maxItems, true, sortProps);
+ PagingFileInfoRequest pageRequest = new PagingFileInfoRequest(skipOffset, maxItems, sortProps, queryExecutionId);
+ pageRequest.setRequestTotalCountMax(requestTotalCountMax);
PagingFileInfoResults pageOfNodeInfos = this.fileFolderService.list(this.nodeRef, files, folders, ignoreTypeQNames, pageRequest);
- List nodeInfos = pageOfNodeInfos.getResultsForPage();
+ List nodeInfos = pageOfNodeInfos.getPage();
int size = nodeInfos.size();
results = new Object[size];
@@ -606,7 +653,17 @@ public class ScriptNode implements Serializable, Scopeable, NamespacePrefixResol
results[i] = newInstance(nodeInfo, this.services, this.scope);
}
- return new ScriptPagingNodes(Context.getCurrentContext().newArray(this.scope, results), pageOfNodeInfos.getTotalCount(), pageOfNodeInfos.hasMore());
+ int totalResultCountLower = -1;
+ int totalResultCountUpper = -1;
+
+ Pair totalResultCount = pageOfNodeInfos.getTotalResultCount();
+ if (totalResultCount != null)
+ {
+ totalResultCountLower = (totalResultCount.getFirst() != null ? totalResultCount.getFirst() : -1);
+ totalResultCountUpper = (totalResultCount.getSecond() != null ? totalResultCount.getSecond() : -1);
+ }
+
+ return new ScriptPagingNodes(Context.getCurrentContext().newArray(this.scope, results), pageOfNodeInfos.hasMoreItems(), totalResultCountLower, totalResultCountUpper);
}
/**
@@ -2922,7 +2979,7 @@ public class ScriptNode implements Serializable, Scopeable, NamespacePrefixResol
if (this.nodeService.exists(nodeRef))
{
- if(this.services.getPublicServiceAccessService().hasAccess(this.services.NODE_SERVICE.getLocalName(), "getProperties", this.nodeRef) == AccessStatus.ALLOWED)
+ if(this.services.getPublicServiceAccessService().hasAccess(ServiceRegistry.NODE_SERVICE.getLocalName(), "getProperties", this.nodeRef) == AccessStatus.ALLOWED)
{
JSONObject json = new JSONObject();
diff --git a/source/java/org/alfresco/repo/jscript/ScriptPagingNodes.java b/source/java/org/alfresco/repo/jscript/ScriptPagingNodes.java
index 95f600364e..8248b3036d 100644
--- a/source/java/org/alfresco/repo/jscript/ScriptPagingNodes.java
+++ b/source/java/org/alfresco/repo/jscript/ScriptPagingNodes.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2005-2010 Alfresco Software Limited.
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
*
* This file is part of Alfresco
*
@@ -23,37 +23,45 @@ import java.io.Serializable;
import org.mozilla.javascript.Scriptable;
/**
- * TEMP
+ * Response for page of ScriptNode results
*
- * @deprecated for review (API is subject to change)
+ * @author janv
+ * @version 4.0
*/
public class ScriptPagingNodes implements Serializable
{
private static final long serialVersionUID = -3252996649397737176L;
- private Scriptable result; // array of script nodes
- private Boolean hasMore; // null => unknown
- private Long totalCount; // null => not requested (or unknown)
+ private Scriptable results; // array of script nodes
+ private Boolean hasMoreItems; // true if has more items (past this page) - note: could also indicate cutoff/trimmed page
+ private int totalResultCountLower; // possible total count lower estimate (-1 => unknown)
+ private int totalResultCountUpper; // possible total count upper estimate (-1 => unknown)
- public ScriptPagingNodes(Scriptable result, Long totalCount, Boolean hasMore)
+ public ScriptPagingNodes(Scriptable results, Boolean hasMoreItems, int totalResultCountLower, int totalResultCountUpper)
{
- this.result = result;
- this.totalCount = totalCount;
- this.hasMore = hasMore;
+ this.results = results;
+ this.hasMoreItems = hasMoreItems;
+ this.totalResultCountLower = totalResultCountLower;
+ this.totalResultCountUpper = totalResultCountUpper;
}
- public Scriptable getResult()
+ public Scriptable getPage()
{
- return result;
+ return results;
}
- public Long getTotalCount()
+ public Boolean hasMoreItems()
{
- return totalCount;
+ return hasMoreItems;
}
- public Boolean hasMore()
+ public int getTotalResultCountLower()
{
- return hasMore;
+ return totalResultCountLower;
+ }
+
+ public int getTotalResultCountUpper()
+ {
+ return totalResultCountUpper;
}
}
diff --git a/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImpl.java b/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImpl.java
index b880f11339..41640773fb 100644
--- a/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImpl.java
+++ b/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImpl.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2005-2010 Alfresco Software Limited.
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
*
* This file is part of Alfresco
*
@@ -35,13 +35,8 @@ import java.util.Stack;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.query.CannedQueryFactory;
-import org.alfresco.query.CannedQueryPageDetails;
-import org.alfresco.query.CannedQueryParameters;
import org.alfresco.query.CannedQueryResults;
-import org.alfresco.query.CannedQuerySortDetails;
-import org.alfresco.query.CannedQuerySortDetails.SortOrder;
import org.alfresco.repo.search.QueryParameterDefImpl;
-import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.model.FileExistsException;
@@ -50,8 +45,8 @@ import org.alfresco.service.cmr.model.FileFolderServiceType;
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.model.PagingFileInfoRequest;
import org.alfresco.service.cmr.model.PagingFileInfoResults;
-import org.alfresco.service.cmr.model.PagingSortRequest;
import org.alfresco.service.cmr.model.SubFolderFilter;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentData;
@@ -64,7 +59,6 @@ import org.alfresco.service.cmr.repository.InvalidNodeRefException;
import org.alfresco.service.cmr.repository.MimetypeService;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
-import org.alfresco.service.cmr.repository.PagingSortProp;
import org.alfresco.service.cmr.repository.Path;
import org.alfresco.service.cmr.search.QueryParameterDefinition;
import org.alfresco.service.cmr.search.SearchService;
@@ -119,7 +113,7 @@ public class FileFolderServiceImpl implements FileFolderService
" or subtypeOf('" + ContentModel.TYPE_LINK + "'))]";
private static Log logger = LogFactory.getLog(FileFolderServiceImpl.class);
-
+
private NamespaceService namespaceService;
private DictionaryService dictionaryService;
private NodeService nodeService;
@@ -134,6 +128,9 @@ public class FileFolderServiceImpl implements FileFolderService
// TODO: Replace this with a more formal means of identifying "system" folders (i.e. aspect or UUID)
private List systemPaths;
+ // applies to list "all" methods - note: final result count also depends on "system.acl.maxPermissionCheckTimeMillis"
+ private int defaultListMaxResults = 5000;
+
/**
* Default constructor
*/
@@ -207,6 +204,11 @@ public class FileFolderServiceImpl implements FileFolderService
this.systemPaths = systemPaths;
}
+ public void setDefaultListMaxResults(int defaultListMaxResults)
+ {
+ this.defaultListMaxResults = defaultListMaxResults;
+ }
+
public void init()
{
@@ -335,88 +337,61 @@ public class FileFolderServiceImpl implements FileFolderService
/* (non-Javadoc)
* @see org.alfresco.service.cmr.model.FileFolderService#list(org.alfresco.service.cmr.repository.NodeRef, boolean, boolean, java.util.Set, org.alfresco.service.cmr.model.PagingSortRequest)
*/
- public PagingFileInfoResults list(NodeRef contextNodeRef, boolean files, boolean folders, Set ignoreQNameTypes, PagingSortRequest pagingRequest)
+ public PagingFileInfoResults list(NodeRef contextNodeRef, boolean files, boolean folders, Set ignoreQNameTypes, PagingFileInfoRequest pagingRequest)
{
- long start = System.currentTimeMillis();
+ ParameterCheck.mandatory("contextNodeRef", contextNodeRef);
+ ParameterCheck.mandatory("pagingRequest", pagingRequest);
- Set searchTypeQNames = buildTypes(files, folders, ignoreQNameTypes);
-
- // specific query params
- GetChildrenCannedQueryParams paramBean = new GetChildrenCannedQueryParams(contextNodeRef, searchTypeQNames);
-
- CannedQueryPageDetails cqpd = null;
- CannedQuerySortDetails cqsd = null;
- boolean requestTotalCount = true;
- Integer pageSize = null;
- int skipCount = -1;
- int maxItems = -1;
-
- if (pagingRequest != null)
- {
- skipCount = (pagingRequest.getSkipCount() == -1 ? CannedQueryPageDetails.DEFAULT_SKIP_RESULTS : pagingRequest.getSkipCount());
- maxItems = (pagingRequest.getMaxItems() == -1 ? CannedQueryPageDetails.DEFAULT_PAGE_SIZE : pagingRequest.getMaxItems());
-
- // page details
- pageSize = (maxItems == CannedQueryPageDetails.DEFAULT_PAGE_SIZE ? maxItems : maxItems + 1); // TODO: add 1 to support hasMore (see below) - push down to canned query fwk
- cqpd = new CannedQueryPageDetails(skipCount, pageSize, CannedQueryPageDetails.DEFAULT_PAGE_NUMBER, CannedQueryPageDetails.DEFAULT_PAGE_COUNT);
-
- // sort details
- if (pagingRequest.getSortProps() != null)
- {
- List> sortPairs = new ArrayList>(pagingRequest.getSortProps().size());
- for (PagingSortProp sortProp : pagingRequest.getSortProps())
- {
- sortPairs.add(new Pair(sortProp.getSortProp(), (sortProp.isAscending() ? SortOrder.ASCENDING : SortOrder.DESCENDING)));
- }
-
- cqsd = new CannedQuerySortDetails(sortPairs);
- }
-
- requestTotalCount = pagingRequest.requestTotalCount();
- }
-
- // create query params holder
- CannedQueryParameters params = new CannedQueryParameters(paramBean, cqpd, cqsd, AuthenticationUtil.getRunAsUser(), requestTotalCount, null);
-
- // get canned query
- GetChildrenCannedQueryFactory getChildrenCannedQueryFactory = (GetChildrenCannedQueryFactory)cannedQueryRegistry.getNamedObject("getChildrenCannedQueryFactory");
- GetChildrenCannedQuery cq = (GetChildrenCannedQuery)getChildrenCannedQueryFactory.getCannedQuery(params);
-
- // execute canned query
- CannedQueryResults results = cq.execute();
+ // execute query
+ CannedQueryResults results = listImpl(contextNodeRef, files, folders, ignoreQNameTypes, pagingRequest);
List nodeRefs = results.getPages().get(0);
- boolean hasMore = false;
- if ((pageSize != null) && (results.getPagedResultCount() == pageSize))
+ // set total count
+ Pair totalCount = null;
+ if (pagingRequest.getRequestTotalCountMax() > 0)
{
- // TODO: remove 1 to support hasMore (see above) - push down to canned query fwk
- hasMore = true;
- nodeRefs.remove(nodeRefs.size() - 1);
+ totalCount = results.getTotalResultCount();
}
+ boolean hasMoreItems = results.hasMoreItems();
+
List nodeInfos = new ArrayList(nodeRefs.size());
for (NodeRef nodeRef : nodeRefs)
{
nodeInfos.add(toFileInfo(nodeRef, true));
}
- long totalCount = new Long(results.getTotalResultCount());
+ return new PagingFileInfoResultsImpl(nodeInfos, hasMoreItems, totalCount, results.getQueryExecutionId(), true);
+ }
+
+ private CannedQueryResults listImpl(NodeRef contextNodeRef, boolean files, boolean folders, Set ignoreQNameTypes, PagingFileInfoRequest pagingRequest)
+ {
+ long start = System.currentTimeMillis();
+
+ Set searchTypeQNames = buildTypes(files, folders, ignoreQNameTypes);
+
+ // get canned query
+ GetChildrenCannedQueryFactory getChildrenCannedQueryFactory = (GetChildrenCannedQueryFactory)cannedQueryRegistry.getNamedObject("getChildrenCannedQueryFactory");
+
+ GetChildrenCannedQuery cq = (GetChildrenCannedQuery)getChildrenCannedQueryFactory.getCannedQuery(contextNodeRef, searchTypeQNames, pagingRequest, pagingRequest.getSortProps());
+
+ // execute canned query
+ CannedQueryResults results = cq.execute();
if (logger.isInfoEnabled())
{
- if ((skipCount == -1) && (maxItems == -1))
- {
- logger.info("List: "+nodeInfos.size()+" items [total="+totalCount+"] in "+(System.currentTimeMillis()-start)+" msecs");
- }
- else
- {
- int pageNum = (skipCount / maxItems) + 1;
- logger.info("List: page "+pageNum+" of "+nodeInfos.size()+" items [skip="+skipCount+",max="+maxItems+",total="+totalCount+"] in "+(System.currentTimeMillis()-start)+" msecs");
- }
+ int cnt = results.getPagedResultCount();
+ int skipCount = pagingRequest.getSkipCount();
+ int maxItems = pagingRequest.getMaxItems();
+ boolean hasMoreItems = results.hasMoreItems();
+ Pair totalCount = (pagingRequest.getRequestTotalCountMax() > 0 ? results.getTotalResultCount() : null);
+ int pageNum = (skipCount / maxItems) + 1;
+
+ logger.info("List: "+cnt+" items in "+(System.currentTimeMillis()-start)+" msecs [pageNum="+pageNum+",skip="+skipCount+",max="+maxItems+",hasMorePages="+hasMoreItems+",totalCount="+totalCount+",parentNodeRef="+contextNodeRef+"]");
}
- return new PagingFileInfoResultsImpl(nodeInfos, hasMore, totalCount);
+ return results;
}
private List list(NodeRef contextNodeRef, boolean files, boolean folders)
@@ -679,30 +654,8 @@ public class FileFolderServiceImpl implements FileFolderService
private List listSimple(NodeRef contextNodeRef, boolean files, boolean folders)
{
- return listSimple(contextNodeRef, files, folders, null);
- }
-
- private List listSimple(NodeRef contextNodeRef, boolean files, boolean folders, Set ignoreQNameTypes)
- {
- Set searchTypeQNames = buildTypes(files, folders, ignoreQNameTypes);
-
- // Shortcut
- if (searchTypeQNames.size() == 0)
- {
- return Collections.emptyList();
- }
-
- // Do the query
- List childAssocRefs = nodeService.getChildAssocs(contextNodeRef, searchTypeQNames);
-
- List result = new ArrayList(childAssocRefs.size());
- for (ChildAssociationRef assocRef : childAssocRefs)
- {
- result.add(assocRef.getChildRef());
- }
-
- // Done
- return result;
+ PagingFileInfoRequest pagingRequest = new PagingFileInfoRequest(0, defaultListMaxResults, null, null);
+ return listImpl(contextNodeRef, files, folders, null, pagingRequest).getPage();
}
private Set buildTypes(boolean files, boolean folders, Set ignoreQNameTypes)
diff --git a/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImplTest.java b/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImplTest.java
index 5f3fdf12be..49af5d73cc 100644
--- a/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImplTest.java
+++ b/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImplTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2005-2010 Alfresco Software Limited.
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
*
* This file is part of Alfresco
*
@@ -51,6 +51,8 @@ import org.alfresco.service.cmr.model.FileFolderService;
import org.alfresco.service.cmr.model.FileFolderServiceType;
import org.alfresco.service.cmr.model.FileInfo;
import org.alfresco.service.cmr.model.FileNotFoundException;
+import org.alfresco.service.cmr.model.PagingFileInfoRequest;
+import org.alfresco.service.cmr.model.PagingFileInfoResults;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter;
@@ -68,6 +70,7 @@ import org.alfresco.service.namespace.RegexQNamePattern;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.ApplicationContextHelper;
import org.alfresco.util.GUID;
+import org.alfresco.util.Pair;
import org.springframework.context.ApplicationContext;
import org.springframework.extensions.surf.util.I18NUtil;
@@ -213,6 +216,28 @@ public class FileFolderServiceImplTest extends TestCase
{ NAME_L0_FILE_A, NAME_L0_FILE_B, NAME_L0_FOLDER_A, NAME_L0_FOLDER_B, NAME_L0_FOLDER_C };
checkFileList(files, 2, 3, expectedNames);
}
+
+ public void testListPage() throws Exception
+ {
+ // sanity check only (see also GetChildrenCannedQueryTest)
+
+ PagingFileInfoRequest pagingRequest = new PagingFileInfoRequest(100, null);
+ PagingFileInfoResults pagingResults = fileFolderService.list(workingRootNodeRef, true, true, null, pagingRequest);
+
+ assertNotNull(pagingResults);
+ assertFalse(pagingResults.hasMoreItems());
+ assertTrue((pagingResults.getQueryExecutionId() != null) && (pagingResults.getQueryExecutionId().length() > 0));
+ assertNull(pagingResults.getTotalResultCount());
+
+ List files = pagingResults.getPage();
+
+ // check
+ String[] expectedNames = new String[]
+ { NAME_L0_FILE_A, NAME_L0_FILE_B, NAME_L0_FOLDER_A, NAME_L0_FOLDER_B, NAME_L0_FOLDER_C };
+ checkFileList(files, 2, 3, expectedNames);
+
+ // TODO add more here
+ }
public void testShallowFilesOnlyList() throws Exception
{
diff --git a/source/java/org/alfresco/repo/model/filefolder/GetChildrenCannedQuery.java b/source/java/org/alfresco/repo/model/filefolder/GetChildrenCannedQuery.java
index 9cb6b5822b..43a85de1be 100644
--- a/source/java/org/alfresco/repo/model/filefolder/GetChildrenCannedQuery.java
+++ b/source/java/org/alfresco/repo/model/filefolder/GetChildrenCannedQuery.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2005-2010 Alfresco Software Limited.
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
*
* This file is part of Alfresco
*
@@ -18,131 +18,295 @@
*/
package org.alfresco.repo.model.filefolder;
-import java.lang.reflect.AccessibleObject;
+import java.io.Serializable;
import java.lang.reflect.Method;
+import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
-import net.sf.acegisecurity.Authentication;
-import net.sf.acegisecurity.ConfigAttributeDefinition;
-import net.sf.acegisecurity.context.Context;
-import net.sf.acegisecurity.context.ContextHolder;
-import net.sf.acegisecurity.context.security.SecureContext;
-
+import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
-import org.alfresco.query.AbstractCannedQuery;
import org.alfresco.query.CannedQueryParameters;
import org.alfresco.query.CannedQuerySortDetails;
+import org.alfresco.query.PagingResults;
import org.alfresco.query.CannedQuerySortDetails.SortOrder;
+import org.alfresco.repo.domain.node.AuditablePropertiesEntity;
+import org.alfresco.repo.domain.node.NodeDAO;
+import org.alfresco.repo.domain.node.NodeEntity;
+import org.alfresco.repo.domain.node.NodePropertyEntity;
+import org.alfresco.repo.domain.node.NodePropertyHelper;
+import org.alfresco.repo.domain.node.NodePropertyKey;
+import org.alfresco.repo.domain.node.NodePropertyValue;
+import org.alfresco.repo.domain.qname.QNameDAO;
+import org.alfresco.repo.domain.query.CannedQueryDAO;
+import org.alfresco.repo.security.permissions.impl.acegi.AbstractCannedQueryPermissions;
import org.alfresco.repo.security.permissions.impl.acegi.MethodSecurityInterceptor;
-import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentData;
+import org.alfresco.service.cmr.repository.InvalidNodeRefException;
import org.alfresco.service.cmr.repository.MLText;
import org.alfresco.service.cmr.repository.NodeRef;
-import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.Pair;
-import org.aopalliance.intercept.MethodInvocation;
+import org.alfresco.util.ParameterCheck;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
- * Canned query to get children of a parent node
+ * GetChidren canned query - to get paged list of children of a parent node (sorted or unsorted)
*
* @author janv
* @since 4.0
*/
-public class GetChildrenCannedQuery extends AbstractCannedQuery
+public class GetChildrenCannedQuery extends AbstractCannedQueryPermissions
{
private Log logger = LogFactory.getLog(getClass());
- private NodeService nodeService;
- private MethodSecurityInterceptor methodSecurityInterceptor;
+ private static final String QUERY_NAMESPACE = "alfresco.node";
+ private static final String QUERY_SELECT_GET_CHILDREN_SORTED = "select_GetChildrenSortedCannedQuery";
+ private static final String QUERY_SELECT_GET_CHILDREN = "select_GetChildrenCannedQuery";
+
+ public static final int MAX_SORT_PAIRS = 2;
+
+ // note: speical qnames - originally from Share DocLib default config (however, we do not support arbitrary "fts-alfresco" special sortable fields)
+ public static final QName SORT_QNAME_CONTENT_SIZE = QName.createQName("http://www.alfresco.org/model/content/1.0", "content.size");
+ public static final QName SORT_QNAME_CONTENT_MIMETYPE = QName.createQName("http://www.alfresco.org/model/content/1.0", "content.mimetype");
+ public static final QName SORT_QNAME_NODE_TYPE = QName.createQName("", "TYPE");
+
+
+ private NodeDAO nodeDAO;
+ private QNameDAO qnameDAO;
+ private CannedQueryDAO cannedQueryDAO;
+ private NodePropertyHelper nodePropertyHelper;
+ private boolean sorted = true;
public GetChildrenCannedQuery(
- NodeService nodeService,
+ NodeDAO nodeDAO,
+ QNameDAO qnameDAO,
+ CannedQueryDAO cannedQueryDAO,
+ NodePropertyHelper nodePropertyHelper,
MethodSecurityInterceptor methodSecurityInterceptor,
+ Method method,
CannedQueryParameters params,
String queryExecutionId)
{
- super(params, queryExecutionId);
+ super(params, queryExecutionId, methodSecurityInterceptor, method);
- this.nodeService = nodeService;
- this.methodSecurityInterceptor = methodSecurityInterceptor;
+ this.nodeDAO = nodeDAO;
+ this.qnameDAO = qnameDAO;
+ this.cannedQueryDAO = cannedQueryDAO;
+ this.nodePropertyHelper = nodePropertyHelper;
+
+ if ((params.getSortDetails() == null) || (params.getSortDetails().getSortPairs().size() == 0))
+ {
+ sorted = false;
+ }
}
@Override
protected List query(CannedQueryParameters parameters)
{
- long start = System.currentTimeMillis();
+ Long start = (logger.isDebugEnabled() ? System.currentTimeMillis() : null);
+ // Get parameters
GetChildrenCannedQueryParams paramBean = (GetChildrenCannedQueryParams)parameters.getParameterBean();
- List childAssocRefs = nodeService.getChildAssocs(paramBean.getParentRef(), paramBean.getSearchTypeQNames());
+ NodeRef parentRef = paramBean.getParentRef();
+ Set childNodeTypeQNames = paramBean.getSearchTypeQNames();
- List result = new ArrayList(childAssocRefs.size());
- for (ChildAssociationRef assocRef : childAssocRefs)
+ CannedQuerySortDetails sortDetails = parameters.getSortDetails();
+ List> sortPairs = sortDetails.getSortPairs();
+
+ ParameterCheck.mandatory("nodeRef", parentRef);
+
+ Pair nodePair = nodeDAO.getNodePair(parentRef);
+ if (nodePair == null)
{
- result.add(assocRef.getChildRef());
+ throw new InvalidNodeRefException("Node does not exist: " + parentRef, parentRef);
}
- if (logger.isDebugEnabled())
+ Long parentNodeId = nodePair.getFirst();
+
+ // Set query params - note: currently using SortableChildEntity to hold (supplemental-) query params
+ SortableNodeEntity params = new SortableNodeEntity();
+
+ // Set parent node id
+ params.setParentNodeId(parentNodeId);
+
+ // Set sort props
+ int sortPairsCnt = setSortParams(sortPairs, params);
+
+ // Set child node type qnames
+ if (childNodeTypeQNames != null)
{
- logger.debug("Raw query: "+result.size()+" in "+(System.currentTimeMillis()-start)+" msecs");
+ Set childNodeTypeQNameIds = qnameDAO.convertQNamesToIds(childNodeTypeQNames, false);
+ if (childNodeTypeQNameIds.size() > 0)
+ {
+ params.setChildNodeTypeQNameIds(new ArrayList(childNodeTypeQNameIds));
+ }
+ }
+
+ final List result;
+
+ if (sortPairsCnt > 0)
+ {
+ final List children = new ArrayList(100);
+
+ SortedChildQueryCallback callback = new SortedChildQueryCallback()
+ {
+ public boolean handle(NodeRef nodeRef, Map sortPropVals)
+ {
+ children.add(new SortableNode(nodeRef, sortPropVals));
+ // More results
+ return true;
+ }
+ };
+
+ SortedResultHandler resultHandler = new SortedResultHandler(callback);
+ cannedQueryDAO.executeQuery(QUERY_NAMESPACE, QUERY_SELECT_GET_CHILDREN_SORTED, params, 0, Integer.MAX_VALUE, resultHandler);
+ resultHandler.done();
+
+ // sort
+ Collections.sort(children, new PropComparatorAsc(sortPairs));
+
+ result = new ArrayList(children.size());
+ for (SortableNode child : children)
+ {
+ result.add(child.getNodeRef());
+ }
+ }
+ else
+ {
+ int requestedCount = parameters.getPageDetails().getResultsRequiredForPaging();
+ if (requestedCount != Integer.MAX_VALUE)
+ {
+ requestedCount++; // add one for "hasMoreItems"
+ }
+
+ result = new ArrayList(100);
+
+ final int maxItems = requestedCount;
+
+ UnsortedChildQueryCallback callback = new UnsortedChildQueryCallback()
+ {
+ public boolean handle(NodeRef nodeRef)
+ {
+ result.add(nodeRef);
+
+ // More results ?
+ return (result.size() < maxItems);
+ }
+ };
+
+ UnsortedResultHandler resultHandler = new UnsortedResultHandler(callback, parameters.getAuthenticationToken());
+ cannedQueryDAO.executeQuery(QUERY_NAMESPACE, QUERY_SELECT_GET_CHILDREN, params, 0, Integer.MAX_VALUE, resultHandler);
+ resultHandler.done();
+ }
+
+ if (start != null)
+ {
+ logger.debug("Base query "+(sortPairsCnt > 0 ? "(sort=y, perms=n)" : "(sort=n, perms=y)")+": "+result.size()+" in "+(System.currentTimeMillis()-start)+" msecs");
}
return result;
}
+ // Set sort props (0, 1 or 2)
+ private int setSortParams(List> sortPairs, SortableNodeEntity params)
+ {
+ int sortPairsCnt = sortPairs.size();
+
+ if (sortPairsCnt > MAX_SORT_PAIRS)
+ {
+ throw new AlfrescoRuntimeException("GetChildren: exceeded maximum number sort parameters: (max="+MAX_SORT_PAIRS+", actual="+sortPairsCnt);
+ }
+
+ int cnt = 0;
+
+ for (int i = 0; i < sortPairsCnt; i++)
+ {
+ QName sortQName = (QName)sortPairs.get(i).getFirst();
+ if (AuditablePropertiesEntity.getAuditablePropertyQNames().contains(sortQName))
+ {
+ params.setAuditableProps(true);
+ }
+ else if (sortQName.equals(SORT_QNAME_NODE_TYPE))
+ {
+ params.setNodeType(true);
+ }
+ else
+ {
+ Long sortQNameId = getQNameId(sortQName);
+ if (sortQNameId != null)
+ {
+ if (i == 0)
+ {
+ params.setProp1qnameId(sortQNameId);
+ }
+ else if (i == 1)
+ {
+ params.setProp2qnameId(sortQNameId);
+ }
+ else
+ {
+ // belts and braces
+ throw new AlfrescoRuntimeException("GetChildren: unexpected - cannot set sort parameter: "+i);
+ }
+ }
+ else
+ {
+ logger.warn("Skipping sort param - cannot find: "+sortQName);
+ break;
+ }
+ }
+
+ cnt++;
+ }
+
+ return cnt;
+ }
+
+ private Long getQNameId(QName sortPropQName)
+ {
+ if (sortPropQName.equals(SORT_QNAME_CONTENT_SIZE) || sortPropQName.equals(SORT_QNAME_CONTENT_MIMETYPE))
+ {
+ sortPropQName = ContentModel.PROP_CONTENT;
+ }
+
+ Pair qnamePair = qnameDAO.getQName(sortPropQName);
+ return (qnamePair == null ? null : qnamePair.getFirst());
+ }
+
@Override
protected boolean isApplyPostQuerySorting()
{
- return true;
- }
-
- @Override
- protected List applyPostQuerySorting(List results, CannedQuerySortDetails sortDetails)
- {
- if (sortDetails.getSortPairs().size() == 0)
- {
- // Nothing to sort on
- return results;
- }
-
- long start = System.currentTimeMillis();
-
- List ret = new ArrayList(results);
-
- List> sortPairs = sortDetails.getSortPairs();
-
- Collections.sort(ret, new PropComparatorAsc(sortPairs));
-
- if (logger.isDebugEnabled())
- {
- logger.debug("Post query sort: "+ret.size()+" in "+(System.currentTimeMillis()-start)+" msecs");
- }
-
- return ret;
+ // note: sorted as part of the query impl (using SortableNode results)
+ return false;
}
- private class PropComparatorAsc implements Comparator
+ private class PropComparatorAsc implements Comparator
{
private List> sortProps;
+ private Collator collator;
public PropComparatorAsc(List> sortProps)
{
this.sortProps = sortProps;
+ this.collator = Collator.getInstance(); // note: currently default locale
}
- public int compare(NodeRef n1, NodeRef n2)
+ public int compare(SortableNode n1, SortableNode n2)
{
return compareImpl(n1, n2, sortProps);
}
- private int compareImpl(NodeRef node1In, NodeRef node2In, List> sortProps)
+ private int compareImpl(SortableNode node1In, SortableNode node2In, List> sortProps)
{
Object pv1 = null;
Object pv2 = null;
@@ -150,8 +314,8 @@ public class GetChildrenCannedQuery extends AbstractCannedQuery
QName sortPropQName = (QName)sortProps.get(0).getFirst();
boolean sortAscending = (sortProps.get(0).getSecond() == SortOrder.ASCENDING);
- NodeRef node1 = node1In;
- NodeRef node2 = node2In;
+ SortableNode node1 = node1In;
+ SortableNode node2 = node2In;
if (sortAscending == false)
{
@@ -161,158 +325,281 @@ public class GetChildrenCannedQuery extends AbstractCannedQuery
int result = 0;
- if (sortPropQName.equals(QName.createQName(".size")) || sortPropQName.equals(QName.createQName(".mimetype")))
+ pv1 = node1.getVal(sortPropQName);
+ pv2 = node2.getVal(sortPropQName);
+
+ if (pv1 == null)
{
- // content data properties (size or mimetype)
-
- ContentData cd1 = (ContentData)nodeService.getProperty(node1, ContentModel.PROP_CONTENT);
- ContentData cd2 = (ContentData)nodeService.getProperty(node2, ContentModel.PROP_CONTENT);
-
- if (cd1 == null)
- {
- return (cd2 == null ? 0 : 1);
- }
- else if (cd2 == null)
- {
- return -1;
- }
-
- if (sortPropQName.equals(QName.createQName(".size")))
- {
- result = ((Long)cd1.getSize()).compareTo((Long)cd2.getSize());
- }
- else if (sortPropQName.equals(QName.createQName(".mimetype")))
- {
- result = (cd1.getMimetype()).compareTo(cd2.getMimetype());
- }
+ return (pv2 == null ? 0 : -1);
+ }
+ else if (pv2 == null)
+ {
+ return 1;
+ }
+
+ if (pv1 instanceof String)
+ {
+ result = collator.compare((String)pv1, (String)pv2); // TODO use collation keys (re: performance)
+ }
+ else if (pv1 instanceof Date)
+ {
+ result = (((Date)pv1).compareTo((Date)pv2));
+ }
+ else if (pv1 instanceof Long)
+ {
+ result = (((Long)pv1).compareTo((Long)pv2));
+ }
+ else if (pv1 instanceof QName)
+ {
+ result = (((QName)pv1).compareTo((QName)pv2));
}
else
{
- // property other than content size / mimetype
- pv1 = nodeService.getProperty(node1, sortPropQName);
- pv2 = nodeService.getProperty(node2, sortPropQName);
-
- if (pv1 == null)
- {
- return (pv2 == null ? 0 : 1);
- }
- else if (pv2 == null)
- {
- return -1;
- }
-
- if (pv1 instanceof String)
- {
- result = (((String)pv1).compareTo((String)pv2));
- }
- else if (pv1 instanceof MLText) // eg. title, description
- {
- String sv1 = DefaultTypeConverter.INSTANCE.convert(String.class, (MLText)pv1);
- String sv2 = DefaultTypeConverter.INSTANCE.convert(String.class, (MLText)pv2);
-
- result = (sv1.compareTo(sv2));
- }
- else if (pv1 instanceof Date)
- {
- result = (((Date)pv1).compareTo((Date)pv2));
- }
- else
- {
- // TODO other comparisons
- throw new RuntimeException("Unsupported sort type");
- }
+ // TODO other comparisons
+ throw new RuntimeException("Unsupported sort type: "+pv1.getClass().getName());
}
if ((result == 0) && (sortProps.size() > 1))
{
- return compareImpl(node1In, node2In, sortProps.subList(1, sortProps.size()-1));
+ return compareImpl(node1In, node2In, sortProps.subList(1, sortProps.size()));
}
return result;
}
}
+
@Override
protected boolean isApplyPostQueryPermissions()
{
- return true;
+ return sorted; // true if sorted (if unsorted then permissions are applied as part of the query impl)
}
- @SuppressWarnings("unchecked")
@Override
- protected List applyPostQueryPermissions(List results, String authenticationToken, int requestedCount)
+ protected PagingResults applyPostQueryPermissions(List results, String authenticationToken, int requestedCount)
{
- long start = System.currentTimeMillis();
+ Long start = (logger.isDebugEnabled() ? System.currentTimeMillis() : null);
- // TODO push down cut-off (as option within permission interceptor)
- //boolean cutoffAllowed = !getParameters().isReturnTotalResultCount();
+ int requestTotalCountMax = getParameters().requestTotalResultCountMax();
+ int maxChecks = (((requestTotalCountMax > 0) && (requestTotalCountMax > requestedCount)) ? requestTotalCountMax : requestedCount);
+ int cnt = results.size();
- List ret = new ArrayList(results.size());
+ int toIdx = (maxChecks > cnt ? cnt : maxChecks);
- Context context = ContextHolder.getContext();
- if ((context == null) || !(context instanceof SecureContext))
+ // note: assume user has read access to most/majority of the items hence pre-load up to max checks
+ preload(results.subList(0, toIdx));
+
+ PagingResults ret = super.applyPostQueryPermissions(results, authenticationToken, requestedCount);
+
+ if (start != null)
{
- return ret; // empty list
- }
-
- Authentication authentication = (((SecureContext) context).getAuthentication());
-
- Method listMethod = null;
- for (Method method : FileFolderServiceImpl.class.getMethods())
- {
- if (method.getName().equals("list"))
- {
- // found one of the list methods
- listMethod = method;
- break;
- }
- }
-
- if (listMethod == null)
- {
- return ret; // empty list
- }
-
- // TODO ALF-8419
- ConfigAttributeDefinition listCad = methodSecurityInterceptor.getObjectDefinitionSource().getAttributes(new InternalMethodInvocation(listMethod));
- ret = (List)methodSecurityInterceptor.getAfterInvocationManager().decide(authentication, null, listCad, results); // need to be able to cut-off etc ...
-
- if (logger.isDebugEnabled())
- {
- logger.debug("Post query perms: "+ret.size()+" in "+(System.currentTimeMillis()-start)+" msecs");
+ logger.debug("Post-query perms: "+ret.getPage().size()+" in "+(System.currentTimeMillis()-start)+" msecs");
}
return ret;
}
- class InternalMethodInvocation implements MethodInvocation {
- Method method;
-
- public InternalMethodInvocation(Method method) {
- this.method = method;
+ private void preload(List nodeRefs)
+ {
+ Long start = (logger.isTraceEnabled() ? System.currentTimeMillis() : null);
+
+ // note: currently pre-loads aspects AND properties
+ nodeDAO.cacheNodes(nodeRefs);
+
+ if (start != null)
+ {
+ logger.trace("Pre-load: "+nodeRefs.size()+" in "+(System.currentTimeMillis()-start)+" msecs");
}
-
- protected InternalMethodInvocation() {
- throw new UnsupportedOperationException();
+ }
+
+ private interface SortedChildQueryCallback
+ {
+ boolean handle(NodeRef nodeRef, Map sortPropVals);
+ }
+
+ private interface UnsortedChildQueryCallback
+ {
+ boolean handle(NodeRef nodeRef);
+ }
+
+ private class SortedResultHandler implements CannedQueryDAO.ResultHandler
+ {
+ private final SortedChildQueryCallback resultsCallback;
+ private boolean more = true;
+
+ private SortedResultHandler(SortedChildQueryCallback resultsCallback)
+ {
+ this.resultsCallback = resultsCallback;
}
-
- public Object[] getArguments() {
- throw new UnsupportedOperationException();
+
+ public boolean handleResult(SortableNodeEntity result)
+ {
+ // Do nothing if no further results are required
+ if (!more)
+ {
+ return false;
+ }
+
+ NodeRef nodeRef = result.getNode().getNodeRef();
+
+ Map propertyValues = new HashMap(3);
+
+ NodePropertyEntity prop1 = result.getProp1();
+ if (prop1 != null)
+ {
+ propertyValues.put(prop1.getKey(), prop1.getValue());
+ }
+
+ NodePropertyEntity prop2 = result.getProp2();
+ if (prop2 != null)
+ {
+ propertyValues.put(prop2.getKey(), prop2.getValue());
+ }
+
+ Map sortPropVals = nodePropertyHelper.convertToPublicProperties(propertyValues);
+
+ // special cases
+
+ // MLText (eg. cm:title, cm:description, ...)
+ for (Map.Entry entry : sortPropVals.entrySet())
+ {
+ if (entry.getValue() instanceof MLText)
+ {
+ sortPropVals.put(entry.getKey(), DefaultTypeConverter.INSTANCE.convert(String.class, (MLText)entry.getValue()));
+ }
+ }
+
+ // ContentData (eg. cm:content.size, cm:content.mimetype)
+ ContentData contentData = (ContentData)sortPropVals.get(ContentModel.PROP_CONTENT);
+ if (contentData != null)
+ {
+ sortPropVals.put(SORT_QNAME_CONTENT_SIZE, contentData.getSize());
+ sortPropVals.put(SORT_QNAME_CONTENT_MIMETYPE, contentData.getMimetype());
+ }
+
+ // Auditable props (eg. cm:creator, cm:created, cm:modifier, cm:modified, ...)
+ AuditablePropertiesEntity auditableProps = result.getNode().getAuditableProperties();
+ if (auditableProps != null)
+ {
+ for (Map.Entry entry : auditableProps.getAuditableProperties().entrySet())
+ {
+ sortPropVals.put(entry.getKey(), entry.getValue());
+ }
+ }
+
+ // Node type
+ Long nodeTypeQNameId = result.getNode().getTypeQNameId();
+ if (nodeTypeQNameId != null)
+ {
+ Pair pair = qnameDAO.getQName(nodeTypeQNameId);
+ if (pair != null)
+ {
+ sortPropVals.put(SORT_QNAME_NODE_TYPE, pair.getSecond());
+ }
+ }
+
+ // Call back
+ boolean more = resultsCallback.handle(nodeRef, sortPropVals);
+ if (!more)
+ {
+ this.more = false;
+ }
+
+ return more;
}
-
- public Method getMethod() {
- return this.method;
+
+ public void done()
+ {
}
-
- public AccessibleObject getStaticPart() {
- throw new UnsupportedOperationException();
+ }
+
+ private class SortableNode
+ {
+ private NodeRef nodeRef;
+ private Map sortPropVals;
+
+ public SortableNode(NodeRef nodeRef, Map sortPropVals)
+ {
+ this.nodeRef = nodeRef;
+ this.sortPropVals = sortPropVals;
}
-
- public Object getThis() {
- throw new UnsupportedOperationException();
+
+ public NodeRef getNodeRef()
+ {
+ return nodeRef;
}
-
- public Object proceed() throws Throwable {
- throw new UnsupportedOperationException();
+
+ public Serializable getVal(QName sortProp)
+ {
+ return sortPropVals.get(sortProp);
+ }
+ }
+
+ private class UnsortedResultHandler implements CannedQueryDAO.ResultHandler
+ {
+ private final UnsortedChildQueryCallback resultsCallback;
+ private final String authenticationToken;
+
+ private boolean more = true;
+
+ private static final int BATCH_SIZE = 256 * 4;
+ private final List nodeRefs;
+
+ private UnsortedResultHandler(UnsortedChildQueryCallback resultsCallback, String authenticationToken)
+ {
+ this.resultsCallback = resultsCallback;
+ this.authenticationToken = authenticationToken;
+
+ nodeRefs = new LinkedList();
+ }
+
+ public boolean handleResult(NodeEntity result)
+ {
+ // Do nothing if no further results are required
+ if (!more)
+ {
+ return false;
+ }
+
+ NodeRef nodeRef = result.getNodeRef();
+
+ if (nodeRefs.size() >= BATCH_SIZE)
+ {
+ // batch
+ preloadAndApplyPermissions();
+ }
+
+ nodeRefs.add(nodeRef);
+
+ return more;
+ }
+
+ private void preloadAndApplyPermissions()
+ {
+ preload(nodeRefs);
+
+ PagingResults results = applyPermissions(nodeRefs, authenticationToken, nodeRefs.size());
+
+ for (NodeRef nodeRef : results.getPage())
+ {
+ // Call back
+ boolean more = resultsCallback.handle(nodeRef);
+ if (!more)
+ {
+ this.more = false;
+ break;
+ }
+ }
+
+ nodeRefs.clear();
+ }
+
+ public void done()
+ {
+ if (nodeRefs.size() >= 0)
+ {
+ // finish batch
+ preloadAndApplyPermissions();
+ }
}
}
}
diff --git a/source/java/org/alfresco/repo/model/filefolder/GetChildrenCannedQueryFactory.java b/source/java/org/alfresco/repo/model/filefolder/GetChildrenCannedQueryFactory.java
index df5b6fb3e0..f57312c20c 100644
--- a/source/java/org/alfresco/repo/model/filefolder/GetChildrenCannedQueryFactory.java
+++ b/source/java/org/alfresco/repo/model/filefolder/GetChildrenCannedQueryFactory.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2005-2010 Alfresco Software Limited.
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
*
* This file is part of Alfresco
*
@@ -18,38 +18,210 @@
*/
package org.alfresco.repo.model.filefolder;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.query.AbstractCannedQueryFactory;
import org.alfresco.query.CannedQuery;
+import org.alfresco.query.CannedQueryPageDetails;
import org.alfresco.query.CannedQueryParameters;
+import org.alfresco.query.CannedQuerySortDetails;
+import org.alfresco.query.PagingRequest;
+import org.alfresco.query.CannedQuerySortDetails.SortOrder;
+import org.alfresco.repo.domain.contentdata.ContentDataDAO;
+import org.alfresco.repo.domain.locale.LocaleDAO;
+import org.alfresco.repo.domain.node.NodeDAO;
+import org.alfresco.repo.domain.node.NodePropertyHelper;
+import org.alfresco.repo.domain.qname.QNameDAO;
+import org.alfresco.repo.domain.query.CannedQueryDAO;
+import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.permissions.impl.acegi.MethodSecurityInterceptor;
-import org.alfresco.service.cmr.repository.NodeService;
+import org.alfresco.service.cmr.dictionary.DictionaryService;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.namespace.QName;
+import org.alfresco.util.Pair;
+import org.alfresco.util.ParameterCheck;
+import org.alfresco.util.PropertyCheck;
/**
- * GetChildren (paged list of FileInfo)
+ * GetChildren canned query factory - to get paged list of children of a parent node
*
* @author janv
* @since 4.0
*/
-public class GetChildrenCannedQueryFactory extends AbstractCannedQueryFactory