From d6a8002a7cefea503c607e24614d4af45a8910a6 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Thu, 12 Jul 2018 11:46:27 -0400 Subject: [PATCH 1/9] [DS-3951] Change request-copy recipient to a List; add strategies to list collection administrators and to combine other strategies. --- ...tionAdministratorsRequestItemStrategy.java | 46 +++++++++++++++ .../CombiningRequestItemStrategy.java | 56 +++++++++++++++++++ .../dspace/app/requestitem/RequestItem.java | 18 +++++- .../app/requestitem/RequestItemAuthor.java | 32 ++++++++++- .../RequestItemAuthorExtractor.java | 3 +- .../RequestItemHelpdeskStrategy.java | 24 ++++---- .../RequestItemMetadataStrategy.java | 38 +++++++------ .../RequestItemSubmitterStrategy.java | 8 ++- dspace/config/spring/api/requestitem.xml | 47 ++++++++++++---- 9 files changed, 229 insertions(+), 43 deletions(-) create mode 100644 dspace-api/src/main/java/org/dspace/app/requestitem/CollectionAdministratorsRequestItemStrategy.java create mode 100644 dspace-api/src/main/java/org/dspace/app/requestitem/CombiningRequestItemStrategy.java diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/CollectionAdministratorsRequestItemStrategy.java b/dspace-api/src/main/java/org/dspace/app/requestitem/CollectionAdministratorsRequestItemStrategy.java new file mode 100644 index 000000000000..087198396964 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/requestitem/CollectionAdministratorsRequestItemStrategy.java @@ -0,0 +1,46 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +package org.dspace.app.requestitem; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import org.dspace.content.Collection; +import org.dspace.content.Item; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; + +/** + * Derive request recipients from groups of all collections which hold an Item. + * The list will include all members of the administrators group. If the + * resulting list is empty, delegates to {@link RequestItemHelpdeskStrategy}. + * + * @author Mark H. Wood + */ +public class CollectionAdministratorsRequestItemStrategy + implements RequestItemAuthorExtractor { + @Override + public List getRequestItemAuthor(Context context, + Item item) + throws SQLException { + List recipients = new ArrayList<>(); + for (Collection collection : item.getCollections()) { + for (EPerson admin : collection.getAdministrators().getMembers()) { + recipients.add(new RequestItemAuthor(admin)); + } + } + if (recipients.isEmpty()) { + return new RequestItemHelpdeskStrategy() + .getRequestItemAuthor(context, item); + } else { + return recipients; + } + } +} diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/CombiningRequestItemStrategy.java b/dspace-api/src/main/java/org/dspace/app/requestitem/CombiningRequestItemStrategy.java new file mode 100644 index 000000000000..7623383d5067 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/requestitem/CombiningRequestItemStrategy.java @@ -0,0 +1,56 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.requestitem; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import javax.inject.Named; + +import org.dspace.content.Item; +import org.dspace.core.Context; + +/** + * Assemble a list of recipients from the results of other strategies. + * The list of strategy classes is injected as the property {@code strategies}. + * If the property is not configured, returns an empty List. + * + * @author Mark H. Wood + */ +@Named +public class CombiningRequestItemStrategy + implements RequestItemAuthorExtractor { + /** The strategies to combine. */ + private final List strategies; + + public CombiningRequestItemStrategy(List strategies) { + this.strategies = strategies; + } + + /** + * Do not call. + * @throws IllegalArgumentException always + */ + private CombiningRequestItemStrategy() { + throw new IllegalArgumentException(); + } + + @Override + public List getRequestItemAuthor(Context context, Item item) + throws SQLException { + List recipients = new ArrayList<>(); + + if (null != strategies) { + for (RequestItemAuthorExtractor strategy : strategies) { + recipients.addAll(strategy.getRequestItemAuthor(context, item)); + } + } + + return recipients; + } +} diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItem.java b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItem.java index d96cbbb5a47a..d86b5503d632 100644 --- a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItem.java +++ b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItem.java @@ -27,7 +27,7 @@ import org.dspace.core.ReloadableEntity; /** - * Object representing an Item Request + * Object representing an Item Request. */ @Entity @Table(name = "requestitem") @@ -88,6 +88,7 @@ public class RequestItem implements ReloadableEntity { protected RequestItem() { } + @Override public Integer getID() { return requestitem_id; } @@ -96,6 +97,9 @@ void setAllfiles(boolean allfiles) { this.allfiles = allfiles; } + /** + * @return {@code true} if all of the Item's files are requested. + */ public boolean isAllfiles() { return allfiles; } @@ -104,6 +108,9 @@ void setReqMessage(String reqMessage) { this.reqMessage = reqMessage; } + /** + * @return a message from the requester. + */ public String getReqMessage() { return reqMessage; } @@ -112,6 +119,9 @@ void setReqName(String reqName) { this.reqName = reqName; } + /** + * @return Human-readable name of the user requesting access. + */ public String getReqName() { return reqName; } @@ -120,6 +130,9 @@ void setReqEmail(String reqEmail) { this.reqEmail = reqEmail; } + /** + * @return address of the user requesting access. + */ public String getReqEmail() { return reqEmail; } @@ -128,6 +141,9 @@ void setToken(String token) { this.token = token; } + /** + * @return a unique request identifier which can be emailed. + */ public String getToken() { return token; } diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemAuthor.java b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemAuthor.java index 49e26fe00bd3..9731a96ae679 100644 --- a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemAuthor.java +++ b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemAuthor.java @@ -7,6 +7,9 @@ */ package org.dspace.app.requestitem; +import java.util.ArrayList; +import java.util.List; + import org.dspace.eperson.EPerson; /** @@ -16,8 +19,8 @@ * @author Andrea Bollini */ public class RequestItemAuthor { - private String fullName; - private String email; + private final String fullName; + private final String email; public RequestItemAuthor(String fullName, String email) { super(); @@ -38,4 +41,29 @@ public String getEmail() { public String getFullName() { return fullName; } + /** + * Build a comma-list of addresses from a list of request recipients. + * @param recipients those to receive the request. + * @return addresses of the recipients, separated by ", ". + */ + public static String listAddresses(List recipients) { + List addresses = new ArrayList(recipients.size()); + for (RequestItemAuthor recipient : recipients) { + addresses.add(recipient.getEmail()); + } + return String.join(", ", addresses); + } + + /** + * Build a comma-list of full names from a list of request recipients. + * @param recipients those to receive the request. + * @return names of the recipients, separated by ", ". + */ + public static String listNames(List recipients) { + List names = new ArrayList(recipients.size()); + for (RequestItemAuthor recipient : recipients) { + names.add(recipient.getFullName()); + } + return String.join(", ", names); + } } diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemAuthorExtractor.java b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemAuthorExtractor.java index bba09131932b..ba75faa854c5 100644 --- a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemAuthorExtractor.java +++ b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemAuthorExtractor.java @@ -8,6 +8,7 @@ package org.dspace.app.requestitem; import java.sql.SQLException; +import java.util.List; import org.dspace.content.Item; import org.dspace.core.Context; @@ -19,6 +20,6 @@ * @author Andrea Bollini */ public interface RequestItemAuthorExtractor { - public RequestItemAuthor getRequestItemAuthor(Context context, Item item) + public List getRequestItemAuthor(Context context, Item item) throws SQLException; } diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemHelpdeskStrategy.java b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemHelpdeskStrategy.java index a5f7341039b7..452a368a45d3 100644 --- a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemHelpdeskStrategy.java +++ b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemHelpdeskStrategy.java @@ -8,19 +8,20 @@ package org.dspace.app.requestitem; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.Logger; import org.dspace.content.Item; -import org.dspace.core.ConfigurationManager; import org.dspace.core.Context; import org.dspace.core.I18nUtil; import org.dspace.eperson.EPerson; import org.dspace.eperson.service.EPersonService; +import org.dspace.services.ConfigurationService; import org.springframework.beans.factory.annotation.Autowired; /** - * RequestItem strategy to allow DSpace support team's helpdesk to receive requestItem request + * RequestItem strategy to allow DSpace support team's helpdesk to receive requestItem request. * With this enabled, then the Item author/submitter doesn't receive the request, but the helpdesk instead does. * * Failover to the RequestItemSubmitterStrategy, which means the submitter would get the request if there is no @@ -30,23 +31,26 @@ * @author Peter Dietz */ public class RequestItemHelpdeskStrategy extends RequestItemSubmitterStrategy { - - private Logger log = org.apache.logging.log4j.LogManager.getLogger(RequestItemHelpdeskStrategy.class); - @Autowired(required = true) protected EPersonService ePersonService; + @Autowired(required = true) + private ConfigurationService configuration; + public RequestItemHelpdeskStrategy() { } @Override - public RequestItemAuthor getRequestItemAuthor(Context context, Item item) throws SQLException { - boolean helpdeskOverridesSubmitter = ConfigurationManager + public List getRequestItemAuthor(Context context, Item item) + throws SQLException { + boolean helpdeskOverridesSubmitter = configuration .getBooleanProperty("request.item.helpdesk.override", false); - String helpDeskEmail = ConfigurationManager.getProperty("mail.helpdesk"); + String helpDeskEmail = configuration.getProperty("mail.helpdesk"); if (helpdeskOverridesSubmitter && StringUtils.isNotBlank(helpDeskEmail)) { - return getHelpDeskPerson(context, helpDeskEmail); + List authors = new ArrayList<>(1); + authors.add(getHelpDeskPerson(context, helpDeskEmail)); + return authors; } else { //Fallback to default logic (author of Item) if helpdesk isn't fully enabled or setup return super.getRequestItemAuthor(context, item); diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemMetadataStrategy.java b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemMetadataStrategy.java index 4d2f78408abd..990b19d104e1 100644 --- a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemMetadataStrategy.java +++ b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemMetadataStrategy.java @@ -8,6 +8,7 @@ package org.dspace.app.requestitem; import java.sql.SQLException; +import java.util.ArrayList; import java.util.List; import org.apache.commons.lang3.StringUtils; @@ -20,7 +21,7 @@ /** * Try to look to an item metadata for the corresponding author name and email. - * Failover to the RequestItemSubmitterStrategy + * Failover to the RequestItemSubmitterStrategy. * * @author Andrea Bollini */ @@ -36,29 +37,32 @@ public RequestItemMetadataStrategy() { } @Override - public RequestItemAuthor getRequestItemAuthor(Context context, Item item) + public List getRequestItemAuthor(Context context, Item item) throws SQLException { if (emailMetadata != null) { List vals = itemService.getMetadataByMetadataString(item, emailMetadata); if (vals.size() > 0) { - String email = vals.iterator().next().getValue(); - String fullname = null; - if (fullNameMetadata != null) { - List nameVals = itemService.getMetadataByMetadataString(item, fullNameMetadata); - if (nameVals.size() > 0) { - fullname = nameVals.iterator().next().getValue(); + List authors = new ArrayList<>(vals.size()); + for (MetadataValue datum : vals) { + String email = datum.getValue(); + String fullname = null; + if (fullNameMetadata != null) { + List nameVals = itemService.getMetadataByMetadataString(item, fullNameMetadata); + if (!nameVals.isEmpty()) { + fullname = nameVals.get(0).getValue(); + } } - } - if (StringUtils.isBlank(fullname)) { - fullname = I18nUtil - .getMessage( - "org.dspace.app.requestitem.RequestItemMetadataStrategy.unnamed", - context); + if (StringUtils.isBlank(fullname)) { + fullname = I18nUtil.getMessage( + "org.dspace.app.requestitem.RequestItemMetadataStrategy.unnamed", + context); + } + RequestItemAuthor author = new RequestItemAuthor( + fullname, email); + authors.add(author); } - RequestItemAuthor author = new RequestItemAuthor( - fullname, email); - return author; + return authors; } } return super.getRequestItemAuthor(context, item); diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemSubmitterStrategy.java b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemSubmitterStrategy.java index 8ed6238a8cb4..072451ff80a4 100644 --- a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemSubmitterStrategy.java +++ b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemSubmitterStrategy.java @@ -8,6 +8,8 @@ package org.dspace.app.requestitem; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; import org.dspace.content.Item; import org.dspace.core.Context; @@ -24,12 +26,14 @@ public RequestItemSubmitterStrategy() { } @Override - public RequestItemAuthor getRequestItemAuthor(Context context, Item item) + public List getRequestItemAuthor(Context context, Item item) throws SQLException { EPerson submitter = item.getSubmitter(); + List authors = new ArrayList<>(1); RequestItemAuthor author = new RequestItemAuthor( submitter.getFullName(), submitter.getEmail()); - return author; + authors.add(author); + return authors; } } diff --git a/dspace/config/spring/api/requestitem.xml b/dspace/config/spring/api/requestitem.xml index cd18add16d5e..8b28b8b4a923 100644 --- a/dspace/config/spring/api/requestitem.xml +++ b/dspace/config/spring/api/requestitem.xml @@ -8,22 +8,49 @@ http://www.springframework.org/schema/context/spring-context-2.5.xsd" default-autowire-candidates="*Service,*DAO,javax.sql.DataSource"> + Strategies for determining who receives Request Copy emails. + - + + + + + - + + + + + + + + + A list of references to RequestItemAuthorExtractor beans + + + + + + From 5c6cd3eca3226b7a4c4cd88ecf79e0570923060d Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Sat, 13 Mar 2021 20:22:05 -0500 Subject: [PATCH 2/9] [DS-3951] Update a newer test to cope with lists of recipients. --- .../app/rest/eperson/DeleteEPersonSubmitterIT.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/eperson/DeleteEPersonSubmitterIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/eperson/DeleteEPersonSubmitterIT.java index e280bffdeffe..5a41c6c6312b 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/eperson/DeleteEPersonSubmitterIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/eperson/DeleteEPersonSubmitterIT.java @@ -142,10 +142,10 @@ public void testArchivedItemSubmitterDelete() throws Exception { Item item = itemService.find(context, installItem.getID()); - RequestItemAuthor requestItemAuthor = requestItemAuthorExtractor.getRequestItemAuthor(context, item); + List requestItemAuthor = requestItemAuthorExtractor.getRequestItemAuthor(context, item); - assertEquals("Help Desk", requestItemAuthor.getFullName()); - assertEquals("dspace-help@myu.edu", requestItemAuthor.getEmail()); + assertEquals("Help Desk", requestItemAuthor.get(0).getFullName()); + assertEquals("dspace-help@myu.edu", requestItemAuthor.get(0).getEmail()); } /** @@ -171,7 +171,7 @@ public void testWIthdrawnItemSubmitterDelete() throws Exception { Item item = installItemService.installItem(context, wsi); - List opsToWithDraw = new ArrayList(); + List opsToWithDraw = new ArrayList<>(); ReplaceOperation replaceOperationToWithDraw = new ReplaceOperation("/withdrawn", true); opsToWithDraw.add(replaceOperationToWithDraw); String patchBodyToWithdraw = getPatchContent(opsToWithDraw); @@ -191,7 +191,7 @@ public void testWIthdrawnItemSubmitterDelete() throws Exception { assertNull(retrieveItemSubmitter(item.getID())); - List opsToReinstate = new ArrayList(); + List opsToReinstate = new ArrayList<>(); ReplaceOperation replaceOperationToReinstate = new ReplaceOperation("/withdrawn", false); opsToReinstate.add(replaceOperationToReinstate); String patchBodyToReinstate = getPatchContent(opsToReinstate); From 758b02f65c92f00954b7db2326cb46aac6bc6b22 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Fri, 25 Mar 2022 17:00:04 -0400 Subject: [PATCH 3/9] [DS-3951] A new class needs to understand multiple recipients. --- .../requestitem/RequestItemEmailNotifier.java | 38 ++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemEmailNotifier.java b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemEmailNotifier.java index d72e42eac183..02054ee1a0fc 100644 --- a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemEmailNotifier.java +++ b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemEmailNotifier.java @@ -72,28 +72,48 @@ private RequestItemEmailNotifier() {} static public void sendRequest(Context context, RequestItem ri, String responseLink) throws IOException, SQLException { // Who is making this request? - RequestItemAuthor author = requestItemAuthorExtractor + List authors = requestItemAuthorExtractor .getRequestItemAuthor(context, ri.getItem()); - String authorEmail = author.getEmail(); - String authorName = author.getFullName(); // Build an email to the approver. Email email = Email.getEmail(I18nUtil.getEmailFilename(context.getCurrentLocale(), "request_item.author")); - email.addRecipient(authorEmail); + for (RequestItemAuthor author : authors) { + email.addRecipient(author.getEmail()); + } email.setReplyTo(ri.getReqEmail()); // Requester's address + email.addArgument(ri.getReqName()); // {0} Requester's name + email.addArgument(ri.getReqEmail()); // {1} Requester's address + email.addArgument(ri.isAllfiles() // {2} All bitstreams or just one? ? I18nUtil.getMessage("itemRequest.all") : ri.getBitstream().getName()); - email.addArgument(handleService.getCanonicalForm(ri.getItem().getHandle())); + + email.addArgument(handleService.getCanonicalForm(ri.getItem().getHandle())); // {3} + email.addArgument(ri.getItem().getName()); // {4} requested item's title + email.addArgument(ri.getReqMessage()); // {5} message from requester + email.addArgument(responseLink); // {6} Link back to DSpace for action - email.addArgument(authorName); // {7} corresponding author name - email.addArgument(authorEmail); // {8} corresponding author email - email.addArgument(configurationService.getProperty("dspace.name")); - email.addArgument(configurationService.getProperty("mail.helpdesk")); + + StringBuilder names = new StringBuilder(); + StringBuilder addresses = new StringBuilder(); + for (RequestItemAuthor author : authors) { + if (names.length() > 0) { + names.append("; "); + addresses.append("; "); + } + names.append(author.getFullName()); + addresses.append(author.getEmail()); + } + email.addArgument(names.toString()); // {7} corresponding author name + email.addArgument(addresses.toString()); // {8} corresponding author email + + email.addArgument(configurationService.getProperty("dspace.name")); // {9} + + email.addArgument(configurationService.getProperty("mail.helpdesk")); // {10} // Send the email. try { From bd80b0e4c79dee5365a47f38c929a98e99dd835c Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Mon, 28 Mar 2022 10:56:37 -0400 Subject: [PATCH 4/9] [DS-3951] Fix another missed usage. --- .../rest/repository/RequestItemRepository.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/RequestItemRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/RequestItemRepository.java index d013566a2c07..82f9ed331851 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/RequestItemRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/RequestItemRepository.java @@ -15,7 +15,9 @@ import java.net.URI; import java.net.URISyntaxException; import java.sql.SQLException; +import java.util.Collections; import java.util.Date; +import java.util.List; import java.util.UUID; import javax.servlet.http.HttpServletRequest; @@ -229,14 +231,20 @@ public RequestItemRest put(Context context, HttpServletRequest request, } // Check for authorized user - RequestItemAuthor authorizer; + List authorizers; try { - authorizer = requestItemAuthorExtractor.getRequestItemAuthor(context, ri.getItem()); + authorizers = requestItemAuthorExtractor.getRequestItemAuthor(context, ri.getItem()); } catch (SQLException ex) { LOG.warn("Failed to find an authorizer: {}", ex.getMessage()); - authorizer = new RequestItemAuthor("", ""); + authorizers = Collections.EMPTY_LIST; } - if (!authorizer.getEmail().equals(context.getCurrentUser().getEmail())) { + + boolean authorized = false; + String requester = context.getCurrentUser().getEmail(); + for (RequestItemAuthor authorizer : authorizers) { + authorized |= authorizer.getEmail().equals(requester); + } + if (!authorized) { throw new AuthorizeException("Not authorized to approve this request"); } From 2312724f5e9bbdd413b826f699ac66214e77e019 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Mon, 28 Mar 2022 13:51:38 -0400 Subject: [PATCH 5/9] [DS-3951] Add tests for new strategy classes. --- ...AdministratorsRequestItemStrategyTest.java | 60 +++++++++++++++++++ .../CombiningRequestItemStrategyTest.java | 53 ++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 dspace-api/src/test/java/org/dspace/app/requestitem/CollectionAdministratorsRequestItemStrategyTest.java create mode 100644 dspace-api/src/test/java/org/dspace/app/requestitem/CombiningRequestItemStrategyTest.java diff --git a/dspace-api/src/test/java/org/dspace/app/requestitem/CollectionAdministratorsRequestItemStrategyTest.java b/dspace-api/src/test/java/org/dspace/app/requestitem/CollectionAdministratorsRequestItemStrategyTest.java new file mode 100644 index 000000000000..ffb2e5da4624 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/app/requestitem/CollectionAdministratorsRequestItemStrategyTest.java @@ -0,0 +1,60 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.requestitem; + +import static org.junit.Assert.assertEquals; + +import java.util.List; + +import org.dspace.content.Collection; +import org.dspace.content.Item; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.Group; +import org.junit.Test; +import org.mockito.Mockito; + +/** + * + * @author Mark H. Wood + */ +public class CollectionAdministratorsRequestItemStrategyTest { + private static final String NAME = "John Q. Public"; + private static final String EMAIL = "jqpublic@example.com"; + + /** + * Test of getRequestItemAuthor method, of class CollectionAdministratorsRequestItemStrategy. + * @throws java.lang.Exception passed through. + */ + @Test + public void testGetRequestItemAuthor() + throws Exception { + System.out.println("getRequestItemAuthor"); + Context context = null; + + EPerson eperson1 = Mockito.mock(EPerson.class); + Mockito.when(eperson1.getEmail()).thenReturn(EMAIL); + Mockito.when(eperson1.getFullName()).thenReturn(NAME); + + Group group1 = Mockito.mock(Group.class); + Mockito.when(group1.getMembers()).thenReturn(List.of(eperson1)); + + Collection collection1 = Mockito.mock(Collection.class); + Mockito.when(collection1.getAdministrators()).thenReturn(group1); + + Item item = Mockito.mock(Item.class); + Mockito.when(item.getCollections()).thenReturn(List.of(collection1)); + + CollectionAdministratorsRequestItemStrategy instance = new CollectionAdministratorsRequestItemStrategy(); + List result = instance.getRequestItemAuthor(context, + item); + assertEquals("Should be one author", 1, result.size()); + assertEquals("Name should match " + NAME, NAME, result.get(0).getFullName()); + assertEquals("Email should match " + EMAIL, EMAIL, result.get(0).getEmail()); + } +} diff --git a/dspace-api/src/test/java/org/dspace/app/requestitem/CombiningRequestItemStrategyTest.java b/dspace-api/src/test/java/org/dspace/app/requestitem/CombiningRequestItemStrategyTest.java new file mode 100644 index 000000000000..90047ca1bf8d --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/app/requestitem/CombiningRequestItemStrategyTest.java @@ -0,0 +1,53 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.requestitem; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder; + +import java.util.List; + +import org.dspace.content.Item; +import org.dspace.core.Context; +import org.junit.Test; +import org.mockito.Mockito; + +/** + * + * @author Mark H. Wood + */ +public class CombiningRequestItemStrategyTest { + /** + * Test of getRequestItemAuthor method, of class CombiningRequestItemStrategy. + * @throws java.lang.Exception passed through. + */ + @Test + public void testGetRequestItemAuthor() + throws Exception { + System.out.println("getRequestItemAuthor"); + Context context = null; + + Item item = Mockito.mock(Item.class); + RequestItemAuthor author1 = new RequestItemAuthor("Pat Paulsen", "ppaulsen@example.com"); + RequestItemAuthor author2 = new RequestItemAuthor("Alfred E. Neuman", "aeneuman@example.com"); + RequestItemAuthor author3 = new RequestItemAuthor("Alias Undercover", "aundercover@example.com"); + + RequestItemAuthorExtractor strategy1 = Mockito.mock(RequestItemHelpdeskStrategy.class); + Mockito.when(strategy1.getRequestItemAuthor(context, item)).thenReturn(List.of(author1)); + + RequestItemAuthorExtractor strategy2 = Mockito.mock(RequestItemHelpdeskStrategy.class); + Mockito.when(strategy2.getRequestItemAuthor(context, item)).thenReturn(List.of(author2, author3)); + + List strategies = List.of(strategy1, strategy2); + + CombiningRequestItemStrategy instance = new CombiningRequestItemStrategy(strategies); + List result = instance.getRequestItemAuthor(context, + item); + assertThat(result, containsInAnyOrder(author1, author2, author3)); + } +} From 27143d8e56982719aeeaaeaa013c0f4acd7938c8 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Fri, 12 Aug 2022 13:32:10 -0400 Subject: [PATCH 6/9] [DS-3951] Address review comments. --- ...tionAdministratorsRequestItemStrategy.java | 9 ++-- .../CombiningRequestItemStrategy.java | 23 +++++++---- .../app/requestitem/RequestItemAuthor.java | 41 ++++++------------- .../RequestItemAuthorExtractor.java | 11 ++--- .../RequestItemHelpdeskStrategy.java | 2 + .../RequestItemMetadataStrategy.java | 26 +++++++----- .../RequestItemSubmitterStrategy.java | 2 + ...AdministratorsRequestItemStrategyTest.java | 6 ++- .../CombiningRequestItemStrategyTest.java | 2 +- dspace/config/spring/api/requestitem.xml | 7 +++- 10 files changed, 68 insertions(+), 61 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/CollectionAdministratorsRequestItemStrategy.java b/dspace-api/src/main/java/org/dspace/app/requestitem/CollectionAdministratorsRequestItemStrategy.java index 087198396964..763d21758b5a 100644 --- a/dspace-api/src/main/java/org/dspace/app/requestitem/CollectionAdministratorsRequestItemStrategy.java +++ b/dspace-api/src/main/java/org/dspace/app/requestitem/CollectionAdministratorsRequestItemStrategy.java @@ -16,6 +16,7 @@ import org.dspace.content.Item; import org.dspace.core.Context; import org.dspace.eperson.EPerson; +import org.springframework.lang.NonNull; /** * Derive request recipients from groups of all collections which hold an Item. @@ -27,14 +28,14 @@ public class CollectionAdministratorsRequestItemStrategy implements RequestItemAuthorExtractor { @Override + @NonNull public List getRequestItemAuthor(Context context, Item item) throws SQLException { List recipients = new ArrayList<>(); - for (Collection collection : item.getCollections()) { - for (EPerson admin : collection.getAdministrators().getMembers()) { - recipients.add(new RequestItemAuthor(admin)); - } + Collection collection = item.getOwningCollection(); + for (EPerson admin : collection.getAdministrators().getMembers()) { + recipients.add(new RequestItemAuthor(admin)); } if (recipients.isEmpty()) { return new RequestItemHelpdeskStrategy() diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/CombiningRequestItemStrategy.java b/dspace-api/src/main/java/org/dspace/app/requestitem/CombiningRequestItemStrategy.java index 7623383d5067..8292c1a72835 100644 --- a/dspace-api/src/main/java/org/dspace/app/requestitem/CombiningRequestItemStrategy.java +++ b/dspace-api/src/main/java/org/dspace/app/requestitem/CombiningRequestItemStrategy.java @@ -10,25 +10,31 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.List; -import javax.inject.Named; import org.dspace.content.Item; import org.dspace.core.Context; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; /** * Assemble a list of recipients from the results of other strategies. - * The list of strategy classes is injected as the property {@code strategies}. - * If the property is not configured, returns an empty List. + * The list of strategy classes is injected as the constructor argument + * {@code strategies}. + * If the strategy list is not configured, returns an empty List. * * @author Mark H. Wood */ -@Named public class CombiningRequestItemStrategy implements RequestItemAuthorExtractor { /** The strategies to combine. */ private final List strategies; - public CombiningRequestItemStrategy(List strategies) { + /** + * Initialize a combination of strategies. + * @param strategies the author extraction strategies to combine. + */ + public CombiningRequestItemStrategy(@NonNull List strategies) { + Assert.notNull(strategies, "Strategy list may not be null"); this.strategies = strategies; } @@ -41,14 +47,13 @@ private CombiningRequestItemStrategy() { } @Override + @NonNull public List getRequestItemAuthor(Context context, Item item) throws SQLException { List recipients = new ArrayList<>(); - if (null != strategies) { - for (RequestItemAuthorExtractor strategy : strategies) { - recipients.addAll(strategy.getRequestItemAuthor(context, item)); - } + for (RequestItemAuthorExtractor strategy : strategies) { + recipients.addAll(strategy.getRequestItemAuthor(context, item)); } return recipients; diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemAuthor.java b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemAuthor.java index 9731a96ae679..a189e4a5efdd 100644 --- a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemAuthor.java +++ b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemAuthor.java @@ -7,14 +7,11 @@ */ package org.dspace.app.requestitem; -import java.util.ArrayList; -import java.util.List; - import org.dspace.eperson.EPerson; /** * Simple DTO to transfer data about the corresponding author for the Request - * Copy feature + * Copy feature. * * @author Andrea Bollini */ @@ -22,12 +19,23 @@ public class RequestItemAuthor { private final String fullName; private final String email; + /** + * Construct an author record from given data. + * + * @param fullName the author's full name. + * @param email the author's email address. + */ public RequestItemAuthor(String fullName, String email) { super(); this.fullName = fullName; this.email = email; } + /** + * Construct an author from an EPerson's metadata. + * + * @param ePerson the EPerson. + */ public RequestItemAuthor(EPerson ePerson) { super(); this.fullName = ePerson.getFullName(); @@ -41,29 +49,4 @@ public String getEmail() { public String getFullName() { return fullName; } - /** - * Build a comma-list of addresses from a list of request recipients. - * @param recipients those to receive the request. - * @return addresses of the recipients, separated by ", ". - */ - public static String listAddresses(List recipients) { - List addresses = new ArrayList(recipients.size()); - for (RequestItemAuthor recipient : recipients) { - addresses.add(recipient.getEmail()); - } - return String.join(", ", addresses); - } - - /** - * Build a comma-list of full names from a list of request recipients. - * @param recipients those to receive the request. - * @return names of the recipients, separated by ", ". - */ - public static String listNames(List recipients) { - List names = new ArrayList(recipients.size()); - for (RequestItemAuthor recipient : recipients) { - names.add(recipient.getFullName()); - } - return String.join(", ", names); - } } diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemAuthorExtractor.java b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemAuthorExtractor.java index 2ee26355d7a7..bc97bc64bfc2 100644 --- a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemAuthorExtractor.java +++ b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemAuthorExtractor.java @@ -12,23 +12,24 @@ import org.dspace.content.Item; import org.dspace.core.Context; +import org.springframework.lang.NonNull; /** - * Interface to abstract the strategy for select the author to contact for - * request copy + * Interface to abstract the strategy for selecting the author to contact for + * request copy. * * @author Andrea Bollini */ public interface RequestItemAuthorExtractor { /** - * Retrieve the author to contact for a request copy of the give item. + * Retrieve the author to contact for requesting a copy of the given item. * * @param context DSpace context object * @param item item to request - * @return An object containing name an email address to send the request to - * or null if no valid email address was found. + * @return Names and email addresses to send the request to. * @throws SQLException if database error */ + @NonNull public List getRequestItemAuthor(Context context, Item item) throws SQLException; } diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemHelpdeskStrategy.java b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemHelpdeskStrategy.java index 3750df1b3f7d..f440ba380a0e 100644 --- a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemHelpdeskStrategy.java +++ b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemHelpdeskStrategy.java @@ -19,6 +19,7 @@ import org.dspace.eperson.service.EPersonService; import org.dspace.services.ConfigurationService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.NonNull; /** * RequestItem strategy to allow DSpace support team's helpdesk to receive requestItem request. @@ -41,6 +42,7 @@ public RequestItemHelpdeskStrategy() { } @Override + @NonNull public List getRequestItemAuthor(Context context, Item item) throws SQLException { boolean helpdeskOverridesSubmitter = configuration diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemMetadataStrategy.java b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemMetadataStrategy.java index e4927612813a..0dcbf37531d1 100644 --- a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemMetadataStrategy.java +++ b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemMetadataStrategy.java @@ -21,6 +21,7 @@ import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.NonNull; /** * Try to look to an item metadata for the corresponding author name and email. @@ -40,21 +41,26 @@ public RequestItemMetadataStrategy() { } @Override + @NonNull public List getRequestItemAuthor(Context context, Item item) throws SQLException { List authors; if (emailMetadata != null) { List vals = itemService.getMetadataByMetadataString(item, emailMetadata); - if (vals.size() > 0) { + List nameVals; + if (null != fullNameMetadata) { + nameVals = itemService.getMetadataByMetadataString(item, fullNameMetadata); + } else { + nameVals = Collections.EMPTY_LIST; + } + boolean useNames = vals.size() == nameVals.size(); + if (!vals.isEmpty()) { authors = new ArrayList<>(vals.size()); - for (MetadataValue datum : vals) { - String email = datum.getValue(); + for (int authorIndex = 0; authorIndex < vals.size(); authorIndex++) { + String email = vals.get(authorIndex).getValue(); String fullname = null; - if (fullNameMetadata != null) { - List nameVals = itemService.getMetadataByMetadataString(item, fullNameMetadata); - if (!nameVals.isEmpty()) { - fullname = nameVals.get(0).getValue(); - } + if (useNames) { + fullname = nameVals.get(authorIndex).getValue(); } if (StringUtils.isBlank(fullname)) { @@ -100,11 +106,11 @@ public List getRequestItemAuthor(Context context, Item item) } } - public void setEmailMetadata(String emailMetadata) { + public void setEmailMetadata(@NonNull String emailMetadata) { this.emailMetadata = emailMetadata; } - public void setFullNameMetadata(String fullNameMetadata) { + public void setFullNameMetadata(@NonNull String fullNameMetadata) { this.fullNameMetadata = fullNameMetadata; } diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemSubmitterStrategy.java b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemSubmitterStrategy.java index 1212970c8930..8d2775203427 100644 --- a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemSubmitterStrategy.java +++ b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemSubmitterStrategy.java @@ -14,6 +14,7 @@ import org.dspace.content.Item; import org.dspace.core.Context; import org.dspace.eperson.EPerson; +import org.springframework.lang.NonNull; /** * Basic strategy that looks to the original submitter. @@ -33,6 +34,7 @@ public RequestItemSubmitterStrategy() { * @throws SQLException if database error */ @Override + @NonNull public List getRequestItemAuthor(Context context, Item item) throws SQLException { EPerson submitter = item.getSubmitter(); diff --git a/dspace-api/src/test/java/org/dspace/app/requestitem/CollectionAdministratorsRequestItemStrategyTest.java b/dspace-api/src/test/java/org/dspace/app/requestitem/CollectionAdministratorsRequestItemStrategyTest.java index ffb2e5da4624..37292e91c852 100644 --- a/dspace-api/src/test/java/org/dspace/app/requestitem/CollectionAdministratorsRequestItemStrategyTest.java +++ b/dspace-api/src/test/java/org/dspace/app/requestitem/CollectionAdministratorsRequestItemStrategyTest.java @@ -35,7 +35,8 @@ public class CollectionAdministratorsRequestItemStrategyTest { public void testGetRequestItemAuthor() throws Exception { System.out.println("getRequestItemAuthor"); - Context context = null; + + Context context = Mockito.mock(Context.class); EPerson eperson1 = Mockito.mock(EPerson.class); Mockito.when(eperson1.getEmail()).thenReturn(EMAIL); @@ -48,7 +49,8 @@ public void testGetRequestItemAuthor() Mockito.when(collection1.getAdministrators()).thenReturn(group1); Item item = Mockito.mock(Item.class); - Mockito.when(item.getCollections()).thenReturn(List.of(collection1)); + Mockito.when(item.getOwningCollection()).thenReturn(collection1); + Mockito.when(item.getSubmitter()).thenReturn(eperson1); CollectionAdministratorsRequestItemStrategy instance = new CollectionAdministratorsRequestItemStrategy(); List result = instance.getRequestItemAuthor(context, diff --git a/dspace-api/src/test/java/org/dspace/app/requestitem/CombiningRequestItemStrategyTest.java b/dspace-api/src/test/java/org/dspace/app/requestitem/CombiningRequestItemStrategyTest.java index 90047ca1bf8d..c5475612cb31 100644 --- a/dspace-api/src/test/java/org/dspace/app/requestitem/CombiningRequestItemStrategyTest.java +++ b/dspace-api/src/test/java/org/dspace/app/requestitem/CombiningRequestItemStrategyTest.java @@ -40,7 +40,7 @@ public void testGetRequestItemAuthor() RequestItemAuthorExtractor strategy1 = Mockito.mock(RequestItemHelpdeskStrategy.class); Mockito.when(strategy1.getRequestItemAuthor(context, item)).thenReturn(List.of(author1)); - RequestItemAuthorExtractor strategy2 = Mockito.mock(RequestItemHelpdeskStrategy.class); + RequestItemAuthorExtractor strategy2 = Mockito.mock(RequestItemMetadataStrategy.class); Mockito.when(strategy2.getRequestItemAuthor(context, item)).thenReturn(List.of(author2, author3)); List strategies = List.of(strategy1, strategy2); diff --git a/dspace/config/spring/api/requestitem.xml b/dspace/config/spring/api/requestitem.xml index f4da9539b6ca..f60d81e2389c 100644 --- a/dspace/config/spring/api/requestitem.xml +++ b/dspace/config/spring/api/requestitem.xml @@ -34,14 +34,19 @@ + + + From 237ffe4af30dfa31eb571d7e42fecd913cab1cf2 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Fri, 12 Aug 2022 14:23:45 -0400 Subject: [PATCH 7/9] [DS-3951] More review comments. --- .../CollectionAdministratorsRequestItemStrategy.java | 7 +++---- .../app/requestitem/RequestItemSubmitterStrategy.java | 6 +++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/CollectionAdministratorsRequestItemStrategy.java b/dspace-api/src/main/java/org/dspace/app/requestitem/CollectionAdministratorsRequestItemStrategy.java index 763d21758b5a..135406069ae3 100644 --- a/dspace-api/src/main/java/org/dspace/app/requestitem/CollectionAdministratorsRequestItemStrategy.java +++ b/dspace-api/src/main/java/org/dspace/app/requestitem/CollectionAdministratorsRequestItemStrategy.java @@ -19,14 +19,14 @@ import org.springframework.lang.NonNull; /** - * Derive request recipients from groups of all collections which hold an Item. + * Derive request recipients from groups of the Collection which owns an Item. * The list will include all members of the administrators group. If the * resulting list is empty, delegates to {@link RequestItemHelpdeskStrategy}. * * @author Mark H. Wood */ public class CollectionAdministratorsRequestItemStrategy - implements RequestItemAuthorExtractor { + extends RequestItemHelpdeskStrategy { @Override @NonNull public List getRequestItemAuthor(Context context, @@ -38,8 +38,7 @@ public List getRequestItemAuthor(Context context, recipients.add(new RequestItemAuthor(admin)); } if (recipients.isEmpty()) { - return new RequestItemHelpdeskStrategy() - .getRequestItemAuthor(context, item); + return super.getRequestItemAuthor(context, item); } else { return recipients; } diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemSubmitterStrategy.java b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemSubmitterStrategy.java index 8d2775203427..dcc1a3e80eac 100644 --- a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemSubmitterStrategy.java +++ b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemSubmitterStrategy.java @@ -27,10 +27,10 @@ public RequestItemSubmitterStrategy() { } /** - * Returns the submitter of an Item as RequestItemAuthor or null if the - * Submitter is deleted. + * Returns the submitter of an Item as RequestItemAuthor or an empty List if + * the Submitter is deleted. * - * @return The submitter of the item or null if the submitter is deleted + * @return The submitter of the item or empty List if the submitter is deleted * @throws SQLException if database error */ @Override From e4a4737aee53aea831db8f071b39abcc09c79573 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Fri, 26 Aug 2022 12:52:05 -0400 Subject: [PATCH 8/9] [DS-3951] Remove unused import to satisfy Checkstyle. --- .../org/dspace/app/requestitem/RequestItemMetadataStrategy.java | 1 - 1 file changed, 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemMetadataStrategy.java b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemMetadataStrategy.java index 91afb7565129..4372ab9b09b0 100644 --- a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemMetadataStrategy.java +++ b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemMetadataStrategy.java @@ -19,7 +19,6 @@ import org.dspace.core.Context; import org.dspace.core.I18nUtil; import org.dspace.services.ConfigurationService; -import org.dspace.services.factory.DSpaceServicesFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.lang.NonNull; From 07a464902c1db510ae909d3e50e978025551993d Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Tue, 30 Aug 2022 09:29:24 -0400 Subject: [PATCH 9/9] [DS-3951] Improve configuration documentation. --- dspace/config/spring/api/requestitem.xml | 40 +++++++++++++++--------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/dspace/config/spring/api/requestitem.xml b/dspace/config/spring/api/requestitem.xml index f60d81e2389c..90c49156d541 100644 --- a/dspace/config/spring/api/requestitem.xml +++ b/dspace/config/spring/api/requestitem.xml @@ -8,7 +8,14 @@ http://www.springframework.org/schema/context/spring-context-2.5.xsd" default-autowire-candidates="*Service,*DAO,javax.sql.DataSource"> - Strategies for determining who receives Request Copy emails. + + Strategies for determining who receives Request Copy emails. + A copy request "strategy" class produces a list of addresses to which a + request email should be sent. Each strategy gets its addresses from a + different source. Select the one that meets your need, or use the + CombiningRequestItemStrategy to meld the lists from two or more other + strategies. + @@ -20,18 +27,20 @@ - - + + + - + - +