diff --git a/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/core/v0/BaseAPI.java b/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/core/v0/BaseAPI.java
index e3a37ff998..935dcc151c 100644
--- a/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/core/v0/BaseAPI.java
+++ b/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/core/v0/BaseAPI.java
@@ -662,7 +662,7 @@ public abstract class BaseAPI
RETENTION_GHOST,
RETENTION_ELIGIBLE_FIRST_EVENT,
RETENTION_EVENTS,
-
+ COMBINE_DISPOSITION_STEP_CONDITIONS
}
/**
@@ -674,6 +674,8 @@ public abstract class BaseAPI
CUT_OFF("cutoff"),
UNDO_CUT_OFF("undoCutoff"),
TRANSFER("transfer"),
+ COMPLETE_EVENT("completeEvent"),
+ UNDO_EVENT("undoEvent"),
DESTROY("destroy");
String action;
diff --git a/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/core/v0/RMEvents.java b/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/core/v0/RMEvents.java
new file mode 100644
index 0000000000..a63fa5b903
--- /dev/null
+++ b/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/core/v0/RMEvents.java
@@ -0,0 +1,49 @@
+/*
+ * #%L
+ * Alfresco Records Management Module
+ * %%
+ * Copyright (C) 2005 - 2018 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.rest.core.v0;
+
+public enum RMEvents
+{
+ ABOLISHED("abolished"),
+ ALL_ALLOWANCES_GRANTED_ARE_TERMINATED("all_allowances_granted_are_terminated"),
+ CASE_CLOSED("case_closed"),
+ DECLASSIFICATION_REVIEW("declassification_review"),
+ OBSOLETE("obsolete"),
+ NO_LONGER_NEEDED("no_longer_needed"),
+ STUDY_COMPLETE("study_complete");
+ private String eventName;
+
+ RMEvents(String eventName)
+ {
+ this.eventName = eventName;
+ }
+
+ public String getEventName()
+ {
+ return eventName;
+ }
+}
diff --git a/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/fileplancomponents/FilePlanComponentFields.java b/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/fileplancomponents/FilePlanComponentFields.java
index 2fbbf3cc15..4732c28a17 100644
--- a/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/fileplancomponents/FilePlanComponentFields.java
+++ b/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/fileplancomponents/FilePlanComponentFields.java
@@ -58,6 +58,8 @@ public class FilePlanComponentFields
public static final String PROPERTIES_RECORD_SEARCH_DISPOSITION_ACTION_NAME = "rma:recordSearchDispositionActionName";
public static final String PROPERTIES_RECORD_SEARCH_DISPOSITION_EVENTS_ELIGIBLE = "rma:recordSearchDispositionEventsEligible";
public static final String PROPERTIES_RECORD_SEARCH_DISPOSITION_INSTRUCTIONS = "rma:recordSearchDispositionInstructions";
+ public static final String PROPERTIES_DECLASSIFICATION_REVIEW_COMPLETED_BY = "rma:declassificationReviewCompletedBy";
+ public static final String PROPERTIES_DECLASSIFICATION_REVIEW_COMPLETED_AT = "rma:declassificationReviewCompletedAt";
/** File plan properties */
diff --git a/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/RMRolesAndActionsAPI.java b/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/RMRolesAndActionsAPI.java
index 61de24b76c..122ab88bef 100644
--- a/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/RMRolesAndActionsAPI.java
+++ b/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/RMRolesAndActionsAPI.java
@@ -35,6 +35,7 @@ import static org.testng.AssertJUnit.fail;
import java.io.IOException;
import java.text.MessageFormat;
+import java.time.Instant;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
@@ -46,6 +47,7 @@ import org.alfresco.dataprep.AlfrescoHttpClientFactory;
import org.alfresco.dataprep.ContentService;
import org.alfresco.dataprep.UserService;
import org.alfresco.rest.core.v0.BaseAPI;
+import org.alfresco.rest.core.v0.RMEvents;
import org.apache.chemistry.opencmis.client.api.CmisObject;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.http.HttpResponse;
@@ -325,9 +327,9 @@ public class RMRolesAndActionsAPI extends BaseAPI
/**
* Perform an action on the record folder
*
- * @param user the user closing the folder
+ * @param user the user executing the action
* @param password the user's password
- * @param contentName the record folder name
+ * @param contentName the content name
* @param date the date to be updated
* @return The HTTP response.
*/
@@ -349,6 +351,56 @@ public class RMRolesAndActionsAPI extends BaseAPI
return doPostJsonRequest(user, password, SC_OK, requestParams, RM_ACTIONS_API);
}
+ /**
+ * Complete an event on the record/record folder
+ *
+ * @param user the user executing the action
+ * @param password the user's password
+ * @param nodeName the node name
+ * @param event the event to be completed
+ * @param date the date to be updated
+ * @return The HTTP response.
+ */
+ public HttpResponse completeEvent(String user, String password, String nodeName, RMEvents event, Instant date)
+ {
+ String recNodeRef = getNodeRefSpacesStore() + contentService.getNodeRef(user, password, RM_SITE_ID, nodeName);
+ JSONObject requestParams = new JSONObject();
+ requestParams.put("name", RM_ACTIONS.COMPLETE_EVENT.getAction());
+ requestParams.put("nodeRef", recNodeRef);
+ date = (date != null) ? date : Instant.now();
+ String formattedDate = DateTimeFormatter.ISO_INSTANT.format(date);
+ requestParams.put("params", new JSONObject()
+ .put("eventName", event.getEventName())
+ .put("eventCompletedBy", user)
+ .put("eventCompletedAt", new JSONObject()
+ .put("iso8601", formattedDate)
+ )
+ );
+
+ return doPostJsonRequest(user, password, SC_OK, requestParams, RM_ACTIONS_API);
+ }
+
+ /**
+ * Undo an event on the record/record folder
+ *
+ * @param user the user executing the action
+ * @param password the user's password
+ * @param contentName the content name
+ * @param event the event to be undone
+ * @return The HTTP response.
+ */
+ public HttpResponse undoEvent(String user, String password, String contentName, RMEvents event)
+ {
+ String recNodeRef = getNodeRefSpacesStore() + contentService.getNodeRef(user, password, RM_SITE_ID, contentName);
+ JSONObject requestParams = new JSONObject();
+ requestParams.put("name", RM_ACTIONS.UNDO_EVENT.getAction());
+ requestParams.put("nodeRef", recNodeRef);
+ requestParams.put("params", new JSONObject()
+ .put("eventName", event.getEventName()));
+
+ return doPostJsonRequest(user, password, SC_OK, requestParams, RM_ACTIONS_API);
+ }
+
/**
* Deletes every item in the given container
*
diff --git a/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/RecordCategoriesAPI.java b/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/RecordCategoriesAPI.java
index 34ff344f92..6a3caed2cc 100644
--- a/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/RecordCategoriesAPI.java
+++ b/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/RecordCategoriesAPI.java
@@ -51,6 +51,8 @@ public class RecordCategoriesAPI extends BaseAPI
private static final Logger LOGGER = LoggerFactory.getLogger(RecordCategoriesAPI.class);
private static final String RM_ACTIONS_API = "{0}rma/actions/ExecutionQueue";
private static final String DISPOSITION_ACTIONS_API = "{0}node/{1}/dispositionschedule/dispositionactiondefinitions";
+ private static final String DISPOSITION_SCHEDULE_API = "{0}node/{1}/dispositionschedule";
+
/**
* Creates a retention schedule for the category given as parameter
@@ -71,6 +73,21 @@ public class RecordCategoriesAPI extends BaseAPI
return doPostJsonRequest(user, password, SC_OK, requestParams, RM_ACTIONS_API);
}
+ /**
+ * Get the disposition schedule nodeRef
+ *
+ * @param user
+ * @param password
+ * @param categoryName
+ * @return the disposition schedule nodeRef
+ */
+ public String getDispositionScheduleNodeRef(String user, String password, String categoryName)
+ {
+ String catNodeRef = NODE_PREFIX + getItemNodeRef(user, password, "/" + categoryName);
+ JSONObject dispositionSchedule = doGetRequest(user, password, MessageFormat.format(DISPOSITION_SCHEDULE_API, "{0}", catNodeRef));
+ return dispositionSchedule.getJSONObject("data").getString("nodeRef").replace(getNodeRefSpacesStore(), "");
+ }
+
/**
* Sets retention schedule authority and instructions, also if it is applied to records or folders
*
@@ -108,7 +125,12 @@ public class RecordCategoriesAPI extends BaseAPI
addPropertyToRequest(requestParams, "period", properties, RETENTION_SCHEDULE.RETENTION_PERIOD);
addPropertyToRequest(requestParams, "ghostOnDestroy", properties, RETENTION_SCHEDULE.RETENTION_GHOST);
addPropertyToRequest(requestParams, "periodProperty", properties, RETENTION_SCHEDULE.RETENTION_PERIOD_PROPERTY);
- addPropertyToRequest(requestParams, "events", properties, RETENTION_SCHEDULE.RETENTION_EVENTS);
+ String events = getPropertyValue(properties, RETENTION_SCHEDULE.RETENTION_EVENTS);
+ if(!events.equals(""))
+ {
+ requestParams.append("events", events);
+ }
+ addPropertyToRequest(requestParams, "combineDispositionStepConditions", properties, RETENTION_SCHEDULE.COMBINE_DISPOSITION_STEP_CONDITIONS);
addPropertyToRequest(requestParams, "eligibleOnFirstCompleteEvent", properties, RETENTION_SCHEDULE.RETENTION_ELIGIBLE_FIRST_EVENT);
return doPostJsonRequest(user, password, SC_OK, requestParams, MessageFormat.format(DISPOSITION_ACTIONS_API, "{0}", catNodeRef));
diff --git a/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/SearchAPI.java b/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/SearchAPI.java
index 2bad85cbd5..3dc1f1edfd 100644
--- a/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/SearchAPI.java
+++ b/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/SearchAPI.java
@@ -64,8 +64,8 @@ public class SearchAPI extends BaseAPI
private static final String RM_SEARCH_ENDPOINT = "{0}alfresco/s/slingshot/rmsearch/{1}?{2}";
/** RM document search filters */
- private static final String RM_DEFAULT_RECORD_FILTERS =
- "records/true,undeclared/true,vital/false,folders/false,categories/false,frozen/false,cutoff/false";
+ private static final String RM_DEFAULT_NODES_FILTERS =
+ "records/true,undeclared/true,vital/false,folders/{0},categories/{1},frozen/false,cutoff/false";
/**
* Perform search request on search endpoint as a user.
@@ -91,6 +91,7 @@ public class SearchAPI extends BaseAPI
* @param site
* @param query
* @param filters
+ * @param sortby
* @return search results (see API reference for more details), null for any errors
*/
public JSONObject rmSearch(
@@ -98,11 +99,16 @@ public class SearchAPI extends BaseAPI
String password,
String site,
String query,
- String filters)
+ String filters,
+ String sortby)
{
List searchParameters = new ArrayList();
searchParameters.add(new BasicNameValuePair("query", query));
searchParameters.add(new BasicNameValuePair("filters", filters));
+ if (sortby != null)
+ {
+ searchParameters.add(new BasicNameValuePair("sortby", sortby));
+ }
String requestURL = MessageFormat.format(
RM_SEARCH_ENDPOINT,
@@ -114,20 +120,23 @@ public class SearchAPI extends BaseAPI
}
/**
- * Search as a user for records on site "rm" matching query, using SearchAPI.RM_DEFAULT_RECORD_FILTERS
+ * Search as a user for nodes on site "rm" matching query, using SearchAPI.RM_DEFAULT_RECORD_FILTERS and sorted
+ * by sortby
*
* If more fine-grained control of search parameters is required, use rmSearch() directly.
* @param username
* @param password
* @param query
+ * @param sortby
* @return list of record names
*/
public List searchForRecordsAsUser(
- String username,
- String password,
- String query)
+ String username, String password,
+ String query, String sortby,
+ boolean includeCategories, boolean includeFolders)
{
- return getItemNames(rmSearch(username, password, "rm", query, RM_DEFAULT_RECORD_FILTERS));
+ String searchFilterParamaters = MessageFormat.format(RM_DEFAULT_NODES_FILTERS, Boolean.toString(includeFolders), Boolean.toString(includeCategories));
+ return getItemNames(rmSearch(username, password, "rm", query, searchFilterParamaters, sortby));
}
/**
diff --git a/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/service/DispositionScheduleService.java b/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/service/DispositionScheduleService.java
new file mode 100644
index 0000000000..e5265f69ac
--- /dev/null
+++ b/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/service/DispositionScheduleService.java
@@ -0,0 +1,139 @@
+/*
+ * #%L
+ * Alfresco Records Management Module
+ * %%
+ * Copyright (C) 2005 - 2018 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.rest.v0.service;
+
+
+import java.util.HashMap;
+
+import org.alfresco.rest.core.v0.BaseAPI;
+import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory;
+import org.alfresco.rest.v0.RecordCategoriesAPI;
+import org.alfresco.utility.data.DataUser;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * Service for different disposition schedule actions
+ *
+ * @author jcule
+ * @since 2.7.0.1
+ */
+@Service
+public class DispositionScheduleService extends BaseAPI
+{
+ @Autowired
+ private RecordCategoriesAPI recordCategoriesAPI;
+
+ @Autowired
+ private DataUser dataUser;
+
+ /**
+ * Helper method for adding a cut off after period step
+ *
+ * @param categoryName the category in whose schedule the step will be added
+ * @param period
+ * @return
+ */
+ public void addCutOffAfterPeriodStep(String categoryName, String period)
+ {
+ HashMap cutOffStep = new HashMap<>();
+ cutOffStep.put(RETENTION_SCHEDULE.NAME, "cutoff");
+ cutOffStep.put(RETENTION_SCHEDULE.RETENTION_PERIOD, period);
+ cutOffStep.put(RETENTION_SCHEDULE.DESCRIPTION, "Cut off after a period step");
+ recordCategoriesAPI.addDispositionScheduleSteps(dataUser.getAdminUser().getUsername(),
+ dataUser.getAdminUser().getPassword(), categoryName, cutOffStep);
+ }
+
+ /**
+ * Helper method for adding a cut off after an event occurs step
+ *
+ * @param categoryName the category in whose schedule the step will be added
+ * @param events
+ */
+ public void addCutOffAfterEventStep(String categoryName, String events)
+ {
+ HashMap cutOffStep = new HashMap<>();
+ cutOffStep.put(RETENTION_SCHEDULE.NAME, "cutoff");
+ cutOffStep.put(RETENTION_SCHEDULE.RETENTION_EVENTS, events);
+ cutOffStep.put(RETENTION_SCHEDULE.DESCRIPTION, "Cut off after event step");
+
+ recordCategoriesAPI.addDispositionScheduleSteps(dataUser.getAdminUser().getUsername(),
+ dataUser.getAdminUser().getPassword(), categoryName, cutOffStep);
+ }
+
+ /**
+ * Helper method for adding an accession step
+ *
+ * @param timeOrEvent
+ * @param events
+ * @param period
+ * @param periodProperty
+ * @param combineConditions
+ * @return
+ */
+ public void addAccessionStep(String categoryName, Boolean timeOrEvent, String events, String period, String
+ periodProperty, Boolean combineConditions)
+ {
+ HashMap accessionStep = new HashMap<>();
+ accessionStep.put(RETENTION_SCHEDULE.NAME, "accession");
+ accessionStep.put(RETENTION_SCHEDULE.COMBINE_DISPOSITION_STEP_CONDITIONS, Boolean.toString(combineConditions));
+ accessionStep.put(RETENTION_SCHEDULE.RETENTION_PERIOD, period);
+ accessionStep.put(RETENTION_SCHEDULE.RETENTION_PERIOD_PROPERTY, periodProperty);
+ if (!timeOrEvent)
+ {
+ accessionStep.put(RETENTION_SCHEDULE.RETENTION_ELIGIBLE_FIRST_EVENT, Boolean.toString(timeOrEvent));
+ }
+ accessionStep.put(RETENTION_SCHEDULE.RETENTION_EVENTS, events);
+ accessionStep.put(RETENTION_SCHEDULE.DESCRIPTION,
+ "Accession step with time and event conditions.");
+ recordCategoriesAPI.addDispositionScheduleSteps(dataUser.getAdminUser().getUsername(),
+ dataUser.getAdminUser().getPassword(), categoryName, accessionStep);
+ }
+
+ /**
+ * Helper method to create retention schedule with general fields for the given category as admin
+ * and apply it to the records
+ *
+ * @param categoryName
+ * @param appliedToRecords
+ */
+ public void createCategoryRetentionSchedule(String categoryName, Boolean appliedToRecords)
+ {
+ recordCategoriesAPI.createRetentionSchedule(dataUser.getAdminUser().getUsername(),
+ dataUser.getAdminUser().getPassword(), categoryName);
+ String retentionScheduleNodeRef = recordCategoriesAPI.getDispositionScheduleNodeRef(
+ dataUser.getAdminUser().getUsername(), dataUser.getAdminUser().getPassword(), categoryName);
+
+ HashMap retentionScheduleGeneralFields = new HashMap<>();
+ retentionScheduleGeneralFields.put(RETENTION_SCHEDULE.RETENTION_AUTHORITY, "Authority");
+ retentionScheduleGeneralFields.put(RETENTION_SCHEDULE.RETENTION_INSTRUCTIONS, "Instructions");
+ recordCategoriesAPI.setRetentionScheduleGeneralFields(dataUser.getAdminUser().getUsername(),
+ dataUser.getAdminUser().getPassword(), retentionScheduleNodeRef, retentionScheduleGeneralFields,
+ appliedToRecords);
+
+ }
+}
diff --git a/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/base/BaseRMRestTest.java b/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/base/BaseRMRestTest.java
index 25fe4d9954..3523521852 100644
--- a/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/base/BaseRMRestTest.java
+++ b/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/base/BaseRMRestTest.java
@@ -658,7 +658,8 @@ public class BaseRMRestTest extends RestTest
names.add(childNode.onModel().getName());
});
break;
- } else
+ }
+ else
{
counter++;
}
@@ -673,10 +674,12 @@ public class BaseRMRestTest extends RestTest
*
* @param user
* @param term
+ * @param sortby
+ * @param expectedResults
* @return
- * @throws Exception
*/
- public List searchForRMContentAsUser(UserModel user, String term, String expectedResult) throws Exception
+ public List searchForRMContentAsUser(UserModel user, String term, String sortby, boolean includeFolders,
+ boolean includeCategories, List expectedResults)
{
List results = new ArrayList<>();
// wait for solr indexing
@@ -684,26 +687,28 @@ public class BaseRMRestTest extends RestTest
int waitInMilliSeconds = 6000;
while (counter < 3)
{
- results = searchApi.searchForRecordsAsUser(user.getUsername(), user.getPassword(), term);
- if ((results != null && !results.isEmpty() && results.contains(expectedResult)))
- {
- break;
- } else
- {
- counter++;
- }
- // double wait time to not overdo solr search
- waitInMilliSeconds = (waitInMilliSeconds * 2);
synchronized (this)
{
try
{
this.wait(waitInMilliSeconds);
- } catch (InterruptedException e)
+ }
+ catch (InterruptedException e)
{
}
}
-
+ results = searchApi.searchForRecordsAsUser(user.getUsername(), user.getPassword(), term, sortby,
+ includeFolders, includeCategories);
+ if (!results.isEmpty() && results.containsAll(expectedResults))
+ {
+ break;
+ }
+ else
+ {
+ counter++;
+ }
+ // double wait time to not overdo solr search
+ waitInMilliSeconds = (waitInMilliSeconds * 2);
}
return results;
}
diff --git a/rm-community/documentation/README.md b/rm-community/documentation/README.md
index b4b7911330..8e5cf85337 100644
--- a/rm-community/documentation/README.md
+++ b/rm-community/documentation/README.md
@@ -11,14 +11,14 @@
* Easy Access Records
* Physical Records
* Record Import and Export
- * Version Records
+ * [Version Records](./versionRecords)
* Retention
* [Destruction](./destruction)
* Retention Schedules and Events
* Transfer and Accession
* Security
- * [Extended permission service](extendedPermissionService.md)
- * Roles, Capabilities and Permissions
+ * [Extended permission service](security/extendedPermissionService.md)
+ * [Roles and Capabilities](security/rolesAndCapabilities.md)
* Discovery
* Governance Search
* Legal Holds
@@ -27,3 +27,5 @@
* Governance Rules
* Core Module Services
* [RM Patch Service](./PatchService.md)
+* Build and Release
+ * [Build](./build)
diff --git a/rm-community/documentation/build/README.md b/rm-community/documentation/build/README.md
new file mode 100644
index 0000000000..8a50c3219a
--- /dev/null
+++ b/rm-community/documentation/build/README.md
@@ -0,0 +1,6 @@
+## GS Build 
+
+Build location: https://bamboo.alfresco.com/bamboo/browse/RM (not externally accessible.)
+
+Build Flow:
+
\ No newline at end of file
diff --git a/rm-community/documentation/build/resource/build.png b/rm-community/documentation/build/resource/build.png
new file mode 100644
index 0000000000..902e37424b
Binary files /dev/null and b/rm-community/documentation/build/resource/build.png differ
diff --git a/rm-community/documentation/build/resource/build.puml b/rm-community/documentation/build/resource/build.puml
new file mode 100644
index 0000000000..7c56bef154
--- /dev/null
+++ b/rm-community/documentation/build/resource/build.puml
@@ -0,0 +1,66 @@
+@startuml
+
+Title: Governance Services Build Pipeline (RM HEAD)
+
+'build plans:
+'Ent UI: Automated UI Tests Enterprise
+'Com API: Automation Community REST API
+'Ent API: Automation Enterprise REST API
+'Community
+'Com UI: Community Automated UI Tests
+'Enterprise
+'Ent L1: Enterprise Level 1 Automated UI Tests
+'Ent L2: Level 2 Automated UI Tests Enterprise
+'RM Benchmark Driver
+
+
+start
+
+if(Trigger) then (commit to path)
+ if (rm-community/*)
+ :Community;
+ fork
+ :Ent L1;
+ fork again
+ :Enterprise;
+ fork
+ :Ent L2;
+ fork again
+ :Ent UI;
+ end fork
+ end fork
+ elseif (rm-enterprise/*)
+ :Enterprise;
+ fork
+ :Ent L2;
+ fork again
+ :Ent UI;
+ end fork
+ elseif (rm-automation/*)
+ fork
+ :Ent L1;
+ fork again
+ :Ent L2;
+ fork again
+ :Ent UI;
+ end fork
+ stop
+ elseif (rm-community-rest-api/*)
+ :Com API;
+ stop
+ elseif (rm-enterprise-rest-api/*)
+ :Ent API;
+ stop
+ elseif (rm-benchmark-driver/*)
+ :Benchmark;
+ stop
+ else
+ end
+ endif
+else (Time: 1am)
+ :Community UI;
+ stop
+endif
+:Release Step;
+end
+@enduml
\ No newline at end of file
diff --git a/rm-community/documentation/resource/image/CapabilitiesAndRoles.png b/rm-community/documentation/resource/image/CapabilitiesAndRoles.png
new file mode 100644
index 0000000000..606a7d3a73
Binary files /dev/null and b/rm-community/documentation/resource/image/CapabilitiesAndRoles.png differ
diff --git a/rm-community/documentation/extendedPermissionService.md b/rm-community/documentation/security/extendedPermissionService.md
similarity index 100%
rename from rm-community/documentation/extendedPermissionService.md
rename to rm-community/documentation/security/extendedPermissionService.md
diff --git a/rm-community/documentation/security/rolesAndCapabilities.md b/rm-community/documentation/security/rolesAndCapabilities.md
new file mode 100644
index 0000000000..c3372d5b1c
--- /dev/null
+++ b/rm-community/documentation/security/rolesAndCapabilities.md
@@ -0,0 +1,41 @@
+## Alfresco Governance Services' Roles and Capabilities
+
+
+
+
+
+### Purpose
+
+Roles and capabilities allow the GS system to provide a finer grain security evaluation, determining whether an authority has the capability to perform a perticular action on a node.
+
+### Overview
+
+Roles are defined as a collection of capabilities. A capability, generally, has a one to one relationship with an action within the system.
+
+Authorities are assigned roles. If an authority is assigned to a role then it that authority has the capabilities contained within that role, allowing them to perform the related actions.
+
+An authority can be assigned many roles, with the associated capabilities being additive.
+
+Capabilties are evaluated in addition to any ACLs attached to a node, but they are mutally exclusive. A authority may have the capability, but not the permissions on a node and vice versa.
+
+### Design
+
+Roles are implementented as groups. So for every role that is created, there is a corresponding group within the system.
+
+Capabilities are implemented as permissions. In order add a new capability to the system, the extended RM permission model needs to be extended.
+
+When a capability is added to a role, then the capability group is assigned the capability role on the root file plan node.
+
+In this way the permissions of the systems roles reflect their capabilities on the file plan via the capability permissions assigned.
+
+When an authority is assigned to a role, that authority is added as a member of the corresponding role group. In this way they inherit the capability permissions on the file plan that relate to that role group.
+
+If a user attempts to perform an action on a records management artifact which has a related capability. Assuming the user has permission to see the artifact in the first place, then the users capability to perform the action is evaluated.
+
+This is done by firstly determining whether the capability is relevant for this 'kind' of records management artifact. For example the addHold capability is not relevant for a record category.
+
+Then the capability permission is evaluated by traversing to the file plan node and checking whether the current user has the capabilty permission byt virtue of it's membership of the right role group.
+
+Finally any further conditions attached to the capability are evaluated.
+
+
\ No newline at end of file
diff --git a/rm-community/documentation/versionRecords/README.md b/rm-community/documentation/versionRecords/README.md
new file mode 100644
index 0000000000..405827ed13
--- /dev/null
+++ b/rm-community/documentation/versionRecords/README.md
@@ -0,0 +1,20 @@
+## Version Records 
+
+### Notes:
+
+NodesService varies depending on store. Version Service has a different service that hydrates effectively fake nodes (which contain url, version details, associations, aspects, as denormalised meta data) back into a full node
+
+Recorded Versions take content out of version store and create a record by version store implementation extension.
+
+Declaring record as version - standard use case is auto declaring or via records. Head version is extracted to a record, rather than a new version being created
+
+Records are linked by association
+
+Disposition events can be triggered automatically from versioning events.
+
+### Diagram:
+
+
+
+
+
diff --git a/rm-community/documentation/versionRecords/RecordedVersions.png b/rm-community/documentation/versionRecords/RecordedVersions.png
new file mode 100644
index 0000000000..7ac208b9e2
Binary files /dev/null and b/rm-community/documentation/versionRecords/RecordedVersions.png differ
diff --git a/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/model/recordsModel.xml b/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/model/recordsModel.xml
index dc0e5277b3..d23a9f7b37 100644
--- a/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/model/recordsModel.xml
+++ b/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/model/recordsModel.xml
@@ -1132,6 +1132,19 @@
false
+
+ d:date
+ true
+
+
+ d:text
+ true
+
+ true
+ false
+ false
+
+
diff --git a/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-webscript-context.xml b/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-webscript-context.xml
index 846f6518a7..30cddbbeee 100644
--- a/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-webscript-context.xml
+++ b/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-webscript-context.xml
@@ -468,12 +468,18 @@
+
+
+
+
+
+
records = services.getNodeService().getChildAssocs(nodeRef, ContentModel.ASSOC_CONTAINS, RegexQNamePattern.MATCH_ALL);
+ for (ChildAssociationRef child : records)
+ {
+ NodeRef recordNodeRef = child.getChildRef();
+ setPropsOnContent(recordNodeRef, completedAtValue, completedByValue);
+ }
+ }
+ }
+
+ private void setPropsOnContent(NodeRef nodeRef, Date completedAtValue, String completedByValue)
+ {
+ Map nodeProps = services.getNodeService().getProperties(nodeRef);
+ nodeProps.put(PROP_RS_DECLASSIFICATION_REVIEW_COMPLETED_AT, completedAtValue);
+ nodeProps.put(PROP_RS_DECLASSIFICATION_REVIEW_COMPLETED_BY, completedByValue);
+ services.getNodeService().setProperties(nodeRef, nodeProps);
+ }
}
diff --git a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/JSONConversionComponent.java b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/JSONConversionComponent.java
index bae437569b..da2a7234a3 100644
--- a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/JSONConversionComponent.java
+++ b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/JSONConversionComponent.java
@@ -482,8 +482,9 @@ public class JSONConversionComponent extends org.alfresco.repo.jscript.app.JS
{
if(!details.isEventComplete())
{
- ((HashMap) rmNodeValues.get("properties")).put("combineDispositionStepConditions", nodeService.getProperty(dispositionService.getNextDispositionAction(nodeRef).getDispositionActionDefinition().getNodeRef(), PROP_COMBINE_DISPOSITION_STEP_CONDITIONS));
- ((HashMap) rmNodeValues.get("properties")).put("incompleteDispositionEvent", details.getEventName());
+ HashMap properties = ((HashMap) rmNodeValues.get("properties"));
+ properties.put("combineDispositionStepConditions", nodeService.getProperty(dispositionService.getNextDispositionAction(nodeRef).getDispositionActionDefinition().getNodeRef(), PROP_COMBINE_DISPOSITION_STEP_CONDITIONS));
+ properties.put("incompleteDispositionEvent", details.getEventName());
break;
}
}
diff --git a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordsManagementModel.java b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordsManagementModel.java
index 3c02c90627..0b77311cf4 100644
--- a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordsManagementModel.java
+++ b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordsManagementModel.java
@@ -240,7 +240,6 @@ public interface RecordsManagementModel extends RecordsManagementCustomModel
QName PROP_RS_DISPOSITION_ACTION_AS_OF = QName.createQName(RM_URI, "recordSearchDispositionActionAsOf");
QName PROP_RS_DISPOSITION_EVENTS_ELIGIBLE = QName.createQName(RM_URI, "recordSearchDispositionEventsEligible");
QName PROP_RS_DISPOSITION_EVENTS = QName.createQName(RM_URI, "recordSearchDispositionEvents");
- QName PROP_DISPOSITION_EVENTS = QName.createQName(RM_URI, "dispositionEvents");
QName PROP_RS_VITAL_RECORD_REVIEW_PERIOD = QName.createQName(RM_URI, "recordSearchVitalRecordReviewPeriod");
QName PROP_RS_VITAL_RECORD_REVIEW_PERIOD_EXPRESSION = QName.createQName(RM_URI, "recordSearchVitalRecordReviewPeriodExpression");
QName PROP_RS_DISPOSITION_PERIOD = QName.createQName(RM_URI, "recordSearchDispositionPeriod");
@@ -248,6 +247,8 @@ public interface RecordsManagementModel extends RecordsManagementCustomModel
QName PROP_RS_HAS_DISPOITION_SCHEDULE = QName.createQName(RM_URI, "recordSearchHasDispositionSchedule");
QName PROP_RS_DISPOITION_INSTRUCTIONS = QName.createQName(RM_URI, "recordSearchDispositionInstructions");
QName PROP_RS_DISPOITION_AUTHORITY = QName.createQName(RM_URI, "recordSearchDispositionAuthority");
+ QName PROP_RS_DECLASSIFICATION_REVIEW_COMPLETED_AT = QName.createQName(RM_URI, "declassificationReviewCompletedAt");
+ QName PROP_RS_DECLASSIFICATION_REVIEW_COMPLETED_BY = QName.createQName(RM_URI, "declassificationReviewCompletedBy");
/** @depreacted as of 2.2, because disposable items can now be in multiple holds */
@Deprecated
QName PROP_RS_HOLD_REASON = QName.createQName(RM_URI, "recordSearchHoldReason");
diff --git a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionAbstractBase.java b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionAbstractBase.java
index be367e060e..a07f4f7b0f 100644
--- a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionAbstractBase.java
+++ b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionAbstractBase.java
@@ -51,6 +51,8 @@ import org.springframework.extensions.webscripts.WebScriptRequest;
*/
public class DispositionAbstractBase extends AbstractRmWebScript
{
+
+ public final static String COMBINE_DISPOSITION_STEP_CONDITIONS = "combineDispositionStepConditions";
/**
* Parses the request and providing it's valid returns the DispositionSchedule object.
*
diff --git a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionActionDefinitionPost.java b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionActionDefinitionPost.java
index 19357a8680..1fcfcb06b9 100644
--- a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionActionDefinitionPost.java
+++ b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionActionDefinitionPost.java
@@ -133,10 +133,10 @@ public class DispositionActionDefinitionPost extends DispositionAbstractBase
json.getBoolean("eligibleOnFirstCompleteEvent") ? "or" : "and");
}
- if (json.has("combineDispositionStepConditions"))
+ if (json.has(COMBINE_DISPOSITION_STEP_CONDITIONS))
{
props.put(RecordsManagementModel.PROP_COMBINE_DISPOSITION_STEP_CONDITIONS,
- json.getBoolean("combineDispositionStepConditions"));
+ json.getBoolean(COMBINE_DISPOSITION_STEP_CONDITIONS));
}
if (json.has("location"))
diff --git a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionActionDefinitionPut.java b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionActionDefinitionPut.java
index 510118f047..8d826d6a1a 100644
--- a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionActionDefinitionPut.java
+++ b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionActionDefinitionPut.java
@@ -131,10 +131,10 @@ public class DispositionActionDefinitionPut extends DispositionAbstractBase
json.getBoolean("eligibleOnFirstCompleteEvent") ? "or" : "and");
}
- if (json.has("combineDispositionStepConditions"))
+ if (json.has(COMBINE_DISPOSITION_STEP_CONDITIONS))
{
props.put(RecordsManagementModel.PROP_COMBINE_DISPOSITION_STEP_CONDITIONS,
- json.getBoolean("combineDispositionStepConditions"));
+ json.getBoolean(COMBINE_DISPOSITION_STEP_CONDITIONS));
}
if (json.has("location"))
diff --git a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/script/slingshot/ClassificationReasonsUtil.java b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/script/slingshot/ClassificationReasonsUtil.java
new file mode 100644
index 0000000000..fe51b975b9
--- /dev/null
+++ b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/script/slingshot/ClassificationReasonsUtil.java
@@ -0,0 +1,86 @@
+/*
+ * #%L
+ * Alfresco Records Management Module
+ * %%
+ * Copyright (C) 2005 - 2018 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.module.org_alfresco_module_rm.script.slingshot;
+
+import static org.alfresco.model.ContentModel.PROP_NAME;
+import static org.alfresco.service.namespace.QName.createQName;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.repository.StoreRef;
+import org.alfresco.service.namespace.QName;
+
+/**
+ * Method to replace the plain text classification reason id with the correct nodeRef during record search
+ * @author Ross Gale
+ * @since 2.7
+ */
+public class ClassificationReasonsUtil extends SearchUtil
+{
+
+ public static final String CR_URI = "http://www.alfresco.org/model/securitymarks/1.0";
+ public static final QName CLASSIFICATION_REASONS_CONTAINER = createQName(CR_URI,"classificationReasonsContainer");
+ public static final QName PROP_CLASSIFICATION_REASON_CODE = createQName(CR_URI, "classificationReasonCode");
+ public static final String REASONS_KEY = "clf:classificationReasons:";
+
+ /**
+ * Replace plain text reason id with nodeRef
+ * @param searchQuery String e.g. clf:classificationReasons:1.4(a)
+ * @return String e.g. clf:classificationReasons:5cc6d344-fa94-4370-9c81-d947b7e8f2ac
+ */
+ public String replaceReasonWithNodeRef(String searchQuery)
+ {
+ List queries = new ArrayList<>(Arrays.asList(searchQuery.split(" ")));
+ StringBuilder stringBuilder = new StringBuilder();
+ for (String queryToEdit : queries)
+ {
+ if(queryToEdit.contains(REASONS_KEY))
+ {
+ for (String reasonId : retrieveAllNodeIds(getRootContainer(CLASSIFICATION_REASONS_CONTAINER)))
+ {
+ NodeRef reasonNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, reasonId);
+ Map properties = nodeService.getProperties(reasonNodeRef);
+ if (queryToEdit.equals(REASONS_KEY + properties.get(PROP_CLASSIFICATION_REASON_CODE).toString()) ||
+ queryToEdit.equals(REASONS_KEY +"\""+ properties.get(PROP_CLASSIFICATION_REASON_CODE).toString() + "\""))
+ {
+ queryToEdit = REASONS_KEY + properties.get(PROP_NAME).toString();
+ break;
+ }
+ }
+ }
+ stringBuilder.append(queryToEdit).append(" ");
+ }
+ return stringBuilder.toString();
+ }
+
+
+}
diff --git a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/script/slingshot/RMSearchGet.java b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/script/slingshot/RMSearchGet.java
index e8ac49ff96..ca66efefee 100644
--- a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/script/slingshot/RMSearchGet.java
+++ b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/script/slingshot/RMSearchGet.java
@@ -27,6 +27,8 @@
package org.alfresco.module.org_alfresco_module_rm.script.slingshot;
+import static org.alfresco.module.org_alfresco_module_rm.script.slingshot.ClassificationReasonsUtil.REASONS_KEY;
+
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
@@ -103,6 +105,9 @@ public class RMSearchGet extends DeclarativeWebScript
/** Utility class for record categories */
private RecordCategoryUtil recordCategoryUtil;
+ /** Utility class for classification reasons (enterprise only) */
+ private ClassificationReasonsUtil classificationReasonsUtil;
+
/**
* @param recordsManagementSearchService records management search service
*/
@@ -159,6 +164,11 @@ public class RMSearchGet extends DeclarativeWebScript
this.recordCategoryUtil = recordCategoryUtil;
}
+ public void setClassificationReasonsUtil(ClassificationReasonsUtil classificationReasonsUtil)
+ {
+ this.classificationReasonsUtil = classificationReasonsUtil;
+ }
+
/**
* @param personService person service
*/
@@ -198,6 +208,12 @@ public class RMSearchGet extends DeclarativeWebScript
String filters = req.getParameter(PARAM_FILTERS);
// TODO this is optional
+ //Replace any plain text reason ids with the appropriate node reference
+ if(query.contains(REASONS_KEY))
+ {
+ query = classificationReasonsUtil.replaceReasonWithNodeRef(query);
+ }
+
// Convert into a rm search parameter object
RecordsManagementSearchParameters searchParameters =
SavedSearchDetailsCompatibility.createSearchParameters(filters, new String[]{",", "/"}, sortby, namespaceService);
diff --git a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/script/slingshot/SearchUtil.java b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/script/slingshot/SearchUtil.java
new file mode 100644
index 0000000000..84c5b9a61d
--- /dev/null
+++ b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/script/slingshot/SearchUtil.java
@@ -0,0 +1,103 @@
+/*
+ * #%L
+ * Alfresco Records Management Module
+ * %%
+ * Copyright (C) 2005 - 2018 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.module.org_alfresco_module_rm.script.slingshot;
+
+import static org.alfresco.model.ContentModel.ASSOC_CHILDREN;
+import static org.alfresco.model.ContentModel.TYPE_CONTAINER;
+import static org.alfresco.service.cmr.repository.StoreRef.STORE_REF_WORKSPACE_SPACESSTORE;
+
+
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.alfresco.error.AlfrescoRuntimeException;
+import org.alfresco.service.cmr.repository.ChildAssociationRef;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.repository.NodeService;
+import org.alfresco.service.namespace.QName;
+
+/**
+ * Parent class for records search utilities
+ *
+ * @author Ross Gale
+ * @since 2.7
+ */
+public class SearchUtil
+{
+ /**
+ * Node service
+ */
+ protected NodeService nodeService;
+
+ /**
+ * Setter for node service
+ *
+ * @param nodeService Node service
+ */
+ public void setNodeService(NodeService nodeService)
+ {
+ this.nodeService = nodeService;
+ }
+
+ /**
+ * Use a container node ref and return the nodeIds of the contents
+ *
+ * @param nodeRef container
+ * @return list of nodeIds
+ */
+ protected Set retrieveAllNodeIds(NodeRef nodeRef)
+ {
+ List childAssocRefs = nodeService.getChildAssocs(nodeRef);
+ return childAssocRefs.stream().map(assoc -> assoc.getChildRef().getId()).collect(Collectors.toSet());
+ }
+
+ /**
+ * Helper method to get the classification reason root container.
+ * The method creates the container if it doesn't already exist.
+ *
+ * @return reference to the classification reason root container
+ */
+ protected NodeRef getRootContainer(QName container)
+ {
+ NodeRef rootNodeRef = nodeService.getRootNode(STORE_REF_WORKSPACE_SPACESSTORE);
+ List assocRefs = nodeService.getChildAssocs(rootNodeRef, ASSOC_CHILDREN, container);
+
+ if (assocRefs.isEmpty())
+ {
+ return nodeService.createNode(rootNodeRef, ASSOC_CHILDREN, container, TYPE_CONTAINER).getChildRef();
+ }
+ else if (assocRefs.size() != 1)
+ {
+ throw new AlfrescoRuntimeException("Only one container is allowed.");
+ }
+ else
+ {
+ return assocRefs.iterator().next().getChildRef();
+ }
+ }
+}
diff --git a/rm-community/rm-community-repo/unit-test/java/org/alfresco/module/org_alfresco_module_rm/script/slingshot/ClassificationReasonsUtilUnitTest.java b/rm-community/rm-community-repo/unit-test/java/org/alfresco/module/org_alfresco_module_rm/script/slingshot/ClassificationReasonsUtilUnitTest.java
new file mode 100644
index 0000000000..ac0dac5d03
--- /dev/null
+++ b/rm-community/rm-community-repo/unit-test/java/org/alfresco/module/org_alfresco_module_rm/script/slingshot/ClassificationReasonsUtilUnitTest.java
@@ -0,0 +1,128 @@
+/*
+ * #%L
+ * Alfresco Records Management Module
+ * %%
+ * Copyright (C) 2005 - 2018 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.module.org_alfresco_module_rm.script.slingshot;
+
+import static org.alfresco.model.ContentModel.ASSOC_CHILDREN;
+import static org.alfresco.model.ContentModel.PROP_NAME;
+import static org.alfresco.module.org_alfresco_module_rm.script.slingshot.ClassificationReasonsUtil.CLASSIFICATION_REASONS_CONTAINER;
+import static org.alfresco.module.org_alfresco_module_rm.script.slingshot.ClassificationReasonsUtil.PROP_CLASSIFICATION_REASON_CODE;
+import static org.alfresco.service.cmr.repository.StoreRef.STORE_REF_WORKSPACE_SPACESSTORE;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.when;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.alfresco.service.cmr.repository.ChildAssociationRef;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.repository.NodeService;
+import org.alfresco.service.namespace.QName;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * @author Ross Gale
+ * @since 2.7
+ */
+public class ClassificationReasonsUtilUnitTest
+{
+
+ @Mock
+ private NodeService nodeService;
+
+ @Mock
+ private ChildAssociationRef childAssociationRef;
+
+ @Mock
+ private ChildAssociationRef reason;
+
+ @Mock
+ private Map properties;
+
+ @InjectMocks
+ private ClassificationReasonsUtil classificationReasonsUtil;
+
+ private NodeRef childNodeRef;
+
+ @Before
+ public void setUp()
+ {
+ MockitoAnnotations.initMocks(this);
+ NodeRef rootNodeRef = new NodeRef("workspace://SpacesStore/rootNodeRef");
+ NodeRef containerNodeRef = new NodeRef("workspace://SpacesStore/containerNodeRef");
+ childNodeRef = new NodeRef("workspace://SpacesStore/childNodeRef");
+ List assocRefs = new ArrayList<>();
+ List childAssocRefs = new ArrayList<>();
+ assocRefs.add(childAssociationRef);
+ childAssocRefs.add(reason);
+ when(reason.getChildRef()).thenReturn(childNodeRef);
+ when(nodeService.getRootNode(STORE_REF_WORKSPACE_SPACESSTORE)).thenReturn(rootNodeRef);
+ when(nodeService.getChildAssocs(rootNodeRef, ASSOC_CHILDREN, CLASSIFICATION_REASONS_CONTAINER)).thenReturn(assocRefs);
+ when(childAssociationRef.getChildRef()).thenReturn(containerNodeRef);
+ when(nodeService.getChildAssocs(containerNodeRef)).thenReturn(childAssocRefs);
+ }
+
+ /**
+ * Check no modifications are made to non matching parts of the query string
+ */
+ @Test
+ public void testNoChangeMadeToStringIfKeyNotFound()
+ {
+ String stringToTest = "noChangeMadeToString";
+ assertEquals("Change made to string",stringToTest, classificationReasonsUtil.replaceReasonWithNodeRef(stringToTest).trim());
+ }
+
+ /**
+ * Check no modifications made if the plain text parameter doesn't have a stored match
+ */
+ @Test
+ public void testNoChangeMadeToStringIfMatchNotFound()
+ {
+ when(nodeService.getProperties(childNodeRef)).thenReturn(properties);
+ when(properties.get(PROP_CLASSIFICATION_REASON_CODE)).thenReturn("not a match!");
+ String stringToTest = "clf:classificationReasons:noChangeMadeToString";
+ assertEquals("Change made to string", stringToTest, classificationReasonsUtil.replaceReasonWithNodeRef(stringToTest).trim());
+ }
+
+ /**
+ * Check the query is updated correctly when a match is found
+ */
+ @Test
+ public void testChangeMadeToStringIfMatchFound()
+ {
+ when(nodeService.getProperties(childNodeRef)).thenReturn(properties);
+ when(properties.get(PROP_CLASSIFICATION_REASON_CODE)).thenReturn("stringToChange");
+ when(properties.get(PROP_NAME)).thenReturn("newString");
+ String stringToTest = "clf:classificationReasons:stringToChange";
+ assertEquals("No change made to string", "clf:classificationReasons:newString", classificationReasonsUtil.replaceReasonWithNodeRef(stringToTest).trim());
+ }
+}