From d585b3a9f1a34e9e4e6240a803114d2ce9e365da Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Wed, 4 Oct 2017 15:19:57 -0400 Subject: [PATCH 0001/1846] [DS-2670] First attempt --- .../general/CreateMissingIdentifiers.java | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 dspace-api/src/main/java/org/dspace/ctask/general/CreateMissingIdentifiers.java diff --git a/dspace-api/src/main/java/org/dspace/ctask/general/CreateMissingIdentifiers.java b/dspace-api/src/main/java/org/dspace/ctask/general/CreateMissingIdentifiers.java new file mode 100644 index 000000000000..e5afd3afe1a0 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/ctask/general/CreateMissingIdentifiers.java @@ -0,0 +1,78 @@ +/** + * 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.ctask.general; + +import java.io.IOException; +import java.sql.SQLException; +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.DSpaceObject; +import org.dspace.content.Item; +import org.dspace.core.Constants; +import org.dspace.core.Context; +import org.dspace.curate.AbstractCurationTask; +import org.dspace.curate.Curator; +import org.dspace.identifier.IdentifierException; +import org.dspace.identifier.service.IdentifierService; +import org.dspace.utils.DSpace; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Ensure that an object has all of the identifiers that it should, minting them + * as necessary. + * + * @author Mark H. Wood {@literal } + */ +public class CreateMissingIdentifiers + extends AbstractCurationTask +{ + private static final Logger LOG + = LoggerFactory.getLogger(CreateMissingIdentifiers.class); + + @Override + public int perform(DSpaceObject dso) + throws IOException + { + // Only some kinds of model objects get identifiers + if (!(dso instanceof Item)) + return Curator.CURATE_SKIP; + + String typeText = Constants.typeText[dso.getType()]; + + // Get a Context + Context context; + try { + context = Curator.curationContext(); + } catch (SQLException ex) { + report("Could not get the curation Context: " + ex.getMessage()); + return Curator.CURATE_ERROR; + } + + // Find the IdentifierService implementation + IdentifierService identifierService = new DSpace() + .getServiceManager() + .getServiceByName(null, IdentifierService.class); + + // Register any missing identifiers. + try { + identifierService.register(context, dso); + } catch (AuthorizeException | IdentifierException | SQLException ex) { + String message = ex.getMessage(); + report(String.format("Identifier(s) not minted for %s %s: %s%n", + typeText, dso.getID().toString(), message)); + LOG.error("Identifier(s) not minted: {}", message); + return Curator.CURATE_ERROR; + } + + // Success! + report(String.format("%s %s registered.%n", + typeText, dso.getID().toString())); + return Curator.CURATE_SUCCESS; + } +} From d6a8002a7cefea503c607e24614d4af45a8910a6 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Thu, 12 Jul 2018 11:46:27 -0400 Subject: [PATCH 0002/1846] [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 d28c15dae31054377acdf2f8a6fc5a36f52b0506 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Tue, 2 Jul 2019 14:51:25 -0400 Subject: [PATCH 0003/1846] [DS-4289] use a new method to write the 'contents' file. --- .../app/itemexport/ItemExportServiceImpl.java | 48 +++++++++++++++---- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportServiceImpl.java index 73bd16c7d709..ceceb6fc3df3 100644 --- a/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportServiceImpl.java @@ -63,17 +63,21 @@ * Item exporter to create simple AIPs for DSpace content. Currently exports * individual items, or entire collections. For instructions on use, see * printUsage() method. - *

+ *

* ItemExport creates the simple AIP package that the importer also uses. It * consists of: - *

- * /exportdir/42/ (one directory per item) / dublin_core.xml - qualified dublin - * core in RDF schema / contents - text file, listing one file per line / file1 - * - files contained in the item / file2 / ... - *

+ *

{@code
+ * /exportdir/42/ (one directory per item)
+ *              / dublin_core.xml - qualified dublin core in RDF schema
+ *              / contents - text file, listing one file per line
+ *              / file1 - files contained in the item
+ *              / file2
+ *              / ...
+ * }
+ *

* issues -doesn't handle special characters in metadata (needs to turn {@code &'s} into * {@code &}, etc.) - *

+ *

* Modified by David Little, UCSD Libraries 12/21/04 to allow the registration * of files (bitstreams) into DSpace. * @@ -98,7 +102,7 @@ public class ItemExportServiceImpl implements ItemExportService { /** * log4j logger */ - private Logger log = org.apache.logging.log4j.LogManager.getLogger(ItemExportServiceImpl.class); + private final Logger log = org.apache.logging.log4j.LogManager.getLogger(); protected ItemExportServiceImpl() { @@ -165,6 +169,7 @@ protected void exportItem(Context c, Item myItem, String destDirName, // make it this far, now start exporting writeMetadata(c, myItem, itemDir, migrate); writeBitstreams(c, myItem, itemDir, excludeBitstreams); + writeCollections(myItem, itemDir); if (!migrate) { writeHandle(c, myItem, itemDir); } @@ -340,6 +345,33 @@ protected void writeHandle(Context c, Item i, File destDir) } } + /** + * Create the 'collections' file. List handles of all Collections which + * contain this Item. The "owning" Collection is listed first. + * + * @param item list collections holding this Item. + * @param destDir write the file here. + * @throws IOException if the file cannot be created or written. + */ + protected void writeCollections(Item item, File destDir) + throws IOException { + File outFile = new File(destDir, "collections"); + if (outFile.createNewFile()) { + try (PrintWriter out = new PrintWriter(new FileWriter(outFile))) { + String ownerHandle = item.getOwningCollection().getHandle(); + out.println(ownerHandle); + for (Collection collection : item.getCollections()) { + String collectionHandle = collection.getHandle(); + if (!collectionHandle.equals(ownerHandle)) { + out.println(collectionHandle); + } + } + } + } else { + throw new IOException("Cannot create 'collections' in " + destDir); + } + } + /** * Create both the bitstreams and the contents file. Any bitstreams that * were originally registered will be marked in the contents file as such. From e02d91122f9b96aea1441cd82a9e4c45f0290937 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Mon, 15 Jul 2019 12:41:13 -0400 Subject: [PATCH 0004/1846] [DS-4300] Add option to use provided Handles if any. --- .../org/dspace/administer/StructBuilder.java | 65 +++++++++++++++---- 1 file changed, 51 insertions(+), 14 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/administer/StructBuilder.java b/dspace-api/src/main/java/org/dspace/administer/StructBuilder.java index d512358be0cf..841b153513a7 100644 --- a/dspace-api/src/main/java/org/dspace/administer/StructBuilder.java +++ b/dspace-api/src/main/java/org/dspace/administer/StructBuilder.java @@ -30,6 +30,7 @@ import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; +import org.apache.commons.lang3.StringUtils; import org.apache.xpath.XPathAPI; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Collection; @@ -43,6 +44,8 @@ import org.dspace.core.Context; import org.dspace.eperson.factory.EPersonServiceFactory; import org.dspace.eperson.service.EPersonService; +import org.dspace.handle.factory.HandleServiceFactory; +import org.dspace.handle.service.HandleService; import org.jdom.Element; import org.jdom.output.Format; import org.jdom.output.XMLOutputter; @@ -67,6 +70,7 @@ * * * } + * *

* It can be arbitrarily deep, and supports all the metadata elements * that make up the community and collection metadata. See the system @@ -95,12 +99,16 @@ public class StructBuilder { */ private static final Map communityMap = new HashMap<>(); - protected static CommunityService communityService + protected static final CommunityService communityService = ContentServiceFactory.getInstance().getCommunityService(); - protected static CollectionService collectionService + protected static final CollectionService collectionService = ContentServiceFactory.getInstance().getCollectionService(); - protected static EPersonService ePersonService + protected static final EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService(); + protected static final HandleService handleService + = HandleServiceFactory.getInstance().getHandleService(); + + private static boolean keepHandles; /** * Default constructor @@ -136,6 +144,7 @@ public static void main(String[] argv) options.addOption("h", "help", false, "Print this help message."); options.addOption("?", "help"); options.addOption("x", "export", false, "Export the current structure as XML."); + options.addOption("k", "keep-handles", false, "Apply Handles from input document."); options.addOption(Option.builder("e").longOpt("eperson") .desc("User who is manipulating the repository's structure.") @@ -211,6 +220,7 @@ public static void main(String[] argv) inputStream = new FileInputStream(input); } + keepHandles = options.hasOption("k"); importStructure(context, inputStream, outputStream); } System.exit(0); @@ -228,8 +238,10 @@ public static void main(String[] argv) * @throws TransformerException * @throws SQLException */ - static void importStructure(Context context, InputStream input, OutputStream output) - throws IOException, ParserConfigurationException, SQLException, TransformerException { + static void importStructure(Context context, InputStream input, + OutputStream output) + throws IOException, ParserConfigurationException, SQLException, + TransformerException { // load the XML Document document = null; @@ -255,7 +267,19 @@ static void importStructure(Context context, InputStream input, OutputStream out // Check for 'identifier' attributes -- possibly output by this class. NodeList identifierNodes = XPathAPI.selectNodeList(document, "//*[@identifier]"); if (identifierNodes.getLength() > 0) { - System.err.println("The input document has 'identifier' attributes, which will be ignored."); + if (!keepHandles) { + System.err.println("The input document has 'identifier' attributes, which will be ignored."); + } else { + for (int i = 0; i < identifierNodes.getLength() ; i++) { + String identifier = identifierNodes.item(i).getAttributes().item(0).getTextContent(); + if (handleService.resolveToURL(context, identifier) != null) { + System.err.printf("The input document contains handle %s," + + " which is in use already. Aborting...%n", + identifier); + System.exit(1); + } + } + } } // load the mappings into the member variable hashmaps @@ -608,21 +632,23 @@ private static Element[] handleCommunities(Context context, NodeList communities Element[] elements = new Element[communities.getLength()]; for (int i = 0; i < communities.getLength(); i++) { - Community community; - Element element = new Element("community"); + Node tn = communities.item(i); + Node identifier = tn.getAttributes().getNamedItem("identifier"); // create the community or sub community - if (parent != null) { + Community community; + if (null == identifier + || StringUtils.isBlank(identifier.getNodeValue()) + || !keepHandles) { community = communityService.create(parent, context); } else { - community = communityService.create(null, context); + community = communityService.create(parent, context, identifier.getNodeValue()); } // default the short description to be an empty string communityService.setMetadata(context, community, "short_description", " "); // now update the metadata - Node tn = communities.item(i); for (Map.Entry entry : communityMap.entrySet()) { NodeList nl = XPathAPI.selectNodeList(tn, entry.getKey()); if (nl.getLength() == 1) { @@ -647,6 +673,7 @@ private static Element[] handleCommunities(Context context, NodeList communities // but it's here to keep it separate from the create process in // case // we want to move it or make it switchable later + Element element = new Element("community"); element.setAttribute("identifier", community.getHandle()); Element nameElement = new Element("name"); @@ -713,14 +740,23 @@ private static Element[] handleCollections(Context context, NodeList collections Element[] elements = new Element[collections.getLength()]; for (int i = 0; i < collections.getLength(); i++) { - Element element = new Element("collection"); - Collection collection = collectionService.create(context, parent); + Node tn = collections.item(i); + Node identifier = tn.getAttributes().getNamedItem("identifier"); + + // Create the Collection. + Collection collection; + if (null == identifier + || StringUtils.isBlank(identifier.getNodeValue()) + || !keepHandles) { + collection = collectionService.create(context, parent); + } else { + collection = collectionService.create(context, parent, identifier.getNodeValue()); + } // default the short description to the empty string collectionService.setMetadata(context, collection, "short_description", " "); // import the rest of the metadata - Node tn = collections.item(i); for (Map.Entry entry : collectionMap.entrySet()) { NodeList nl = XPathAPI.selectNodeList(tn, entry.getKey()); if (nl.getLength() == 1) { @@ -730,6 +766,7 @@ private static Element[] handleCollections(Context context, NodeList collections collectionService.update(context, collection); + Element element = new Element("collection"); element.setAttribute("identifier", collection.getHandle()); Element nameElement = new Element("name"); From 2a9eb2048bb2669473cb4981b5817cf83cb0c897 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Fri, 3 Jan 2020 12:05:10 -0500 Subject: [PATCH 0005/1846] [DS-4110] New email template sent to a newly-registered user. --- .../org/dspace/eperson/EPersonConsumer.java | 40 +++++++++++++++---- dspace/config/dspace.cfg | 3 ++ dspace/config/emails/welcome | 15 +++++++ 3 files changed, 51 insertions(+), 7 deletions(-) create mode 100644 dspace/config/emails/welcome diff --git a/dspace-api/src/main/java/org/dspace/eperson/EPersonConsumer.java b/dspace-api/src/main/java/org/dspace/eperson/EPersonConsumer.java index 319dcad65ce4..373f7fee8054 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/EPersonConsumer.java +++ b/dspace-api/src/main/java/org/dspace/eperson/EPersonConsumer.java @@ -7,12 +7,13 @@ */ package org.dspace.eperson; +import java.io.IOException; import java.util.Date; import java.util.UUID; import javax.mail.MessagingException; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; -import org.dspace.core.ConfigurationManager; import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.core.Email; @@ -22,6 +23,8 @@ import org.dspace.eperson.service.EPersonService; import org.dspace.event.Consumer; import org.dspace.event.Event; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; /** * Class for handling updates to EPersons @@ -29,16 +32,19 @@ * Recommended filter: EPerson+Create * * @author Stuart Lewis - * @version $Revision$ */ public class EPersonConsumer implements Consumer { /** * log4j logger */ - private static Logger log = org.apache.logging.log4j.LogManager.getLogger(EPersonConsumer.class); + private static final Logger log + = org.apache.logging.log4j.LogManager.getLogger(EPersonConsumer.class); protected EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService(); + protected ConfigurationService cfg + = DSpaceServicesFactory.getInstance().getConfigurationService(); + /** * Initalise the consumer * @@ -69,7 +75,8 @@ public void consume(Context context, Event event) case Constants.EPERSON: if (et == Event.CREATE) { // Notify of new user registration - String notifyRecipient = ConfigurationManager.getProperty("registration.notify"); + String notifyRecipient = cfg.getProperty("registration.notify"); + EPerson eperson = ePersonService.find(context, id); if (notifyRecipient == null) { notifyRecipient = ""; } @@ -77,13 +84,12 @@ public void consume(Context context, Event event) if (!notifyRecipient.equals("")) { try { - EPerson eperson = ePersonService.find(context, id); Email adminEmail = Email .getEmail(I18nUtil.getEmailFilename(context.getCurrentLocale(), "registration_notify")); adminEmail.addRecipient(notifyRecipient); - adminEmail.addArgument(ConfigurationManager.getProperty("dspace.name")); - adminEmail.addArgument(ConfigurationManager.getProperty("dspace.ui.url")); + adminEmail.addArgument(cfg.getProperty("dspace.name")); + adminEmail.addArgument(cfg.getProperty("dspace.ui.url")); adminEmail.addArgument(eperson.getFirstName() + " " + eperson.getLastName()); // Name adminEmail.addArgument(eperson.getEmail()); adminEmail.addArgument(new Date()); @@ -99,6 +105,26 @@ public void consume(Context context, Event event) "error_emailing_administrator", ""), me); } } + + // If enabled, send a "welcome" message to the new EPerson. + if (cfg.getBooleanProperty("mail.welcome.enabled", false)) { + String addressee = eperson.getEmail(); + if (StringUtils.isNotBlank(addressee)) { + log.debug("Sending welcome email to {}", addressee); + try { + Email message = Email.getEmail( + I18nUtil.getEmailFilename(context.getCurrentLocale(), "welcome")); + message.addRecipient(addressee); + message.send(); + } catch (IOException | MessagingException ex) { + log.warn("Welcome message not sent to {}: {}", + addressee, ex.getMessage()); + } + } else { + log.warn("Welcome message not sent to EPerson {} because it has no email address.", + eperson.getID().toString()); + } + } } else if (et == Event.DELETE) { // TODO: Implement this if required } diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index 1bbd8951315c..e3ce2f426ae0 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -128,6 +128,9 @@ alert.recipient = ${mail.admin} # Recipient for new user registration emails (defaults to unspecified) #registration.notify = +# Enable a "welcome letter" to the newly-registered user. +mail.welcome.enabled = false + # Set the default mail character set. This may be overridden by providing a line # inside the email template "charset: ", otherwise this default is used. mail.charset = UTF-8 diff --git a/dspace/config/emails/welcome b/dspace/config/emails/welcome new file mode 100644 index 000000000000..febc082e072e --- /dev/null +++ b/dspace/config/emails/welcome @@ -0,0 +1,15 @@ +## E-mail sent to a DSpace user after a new account is registered. +## +## See org.dspace.core.Email for information on the format of this file. +## +#set($subject = "Welcome new registered ${config.get('dspace.name')} user!") +Thank you for registering an account. Your new account can be used immediately +to subscribe to notices of new content arriving in collections of your choice. + +Your new account can also be granted privileges to submit new content, or to +edit and/or approve submissions. + +If you need assistance with your account, please email +${config.get("mail.admin")}. + +The ${config.get('dspace.name')} Team From d21b019c71488def1e2bca9249fd7ca30a9bd9db Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Tue, 4 Aug 2020 15:21:27 -0400 Subject: [PATCH 0006/1846] [DS-2670] Fix concompliance with new format rules. --- .../dspace/ctask/general/CreateMissingIdentifiers.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/ctask/general/CreateMissingIdentifiers.java b/dspace-api/src/main/java/org/dspace/ctask/general/CreateMissingIdentifiers.java index e5afd3afe1a0..8eba91697158 100644 --- a/dspace-api/src/main/java/org/dspace/ctask/general/CreateMissingIdentifiers.java +++ b/dspace-api/src/main/java/org/dspace/ctask/general/CreateMissingIdentifiers.java @@ -10,6 +10,7 @@ import java.io.IOException; import java.sql.SQLException; + import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; @@ -30,18 +31,17 @@ * @author Mark H. Wood {@literal } */ public class CreateMissingIdentifiers - extends AbstractCurationTask -{ + extends AbstractCurationTask { private static final Logger LOG = LoggerFactory.getLogger(CreateMissingIdentifiers.class); @Override public int perform(DSpaceObject dso) - throws IOException - { + throws IOException { // Only some kinds of model objects get identifiers - if (!(dso instanceof Item)) + if (!(dso instanceof Item)) { return Curator.CURATE_SKIP; + } String typeText = Constants.typeText[dso.getType()]; From 5c6cd3eca3226b7a4c4cd88ecf79e0570923060d Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Sat, 13 Mar 2021 20:22:05 -0500 Subject: [PATCH 0007/1846] [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 81a72354325954191500353a817f932da4f92cd6 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Wed, 25 Aug 2021 10:33:50 -0400 Subject: [PATCH 0008/1846] Take up many IDE suggestions. --- .../dspace/app/statistics/LogAnalyser.java | 105 +++++++++--------- 1 file changed, 54 insertions(+), 51 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/statistics/LogAnalyser.java b/dspace-api/src/main/java/org/dspace/app/statistics/LogAnalyser.java index 0ece47d32d1f..fbb06acb67c1 100644 --- a/dspace-api/src/main/java/org/dspace/app/statistics/LogAnalyser.java +++ b/dspace-api/src/main/java/org/dspace/app/statistics/LogAnalyser.java @@ -236,12 +236,12 @@ public class LogAnalyser { /** * pattern to match commented out lines from the config file */ - private static final Pattern comment = Pattern.compile("^#"); + private static final Pattern COMMENT = Pattern.compile("^#"); /** * pattern to match genuine lines from the config file */ - private static final Pattern real = Pattern.compile("^(.+)=(.+)"); + private static final Pattern REAL = Pattern.compile("^(.+)=(.+)"); /** * pattern to match all search types @@ -406,18 +406,18 @@ public static String processLogs(Context context, String myLogDir, startTime = new GregorianCalendar(); //instantiate aggregators - actionAggregator = new HashMap(); - searchAggregator = new HashMap(); - userAggregator = new HashMap(); - itemAggregator = new HashMap(); - archiveStats = new HashMap(); + actionAggregator = new HashMap<>(); + searchAggregator = new HashMap<>(); + userAggregator = new HashMap<>(); + itemAggregator = new HashMap<>(); + archiveStats = new HashMap<>(); //instantiate lists - generalSummary = new ArrayList(); - excludeWords = new ArrayList(); - excludeTypes = new ArrayList(); - excludeChars = new ArrayList(); - itemTypes = new ArrayList(); + generalSummary = new ArrayList<>(); + excludeWords = new ArrayList<>(); + excludeTypes = new ArrayList<>(); + excludeChars = new ArrayList<>(); + itemTypes = new ArrayList<>(); // set the parameters for this analysis setParameters(myLogDir, myFileTemplate, myConfigFile, myOutFile, myStartDate, myEndDate, myLookUp); @@ -529,10 +529,11 @@ public static String processLogs(Context context, String myLogDir, // for each search word add to the aggregator or // increment the aggregator's counter - for (int j = 0; j < words.length; j++) { + for (String word : words) { // FIXME: perhaps aggregators ought to be objects // themselves - searchAggregator.put(words[j], increment(searchAggregator, words[j])); + searchAggregator.put(word, + increment(searchAggregator, word)); } } @@ -591,7 +592,7 @@ public static String processLogs(Context context, String myLogDir, } // do the average views analysis - if ((archiveStats.get("All Items")).intValue() != 0) { + if ((archiveStats.get("All Items")) != 0) { // FIXME: this is dependent on their being a query on the db, which // there might not always be if it becomes configurable Double avg = Math.ceil( @@ -672,55 +673,55 @@ public static String createOutput() { Iterator keys = null; // output the number of lines parsed - summary.append("log_lines=" + Integer.toString(lineCount) + "\n"); + summary.append("log_lines=").append(Integer.toString(lineCount)).append("\n"); // output the number of warnings encountered - summary.append("warnings=" + Integer.toString(warnCount) + "\n"); - summary.append("exceptions=" + Integer.toString(excCount) + "\n"); + summary.append("warnings=").append(Integer.toString(warnCount)).append("\n"); + summary.append("exceptions=").append(Integer.toString(excCount)).append("\n"); // set the general summary config up in the aggregator file for (int i = 0; i < generalSummary.size(); i++) { - summary.append("general_summary=" + generalSummary.get(i) + "\n"); + summary.append("general_summary=").append(generalSummary.get(i)).append("\n"); } // output the host name - summary.append("server_name=" + hostName + "\n"); + summary.append("server_name=").append(hostName).append("\n"); // output the service name - summary.append("service_name=" + name + "\n"); + summary.append("service_name=").append(name).append("\n"); // output the date information if necessary SimpleDateFormat sdf = new SimpleDateFormat("dd'/'MM'/'yyyy"); if (startDate != null) { - summary.append("start_date=" + sdf.format(startDate) + "\n"); + summary.append("start_date=").append(sdf.format(startDate)).append("\n"); } else if (logStartDate != null) { - summary.append("start_date=" + sdf.format(logStartDate) + "\n"); + summary.append("start_date=").append(sdf.format(logStartDate)).append("\n"); } if (endDate != null) { - summary.append("end_date=" + sdf.format(endDate) + "\n"); + summary.append("end_date=").append(sdf.format(endDate)).append("\n"); } else if (logEndDate != null) { - summary.append("end_date=" + sdf.format(logEndDate) + "\n"); + summary.append("end_date=").append(sdf.format(logEndDate)).append("\n"); } // write out the archive stats keys = archiveStats.keySet().iterator(); while (keys.hasNext()) { String key = keys.next(); - summary.append("archive." + key + "=" + archiveStats.get(key) + "\n"); + summary.append("archive.").append(key).append("=").append(archiveStats.get(key)).append("\n"); } // write out the action aggregation results keys = actionAggregator.keySet().iterator(); while (keys.hasNext()) { String key = keys.next(); - summary.append("action." + key + "=" + actionAggregator.get(key) + "\n"); + summary.append("action.").append(key).append("=").append(actionAggregator.get(key)).append("\n"); } // depending on the config settings for reporting on emails output the // login information - summary.append("user_email=" + userEmail + "\n"); + summary.append("user_email=").append(userEmail).append("\n"); int address = 1; keys = userAggregator.keySet().iterator(); @@ -731,9 +732,10 @@ public static String createOutput() { String key = keys.next(); summary.append("user."); if (userEmail.equals("on")) { - summary.append(key + "=" + userAggregator.get(key) + "\n"); + summary.append(key).append("=").append(userAggregator.get(key)).append("\n"); } else if (userEmail.equals("alias")) { - summary.append("Address " + Integer.toString(address++) + "=" + userAggregator.get(key) + "\n"); + summary.append("Address ").append(Integer.toString(address++)) + .append("=").append(userAggregator.get(key)).append("\n"); } } @@ -742,12 +744,13 @@ public static String createOutput() { // the listing there are // output the search word information - summary.append("search_floor=" + searchFloor + "\n"); + summary.append("search_floor=").append(searchFloor).append("\n"); keys = searchAggregator.keySet().iterator(); while (keys.hasNext()) { String key = keys.next(); - if ((searchAggregator.get(key)).intValue() >= searchFloor) { - summary.append("search." + key + "=" + searchAggregator.get(key) + "\n"); + if ((searchAggregator.get(key)) >= searchFloor) { + summary.append("search.").append(key).append("=") + .append(searchAggregator.get(key)).append("\n"); } } @@ -759,35 +762,35 @@ public static String createOutput() { // be the same thing. // item viewing information - summary.append("item_floor=" + itemFloor + "\n"); - summary.append("host_url=" + url + "\n"); - summary.append("item_lookup=" + itemLookup + "\n"); + summary.append("item_floor=").append(itemFloor).append("\n"); + summary.append("host_url=").append(url).append("\n"); + summary.append("item_lookup=").append(itemLookup).append("\n"); // write out the item access information keys = itemAggregator.keySet().iterator(); while (keys.hasNext()) { String key = keys.next(); - if ((itemAggregator.get(key)).intValue() >= itemFloor) { - summary.append("item." + key + "=" + itemAggregator.get(key) + "\n"); + if ((itemAggregator.get(key)) >= itemFloor) { + summary.append("item.").append(key).append("=") + .append(itemAggregator.get(key)).append("\n"); } } // output the average views per item if (views > 0) { - summary.append("avg_item_views=" + views + "\n"); + summary.append("avg_item_views=").append(views).append("\n"); } // insert the analysis processing time information Calendar endTime = new GregorianCalendar(); long timeInMillis = (endTime.getTimeInMillis() - startTime.getTimeInMillis()); - summary.append("analysis_process_time=" + Long.toString(timeInMillis / 1000) + "\n"); + summary.append("analysis_process_time=") + .append(Long.toString(timeInMillis / 1000)).append("\n"); // finally write the string into the output file - try { - BufferedWriter out = new BufferedWriter(new FileWriter(outFile)); + try (BufferedWriter out = new BufferedWriter(new FileWriter(outFile));) { out.write(summary.toString()); out.flush(); - out.close(); } catch (IOException e) { System.out.println("Unable to write to output file " + outFile); System.exit(0); @@ -891,11 +894,11 @@ public static void setRegex(String fileTemplate) { if (i > 0) { wordRXString.append("|"); } - wordRXString.append(" " + excludeWords.get(i) + " "); + wordRXString.append(" ").append(excludeWords.get(i)).append(" "); wordRXString.append("|"); - wordRXString.append("^" + excludeWords.get(i) + " "); + wordRXString.append("^").append(excludeWords.get(i)).append(" "); wordRXString.append("|"); - wordRXString.append(" " + excludeWords.get(i) + "$"); + wordRXString.append(" ").append(excludeWords.get(i)).append("$"); } wordRXString.append(")"); wordRX = Pattern.compile(wordRXString.toString()); @@ -956,8 +959,8 @@ public static void readConfig(String configFile) throws IOException { // read in the config file and set up our instance variables while ((record = br.readLine()) != null) { // check to see what kind of line we have - Matcher matchComment = comment.matcher(record); - Matcher matchReal = real.matcher(record); + Matcher matchComment = COMMENT.matcher(record); + Matcher matchReal = REAL.matcher(record); // if the line is not a comment and is real, read it in if (!matchComment.matches() && matchReal.matches()) { @@ -968,7 +971,7 @@ public static void readConfig(String configFile) throws IOException { // read the config values into our instance variables (see // documentation for more info on config params) if (key.equals("general.summary")) { - actionAggregator.put(value, Integer.valueOf(0)); + actionAggregator.put(value, 0); generalSummary.add(value); } @@ -1022,9 +1025,9 @@ public static Integer increment(Map map, String key) { Integer newValue = null; if (map.containsKey(key)) { // FIXME: this seems like a ridiculous way to add Integers - newValue = Integer.valueOf((map.get(key)).intValue() + 1); + newValue = (map.get(key)) + 1; } else { - newValue = Integer.valueOf(1); + newValue = 1; } return newValue; } From 702a72ffab9c9b662f185fd54d70f570a7b4956d Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Wed, 25 Aug 2021 12:58:27 -0400 Subject: [PATCH 0009/1846] Replace handmade option parsing with Commons CLI to address LGTM array bounds complaints. --- .../dspace/app/statistics/LogAnalyser.java | 88 +++++++++++++------ .../app/statistics/ReportGenerator.java | 64 +++++++++----- 2 files changed, 103 insertions(+), 49 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/statistics/LogAnalyser.java b/dspace-api/src/main/java/org/dspace/app/statistics/LogAnalyser.java index fbb06acb67c1..6c6cefd9e414 100644 --- a/dspace-api/src/main/java/org/dspace/app/statistics/LogAnalyser.java +++ b/dspace-api/src/main/java/org/dspace/app/statistics/LogAnalyser.java @@ -29,6 +29,10 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; import org.apache.commons.lang3.StringUtils; import org.dspace.core.Context; import org.dspace.core.LogManager; @@ -337,44 +341,72 @@ public static void main(String[] argv) Date myEndDate = null; boolean myLookUp = false; - // read in our command line options - for (int i = 0; i < argv.length; i++) { - if (argv[i].equals("-log")) { - myLogDir = argv[i + 1]; - } + // Define command line options. + Options options = new Options(); + Option option; - if (argv[i].equals("-file")) { - myFileTemplate = argv[i + 1]; - } + option = Option.builder().longOpt("log").hasArg().build(); + options.addOption(option); - if (argv[i].equals("-cfg")) { - myConfigFile = argv[i + 1]; - } + option = Option.builder().longOpt("file").hasArg().build(); + options.addOption(option); - if (argv[i].equals("-out")) { - myOutFile = argv[i + 1]; - } + option = Option.builder().longOpt("cfg").hasArg().build(); + options.addOption(option); - if (argv[i].equals("-help")) { - LogAnalyser.usage(); - System.exit(0); - } + option = Option.builder().longOpt("out").hasArg().build(); + options.addOption(option); - if (argv[i].equals("-start")) { - myStartDate = parseDate(argv[i + 1]); - } + option = Option.builder().longOpt("help").build(); + options.addOption(option); - if (argv[i].equals("-end")) { - myEndDate = parseDate(argv[i + 1]); - } + option = Option.builder().longOpt("start").hasArg().build(); + options.addOption(option); - if (argv[i].equals("-lookup")) { - myLookUp = true; - } + option = Option.builder().longOpt("end").hasArg().build(); + options.addOption(option); + + option = Option.builder().longOpt("lookup").build(); + + // Parse the command. + DefaultParser cmdParser = new DefaultParser(); + CommandLine cmd = cmdParser.parse(options, argv); + + // Analyze the command. + if (cmd.hasOption("help")) { + LogAnalyser.usage(); + System.exit(0); } + if (cmd.hasOption("log")) { + myLogDir = cmd.getOptionValue("log"); + } + + if (cmd.hasOption("file")) { + myFileTemplate = cmd.getOptionValue("file"); + } + + if (cmd.hasOption("cfg")) { + myConfigFile = cmd.getOptionValue("cfg"); + } + + if (cmd.hasOption("out")) { + myOutFile = cmd.getOptionValue("out"); + } + + if (cmd.hasOption("start")) { + myStartDate = parseDate(cmd.getOptionValue("start")); + } + + if (cmd.hasOption("end")) { + myEndDate = parseDate(cmd.getOptionValue("end")); + } + + myLookUp = cmd.hasOption("lookup"); + // now call the method which actually processes the logs - processLogs(context, myLogDir, myFileTemplate, myConfigFile, myOutFile, myStartDate, myEndDate, myLookUp); + processLogs(context, myLogDir, myFileTemplate, myConfigFile, myOutFile, + myStartDate, myEndDate, myLookUp); } /** diff --git a/dspace-api/src/main/java/org/dspace/app/statistics/ReportGenerator.java b/dspace-api/src/main/java/org/dspace/app/statistics/ReportGenerator.java index 25c6d8cb9cf8..c5fe0072f514 100644 --- a/dspace-api/src/main/java/org/dspace/app/statistics/ReportGenerator.java +++ b/dspace-api/src/main/java/org/dspace/app/statistics/ReportGenerator.java @@ -27,6 +27,10 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; import org.dspace.content.Item; import org.dspace.content.MetadataSchemaEnum; import org.dspace.content.MetadataValue; @@ -162,7 +166,7 @@ public class ReportGenerator { /** * pattern that matches an unqualified aggregator property */ - private static final Pattern real = Pattern.compile("^(.+)=(.+)"); + private static final Pattern REAL = Pattern.compile("^(.+)=(.+)"); ////////////////////////// // Miscellaneous variables @@ -221,28 +225,46 @@ public static void main(String[] argv) String myOutput = null; String myMap = null; - // read in our command line options - for (int i = 0; i < argv.length; i++) { - if (argv[i].equals("-format")) { - myFormat = argv[i + 1].toLowerCase(); - } + Options options = new Options(); + Option option; - if (argv[i].equals("-in")) { - myInput = argv[i + 1]; - } + option = Option.builder().longOpt("format").hasArg().build(); + options.addOption(option); - if (argv[i].equals("-out")) { - myOutput = argv[i + 1]; - } + option = Option.builder().longOpt("in").hasArg().build(); + options.addOption(option); - if (argv[i].equals("-map")) { - myMap = argv[i + 1]; - } + option = Option.builder().longOpt("out").hasArg().build(); + options.addOption(option); - if (argv[i].equals("-help")) { - usage(); - System.exit(0); - } + option = Option.builder().longOpt("map").hasArg().build(); + options.addOption(option); + + option = Option.builder().longOpt("help").build(); + options.addOption(option); + + DefaultParser parser = new DefaultParser(); + CommandLine cmd = parser.parse(options, argv); + + if (cmd.hasOption("help")) { + usage(); + System.exit(0); + } + + if (cmd.hasOption("format")) { + myFormat = cmd.getOptionValue("format"); + } + + if (cmd.hasOption("in")) { + myInput = cmd.getOptionValue("in"); + } + + if (cmd.hasOption("out")) { + myOutput = cmd.getOptionValue("out"); + } + + if (cmd.hasOption("map")) { + myMap = cmd.getOptionValue("map"); } processReport(context, myFormat, myInput, myOutput, myMap); @@ -576,7 +598,7 @@ public static void readMap(String map) // loop through the map file and read in the values while ((record = br.readLine()) != null) { - Matcher matchReal = real.matcher(record); + Matcher matchReal = REAL.matcher(record); // if the line is real then read it in if (matchReal.matches()) { @@ -650,7 +672,7 @@ public static void readInput(String input) // loop through the aggregator file and read in the values while ((record = br.readLine()) != null) { // match real lines - Matcher matchReal = real.matcher(record); + Matcher matchReal = REAL.matcher(record); // pre-prepare our input strings String section = null; From 48d409d87d1afac34af643127034fa4e563e63de Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Wed, 25 Aug 2021 14:09:37 -0400 Subject: [PATCH 0010/1846] Formalize more option parsing. --- .../storage/bitstore/S3BitStoreService.java | 48 +++-- .../org/purl/sword/client/ClientOptions.java | 199 +++++++++--------- 2 files changed, 132 insertions(+), 115 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java b/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java index ce2b3b3f05a9..004a8bba042f 100644 --- a/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java +++ b/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java @@ -25,6 +25,12 @@ import com.amazonaws.services.s3.model.PutObjectRequest; import com.amazonaws.services.s3.model.PutObjectResult; import com.amazonaws.services.s3.model.S3Object; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpStatus; @@ -324,27 +330,37 @@ public void setSubfolder(String subfolder) { * @throws Exception generic exception */ public static void main(String[] args) throws Exception { - //TODO use proper CLI, or refactor to be a unit test. Can't mock this without keys though. + //TODO Perhaps refactor to be a unit test. Can't mock this without keys though. // parse command line - String assetFile = null; - String accessKey = null; - String secretKey = null; - - for (int i = 0; i < args.length; i += 2) { - if (args[i].startsWith("-a")) { - accessKey = args[i + 1]; - } else if (args[i].startsWith("-s")) { - secretKey = args[i + 1]; - } else if (args[i].startsWith("-f")) { - assetFile = args[i + 1]; - } - } + Options options = new Options(); + Option option; + + option = Option.builder("a").desc("access key").hasArg().required().build(); + options.addOption(option); + + option = Option.builder("s").desc("secret key").hasArg().required().build(); + options.addOption(option); + + option = Option.builder("f").desc("asset file name").hasArg().required().build(); + options.addOption(option); - if (accessKey == null || secretKey == null || assetFile == null) { - System.out.println("Missing arguments - exiting"); + DefaultParser parser = new DefaultParser(); + + CommandLine command; + try { + command = parser.parse(options, args); + } catch (ParseException e) { + System.err.println(e.getMessage()); + new HelpFormatter().printHelp( + S3BitStoreService.class.getSimpleName() + "options", options); return; } + + String accessKey = command.getOptionValue("a"); + String secretKey = command.getOptionValue("s"); + String assetFile = command.getOptionValue("f"); + S3BitStoreService store = new S3BitStoreService(); AWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); diff --git a/dspace-sword/src/main/java/org/purl/sword/client/ClientOptions.java b/dspace-sword/src/main/java/org/purl/sword/client/ClientOptions.java index e085baeb7826..688b61ad1765 100644 --- a/dspace-sword/src/main/java/org/purl/sword/client/ClientOptions.java +++ b/dspace-sword/src/main/java/org/purl/sword/client/ClientOptions.java @@ -13,6 +13,12 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; /** @@ -72,7 +78,7 @@ public class ClientOptions { private String filename = null; /** - * Filetype. + * File type. */ private String filetype = null; @@ -117,17 +123,18 @@ public class ClientOptions { /** * Logger. */ - private static Logger log = org.apache.logging.log4j.LogManager.getLogger(ClientOptions.class); + private static final Logger log = LogManager.getLogger(); /** * List of multiple destination items. Used if the mode is set to multipost. */ - private List multiPost = new ArrayList(); + private final List multiPost = new ArrayList<>(); /** * Pattern string to extract the data from a destination parameter in multipost mode. */ - private static final Pattern multiPattern = Pattern.compile("(.*?)(\\[(.*?)\\]) {0,1}(:(.*)) {0,1}@(http://.*)"); + private static final Pattern MULTI_PATTERN + = Pattern.compile("(.*?)(\\[(.*?)\\]) {0,1}(:(.*)) {0,1}@(http://.*)"); /** * Flag that indicates if the GUI mode has been set. This is @@ -148,128 +155,122 @@ public class ClientOptions { * @return True if the options were parsed successfully. */ public boolean parseOptions(String[] args) { - try { - // iterate over the args - for (int i = 0; i < args.length; i++) { - if ("-md5".equals(args[i])) { - md5 = true; - } + Options options = new Options(); + options.addOption(Option.builder().longOpt("md5").build()) + .addOption(Option.builder().longOpt("noOp").build()) + .addOption(Option.builder().longOpt("verbose").build()) + .addOption(Option.builder().longOpt("cmd").build()) + .addOption(Option.builder().longOpt("gui").build()) + .addOption(Option.builder().longOpt("help").build()) + .addOption(Option.builder().longOpt("nocapture").build()); - if ("-noOp".equals(args[i])) { - noOp = true; - } + Option option; - if ("-verbose".equals(args[i])) { - verbose = true; - } + option = Option.builder().longOpt("host").hasArg().build(); + options.addOption(option); - if ("-cmd".equals(args[i])) { - guiMode = false; - } + option = Option.builder().longOpt("port").hasArg().build(); + options.addOption(option); - if ("-gui".equals(args[i])) { - guiMode = true; - } + option = Option.builder("u").hasArg().build(); + options.addOption(option); - if ("-host".equals(args[i])) { - i++; - proxyHost = args[i]; - } + option = Option.builder("p").hasArg().build(); + options.addOption(option); - if ("-port".equals(args[i])) { - i++; - proxyPort = Integer.parseInt(args[i]); - } + option = Option.builder().longOpt("href").hasArg().build(); + options.addOption(option); - if ("-u".equals(args[i])) { - i++; - username = args[i]; - } + option = Option.builder("t").hasArg().build(); + options.addOption(option); - if ("-p".equals(args[i])) { - i++; - password = args[i]; - } + option = Option.builder().longOpt("file").hasArg().build(); + options.addOption(option); - if ("-href".equals(args[i])) { - i++; - href = args[i]; - } + option = Option.builder().longOpt("filetype").hasArg().build(); + options.addOption(option); - if ("-help".equals(args[i])) { - // force the calling code to display the usage information - return false; - } + option = Option.builder().longOpt("slug").hasArg().build(); + options.addOption(option); - if ("-t".equals(args[i])) { - i++; - accessType = args[i]; - } - - if ("-file".equals(args[i])) { - i++; - filename = args[i]; - } + option = Option.builder().longOpt("onBehalfOf").hasArg().build(); + options.addOption(option); - if ("-filetype".equals(args[i])) { - i++; - filetype = args[i]; - } + option = Option.builder().longOpt("formatNamespace").hasArg().build(); + options.addOption(option); - if ("-slug".equals(args[i])) { - i++; - slug = args[i]; - } + option = Option.builder().longOpt("checksumError").hasArg().build(); + options.addOption(option); - if ("-onBehalfOf".equals(args[i])) { - i++; - onBehalfOf = args[i]; - } + option = Option.builder().longOpt("dest").hasArg().build(); + options.addOption(option); - if ("-formatNamespace".equals(args[i])) { - i++; - formatNamespace = args[i]; - } + DefaultParser parser = new DefaultParser(); + CommandLine command; + try { + command = parser.parse(options, args); + } catch (ParseException ex) { + log.error(ex.getMessage()); + return false; + } - if ("-checksumError".equals(args[i])) { - i++; - checksumError = true; + if (command.hasOption("help")) { + return false; // force the calling code to display the usage information. + } + md5 = command.hasOption("md5"); + noOp = command.hasOption("noOp"); + verbose = command.hasOption("verbose"); + if (command.hasOption("cmd")) { + guiMode = false; + } + if (command.hasOption("gui")) { + guiMode = true; + } + proxyHost = command.getOptionValue("host"); + if (command.hasOption("port")) { + proxyPort = Integer.parseInt(command.getOptionValue("port")); + } + username = command.getOptionValue("u"); + password = command.getOptionValue("p"); + href = command.getOptionValue("href"); + accessType = command.getOptionValue("t"); + filename = command.getOptionValue("file"); + filetype = command.getOptionValue("filetype"); + slug = command.getOptionValue("slug"); + onBehalfOf = command.getOptionValue("onBehalfOf"); + formatNamespace = command.getOptionValue("formatNamespace"); + checksumError = command.hasOption("checksumError"); + noCapture = command.hasOption("nocapture"); + if (command.hasOption("dest")) { + String dest = command.getOptionValue("dest"); + Matcher m = MULTI_PATTERN.matcher(dest); + if (!m.matches()) { + log.debug("Error with dest parameter. Ignoring value: {}", dest); + } else { + int numGroups = m.groupCount(); + for (int g = 0; g <= numGroups; g++) { + log.debug("Group ({}) is: {}", g, m.group(g)); } - if ("-dest".equals(args[i])) { - i++; - Matcher m = multiPattern.matcher(args[i]); - if (!m.matches()) { - log.debug("Error with dest parameter. Ignoring value: " + args[i]); - } else { - int numGroups = m.groupCount(); - for (int g = 0; g <= numGroups; g++) { - log.debug("Group (" + g + ") is: " + m.group(g)); - } - - String username = m.group(1); - String onBehalfOf = m.group(3); - String password = m.group(5); - String url = m.group(6); - PostDestination destination = new PostDestination(url, username, password, onBehalfOf); - - multiPost.add(destination); - } - } + String group_username = m.group(1); + String group_onBehalfOf = m.group(3); + String group_password = m.group(5); + String group_url = m.group(6); + PostDestination destination = new PostDestination(group_url, + group_username, group_password, group_onBehalfOf); - if ("-nocapture".equals(args[i])) { - i++; - noCapture = true; - } + multiPost.add(destination); } + } + try { // apply any settings if (href == null && "service".equals(accessType)) { log.error("No href specified."); return false; } - if (multiPost.size() == 0 && "multipost".equals(accessType)) { + if (multiPost.isEmpty() && "multipost".equals(accessType)) { log.error("No destinations specified"); return false; } From 134873c98ade6ab921e8cc9bad503620363d2933 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Wed, 25 Aug 2021 15:26:39 -0400 Subject: [PATCH 0011/1846] Replace array with Queue to avoid index-out-of-bounds, make neater. --- .../ctask/general/MetadataWebService.java | 94 ++++++++++--------- 1 file changed, 52 insertions(+), 42 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/ctask/general/MetadataWebService.java b/dspace-api/src/main/java/org/dspace/ctask/general/MetadataWebService.java index edeb2a6d0224..f7ab18c01e54 100644 --- a/dspace-api/src/main/java/org/dspace/ctask/general/MetadataWebService.java +++ b/dspace-api/src/main/java/org/dspace/ctask/general/MetadataWebService.java @@ -10,11 +10,13 @@ import java.io.IOException; import java.io.InputStream; import java.sql.SQLException; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Queue; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.XMLConstants; @@ -33,6 +35,7 @@ import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; @@ -60,18 +63,18 @@ * Intended use: cataloging tool in workflow and general curation. * The task uses a URL 'template' to compose the service call, e.g. * - * {@code http://www.sherpa.ac.uk/romeo/api29.php?issn=\{dc.identifier.issn\}} + *

{@code http://www.sherpa.ac.uk/romeo/api29.php?issn=\{dc.identifier.issn\}} * - * Task will substitute the value of the passed item's metadata field + *

Task will substitute the value of the passed item's metadata field * in the {parameter} position. If multiple values are present in the * item field, the first value is used. * - * The task uses another property (the datamap) to determine what data + *

The task uses another property (the datamap) to determine what data * to extract from the service response and how to use it, e.g. * - * {@code //publisher/name=>dc.publisher,//romeocolour} + *

{@code //publisher/name=>dc.publisher,//romeocolour} * - * Task will evaluate the left-hand side (or entire token) of each + *

Task will evaluate the left-hand side (or entire token) of each * comma-separated token in the property as an XPath 1.0 expression into * the response document, and if there is a mapping symbol (e.g. {@code '=>'}) and * value, it will assign the response document value(s) to the named @@ -79,48 +82,52 @@ * multiple values, they will all be assigned to the item field. The * mapping symbol governs the nature of metadata field assignment: * - * {@code '->'} mapping will add to any existing values in the item field - * {@code '=>'} mapping will replace any existing values in the item field - * {@code '~>'} mapping will add *only* if item field has no existing values + *

    + *
  • {@code '->'} mapping will add to any existing values in the item field
  • + *
  • {@code '=>'} mapping will replace any existing values in the item field
  • + *
  • {@code '~>'} mapping will add *only* if item field has no existing values
  • + *
* - * Unmapped data (without a mapping symbol) will simply be added to the task + *

Unmapped data (without a mapping symbol) will simply be added to the task * result string, prepended by the XPath expression (a little prettified). * Each label/value pair in the result string is separated by a space, * unless the optional 'separator' property is defined. * - * A very rudimentary facility for transformation of data is supported, e.g. + *

A very rudimentary facility for transformation of data is supported, e.g. * - * {@code http://www.crossref.org/openurl/?id=\{doi:dc.relation.isversionof\}&format=unixref} + *

{@code http://www.crossref.org/openurl/?id=\{doi:dc.relation.isversionof\}&format=unixref} * - * The 'doi:' prefix will cause the task to look for a 'transform' with that + *

The 'doi:' prefix will cause the task to look for a 'transform' with that * name, which is applied to the metadata value before parameter substitution * occurs. Transforms are defined in a task property such as the following: * - * {@code transform.doi = match 10. trunc 60} + *

{@code transform.doi = match 10. trunc 60} * - * This means exclude the value string up to the occurrence of '10.', then + *

This means exclude the value string up to the occurrence of '10.', then * truncate after 60 characters. The only transform functions currently defined: * - * {@code 'cut' } = remove number leading characters - * {@code 'trunc' } = remove trailing characters after number length - * {@code 'match' } = start match at pattern - * {@code 'text' } = append literal characters (enclose in ' ' when whitespace needed) + *

    + *
  • {@code 'cut' } = remove number leading characters
  • + *
  • {@code 'trunc' } = remove trailing characters after number length
  • + *
  • {@code 'match' } = start match at pattern
  • + *
  • {@code 'text' } = append literal characters (enclose in ' ' when whitespace needed)
  • + *
* - * If the transform results in an invalid state (e.g. cutting more characters + *

If the transform results in an invalid state (e.g. cutting more characters * than are in the value), the condition will be logged and the * un-transformed value used. * - * Transforms may also be used in datamaps, e.g. + *

Transforms may also be used in datamaps, e.g. * - * {@code //publisher/name=>shorten:dc.publisher,//romeocolour} + *

{@code //publisher/name=>shorten:dc.publisher,//romeocolour} * - * which would apply the 'shorten' transform to the service response value(s) + *

which would apply the 'shorten' transform to the service response value(s) * prior to metadata field assignment. * - * An optional property 'headers' may be defined to stipulate any HTTP headers + *

An optional property 'headers' may be defined to stipulate any HTTP headers * required in the service call. The property syntax is double-pipe separated headers: * - * {@code Accept: text/xml||Cache-Control: no-cache} + *

{@code Accept: text/xml||Cache-Control: no-cache} * * @author richardrodgers */ @@ -128,9 +135,9 @@ @Suspendable public class MetadataWebService extends AbstractCurationTask implements NamespaceContext { /** - * log4j category + * logging category */ - private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(MetadataWebService.class); + private static final Logger log = LogManager.getLogger(); // transform token parsing pattern protected Pattern ttPattern = Pattern.compile("\'([^\']*)\'|(\\S+)"); // URL of web service with template parameters @@ -360,42 +367,45 @@ protected String transform(String value, String transDef) { if (transDef == null) { return value; } - String[] tokens = tokenize(transDef); + Queue tokens = tokenize(transDef); String retValue = value; - for (int i = 0; i < tokens.length; i += 2) { - if ("cut".equals(tokens[i]) || "trunc".equals(tokens[i])) { - int index = Integer.parseInt(tokens[i + 1]); + while (!tokens.isEmpty()) { + String function = tokens.poll(); + if ("cut".equals(function) || "trunc".equals(function)) { + String argument = tokens.poll(); + int index = Integer.parseInt(argument); if (retValue.length() > index) { - if ("cut".equals(tokens[i])) { + if ("cut".equals(function)) { retValue = retValue.substring(index); } else { retValue = retValue.substring(0, index); } - } else if ("cut".equals(tokens[i])) { - log.error("requested cut: " + index + " exceeds value length"); + } else if ("cut".equals(function)) { + log.error("requested cut: {} exceeds value length", index); return value; } - } else if ("match".equals(tokens[i])) { - int index2 = retValue.indexOf(tokens[i + 1]); + } else if ("match".equals(function)) { + String argument = tokens.poll(); + int index2 = retValue.indexOf(argument); if (index2 > 0) { retValue = retValue.substring(index2); } else { - log.error("requested match: " + tokens[i + 1] + " failed"); + log.error("requested match: {} failed", argument); return value; } - } else if ("text".equals(tokens[i])) { - retValue = retValue + tokens[i + 1]; + } else if ("text".equals(function)) { + retValue = retValue + tokens.poll(); } else { - log.error(" unknown transform operation: " + tokens[i]); + log.error(" unknown transform operation: " + function); return value; } } return retValue; } - protected String[] tokenize(String text) { - List list = new ArrayList<>(); + protected Queue tokenize(String text) { Matcher m = ttPattern.matcher(text); + Queue list = new ArrayDeque<>(m.groupCount()); while (m.find()) { if (m.group(1) != null) { list.add(m.group(1)); @@ -403,7 +413,7 @@ protected String[] tokenize(String text) { list.add(m.group(2)); } } - return list.toArray(new String[0]); + return list; } protected int getMapIndex(String mapping) { From 674c3e26072ea996f1220d57eac2b6c022a1027c Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Mon, 30 Aug 2021 16:54:48 -0400 Subject: [PATCH 0012/1846] Remove unnecessary boxing, unread variables. --- .../org/dspace/browse/ItemListConfig.java | 24 ++--------- .../dspace/core/LegacyPluginServiceImpl.java | 42 ++++--------------- .../org/dspace/discovery/SolrServiceImpl.java | 10 +---- 3 files changed, 14 insertions(+), 62 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/browse/ItemListConfig.java b/dspace-api/src/main/java/org/dspace/browse/ItemListConfig.java index 9cbbe8f19429..6a63659c82b2 100644 --- a/dspace-api/src/main/java/org/dspace/browse/ItemListConfig.java +++ b/dspace-api/src/main/java/org/dspace/browse/ItemListConfig.java @@ -25,22 +25,7 @@ public class ItemListConfig { /** * a map of column number to metadata value */ - private Map metadata = new HashMap(); - - /** - * a map of column number to data type - */ - private Map types = new HashMap(); - - /** - * constant for a DATE column - */ - private static final int DATE = 1; - - /** - * constant for a TEXT column - */ - private static final int TEXT = 2; + private Map metadata = new HashMap<>(); private final transient ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); @@ -63,14 +48,11 @@ public ItemListConfig() // parse the config int i = 1; for (String token : browseFields) { - Integer key = Integer.valueOf(i); + Integer key = i; // find out if the field is a date if (token.indexOf("(date)") > 0) { token = token.replaceAll("\\(date\\)", ""); - types.put(key, Integer.valueOf(ItemListConfig.DATE)); - } else { - types.put(key, Integer.valueOf(ItemListConfig.TEXT)); } String[] mdBits = interpretField(token.trim(), null); @@ -100,7 +82,7 @@ public int numCols() { * @return array of metadata */ public String[] getMetadata(int col) { - return metadata.get(Integer.valueOf(col)); + return metadata.get(col); } /** diff --git a/dspace-api/src/main/java/org/dspace/core/LegacyPluginServiceImpl.java b/dspace-api/src/main/java/org/dspace/core/LegacyPluginServiceImpl.java index 7bbbd91d0aad..e92ea137f31f 100644 --- a/dspace-api/src/main/java/org/dspace/core/LegacyPluginServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/core/LegacyPluginServiceImpl.java @@ -10,7 +10,6 @@ import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; -import java.io.Serializable; import java.lang.reflect.Array; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; @@ -173,7 +172,7 @@ public Object[] getPluginSequence(Class interfaceClass) throws PluginInstantiationException { // cache of config data for Sequence Plugins; format its // -> [ .. ] (value is Array) - Map sequenceConfig = new HashMap(); + Map sequenceConfig = new HashMap<>(); // cache the configuration for this interface after grovelling it once: // format is prefix. = @@ -220,10 +219,7 @@ private Object getAnonymousPlugin(String classname) // Map of named plugin classes, [intfc,name] -> class // Also contains intfc -> "marker" to mark when interface has been loaded. - private Map namedPluginClasses = new HashMap(); - - // Map of cached (reusable) named plugin instances, [class,name] -> instance - private Map namedInstanceCache = new HashMap(); + private final Map namedPluginClasses = new HashMap<>(); // load and cache configuration data for the given interface. private void configureNamedPlugin(String iname) @@ -413,14 +409,14 @@ public String[] getAllPluginNames(Class interfaceClass) { String iname = interfaceClass.getName(); configureNamedPlugin(iname); String prefix = iname + SEP; - ArrayList result = new ArrayList(); + ArrayList result = new ArrayList<>(); for (String key : namedPluginClasses.keySet()) { if (key.startsWith(prefix)) { result.add(key.substring(prefix.length())); } } - if (result.size() == 0) { + if (result.isEmpty()) { log.error("Cannot find any names for named plugin, interface=" + iname); } @@ -508,10 +504,10 @@ public void checkConfiguration() */ // tables of config keys for each type of config line: - Map singleKey = new HashMap(); - Map sequenceKey = new HashMap(); - Map namedKey = new HashMap(); - Map selfnamedKey = new HashMap(); + Map singleKey = new HashMap<>(); + Map sequenceKey = new HashMap<>(); + Map namedKey = new HashMap<>(); + Map selfnamedKey = new HashMap<>(); // Find all property keys starting with "plugin." List keys = configurationService.getPropertyKeys("plugin."); @@ -533,7 +529,7 @@ public void checkConfiguration() // 2. Build up list of all interfaces and test that they are loadable. // don't bother testing that they are "interface" rather than "class" // since either one will work for the Plugin Manager. - ArrayList allInterfaces = new ArrayList(); + ArrayList allInterfaces = new ArrayList<>(); allInterfaces.addAll(singleKey.keySet()); allInterfaces.addAll(sequenceKey.keySet()); allInterfaces.addAll(namedKey.keySet()); @@ -547,7 +543,6 @@ public void checkConfiguration() // - each class is loadable. // - plugin.selfnamed values are each subclass of SelfNamedPlugin // - save classname in allImpls - Map allImpls = new HashMap(); // single plugins - just check that it has a valid impl. class ii = singleKey.keySet().iterator(); @@ -558,9 +553,6 @@ public void checkConfiguration() log.error("Single plugin config not found for: " + SINGLE_PREFIX + key); } else { val = val.trim(); - if (checkClassname(val, "implementation class")) { - allImpls.put(val, val); - } } } @@ -571,12 +563,6 @@ public void checkConfiguration() String[] vals = configurationService.getArrayProperty(SEQUENCE_PREFIX + key); if (vals == null || vals.length == 0) { log.error("Sequence plugin config not found for: " + SEQUENCE_PREFIX + key); - } else { - for (String val : vals) { - if (checkClassname(val, "implementation class")) { - allImpls.put(val, val); - } - } } } @@ -591,7 +577,6 @@ public void checkConfiguration() } else { for (String val : vals) { if (checkClassname(val, "selfnamed implementation class")) { - allImpls.put(val, val); checkSelfNamed(val); } } @@ -609,15 +594,6 @@ public void checkConfiguration() log.error("Named plugin config not found for: " + NAMED_PREFIX + key); } else { checkNames(key); - for (String val : vals) { - // each named plugin has two parts to the value, format: - // [classname] = [plugin-name] - String val_split[] = val.split("\\s*=\\s*"); - String classname = val_split[0]; - if (checkClassname(classname, "implementation class")) { - allImpls.put(classname, classname); - } - } } } } diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java index 0791824085d7..ec0c18bacea6 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java @@ -873,7 +873,7 @@ protected DiscoverResult retrieveResult(Context context, DiscoverQuery query) // if we found stale objects we can decide to skip execution of the remaining code to improve performance boolean skipLoadingResponse = false; // use zombieDocs to collect stale found objects - List zombieDocs = new ArrayList(); + List zombieDocs = new ArrayList<>(); QueryResponse solrQueryResponse = solrSearchCore.getSolr().query(solrQuery, solrSearchCore.REQUEST_METHOD); if (solrQueryResponse != null) { @@ -924,12 +924,6 @@ protected DiscoverResult retrieveResult(Context context, DiscoverQuery query) //We need to remove all the "_hl" appendix strings from our keys Map> resultMap = new HashMap<>(); for (String key : highlightedFields.keySet()) { - List highlightOriginalValue = highlightedFields.get(key); - List resultHighlightOriginalValue = new ArrayList<>(); - for (String highlightValue : highlightOriginalValue) { - String[] splitted = highlightValue.split("###"); - resultHighlightOriginalValue.add(splitted); - } resultMap.put(key.substring(0, key.lastIndexOf("_hl")), highlightedFields.get(key)); } @@ -945,7 +939,7 @@ protected DiscoverResult retrieveResult(Context context, DiscoverQuery query) // If any stale entries are found in the current page of results, // we remove those stale entries and rerun the same query again. // Otherwise, the query is valid and the results are returned. - if (zombieDocs.size() != 0) { + if (!zombieDocs.isEmpty()) { log.info("Cleaning " + zombieDocs.size() + " stale objects from Discovery Index"); log.info("ZombieDocs "); zombieDocs.forEach(log::info); From 90a23a000a2c9de696b0cdba36621268f5a96e0d Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Mon, 30 Aug 2021 16:56:09 -0400 Subject: [PATCH 0013/1846] Fix mismatched getter, setter synchronization. --- .../servicemanager/config/DSpaceConfigurationService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dspace-services/src/main/java/org/dspace/servicemanager/config/DSpaceConfigurationService.java b/dspace-services/src/main/java/org/dspace/servicemanager/config/DSpaceConfigurationService.java index 8bf037cbe32e..c20ee1962868 100644 --- a/dspace-services/src/main/java/org/dspace/servicemanager/config/DSpaceConfigurationService.java +++ b/dspace-services/src/main/java/org/dspace/servicemanager/config/DSpaceConfigurationService.java @@ -177,7 +177,7 @@ public Object getPropertyValue(String name) { * @see org.dspace.services.ConfigurationService#getProperty(java.lang.String) */ @Override - public String getProperty(String name) { + public synchronized String getProperty(String name) { return getProperty(name, null); } @@ -188,7 +188,7 @@ public String getProperty(String name) { * @see org.dspace.services.ConfigurationService#getProperty(java.lang.String, java.lang.String) */ @Override - public String getProperty(String name, String defaultValue) { + public synchronized String getProperty(String name, String defaultValue) { return getPropertyAsType(name, defaultValue); } @@ -410,7 +410,7 @@ public String[] loadConfiguration(Map properties) { throw new IllegalArgumentException("properties cannot be null"); } - ArrayList changed = new ArrayList(); + ArrayList changed = new ArrayList<>(); // loop through each new property entry for (Entry entry : properties.entrySet()) { From 0b95376e5d66c641c7ab2369beb8ef0cc5df335c Mon Sep 17 00:00:00 2001 From: Nathan Buckingham Date: Wed, 8 Dec 2021 16:47:41 -0500 Subject: [PATCH 0014/1846] w2p-85808 S3Bitstore uses TransferManager to allow large file transfer --- .../storage/bitstore/S3BitStoreService.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java b/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java index ce2b3b3f05a9..216a34fffdb2 100644 --- a/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java +++ b/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java @@ -22,9 +22,11 @@ import com.amazonaws.services.s3.model.AmazonS3Exception; import com.amazonaws.services.s3.model.GetObjectRequest; import com.amazonaws.services.s3.model.ObjectMetadata; -import com.amazonaws.services.s3.model.PutObjectRequest; -import com.amazonaws.services.s3.model.PutObjectResult; import com.amazonaws.services.s3.model.S3Object; +import com.amazonaws.services.s3.transfer.TransferManager; +import com.amazonaws.services.s3.transfer.TransferManagerBuilder; +import com.amazonaws.services.s3.transfer.Upload; +import com.amazonaws.services.s3.transfer.model.UploadResult; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpStatus; @@ -180,16 +182,20 @@ public void put(Bitstream bitstream, InputStream in) throws IOException { FileUtils.copyInputStreamToFile(in, scratchFile); long contentLength = scratchFile.length(); - PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, key, scratchFile); - PutObjectResult putObjectResult = s3Service.putObject(putObjectRequest); + TransferManager tm = TransferManagerBuilder.standard() + .withS3Client(s3Service) + .build(); + + Upload upload = tm.upload(bucketName, key, scratchFile); + UploadResult result = upload.waitForUploadResult(); bitstream.setSizeBytes(contentLength); - bitstream.setChecksum(putObjectResult.getETag()); + bitstream.setChecksum(result.getETag()); bitstream.setChecksumAlgorithm(CSA); scratchFile.delete(); - } catch (AmazonClientException | IOException e) { + } catch (AmazonClientException | IOException | InterruptedException e) { log.error("put(" + bitstream.getInternalId() + ", is)", e); throw new IOException(e); } finally { From 6c5e568543fa5d260f4dd2e6034ecc360af8bf43 Mon Sep 17 00:00:00 2001 From: Nathan Buckingham Date: Mon, 13 Dec 2021 16:18:16 -0500 Subject: [PATCH 0015/1846] S3 calculated md5 and store bitstream md5 on Multipart upload --- .../dspace/storage/bitstore/S3BitStoreService.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java b/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java index 216a34fffdb2..361c800dbdea 100644 --- a/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java +++ b/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java @@ -181,16 +181,20 @@ public void put(Bitstream bitstream, InputStream in) throws IOException { try { FileUtils.copyInputStreamToFile(in, scratchFile); long contentLength = scratchFile.length(); + String localChecksum = org.dspace.curate.Utils.checksum(scratchFile, "MD5"); TransferManager tm = TransferManagerBuilder.standard() + .withAlwaysCalculateMultipartMd5(true) .withS3Client(s3Service) .build(); - + + Upload upload = tm.upload(bucketName, key, scratchFile); - UploadResult result = upload.waitForUploadResult(); + + upload.waitForUploadResult(); bitstream.setSizeBytes(contentLength); - bitstream.setChecksum(result.getETag()); + bitstream.setChecksum(localChecksum); bitstream.setChecksumAlgorithm(CSA); scratchFile.delete(); @@ -199,8 +203,8 @@ public void put(Bitstream bitstream, InputStream in) throws IOException { log.error("put(" + bitstream.getInternalId() + ", is)", e); throw new IOException(e); } finally { - if (scratchFile.exists()) { - scratchFile.delete(); + if (!scratchFile.delete()) { + scratchFile.deleteOnExit(); } } } From 2992660267f8462eec63557e023b7a485cb2fb74 Mon Sep 17 00:00:00 2001 From: Nathan Buckingham Date: Mon, 13 Dec 2021 16:35:40 -0500 Subject: [PATCH 0016/1846] Add comment explaining ETag --- .../java/org/dspace/storage/bitstore/S3BitStoreService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java b/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java index 361c800dbdea..5af4603aadb7 100644 --- a/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java +++ b/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java @@ -181,6 +181,8 @@ public void put(Bitstream bitstream, InputStream in) throws IOException { try { FileUtils.copyInputStreamToFile(in, scratchFile); long contentLength = scratchFile.length(); + // The ETag may or may not be and MD5 digest of the object data. + // Therefore, we precalculate before uploading String localChecksum = org.dspace.curate.Utils.checksum(scratchFile, "MD5"); TransferManager tm = TransferManagerBuilder.standard() From b7fa187589a1474acdcb9aab0746421b6bf23e97 Mon Sep 17 00:00:00 2001 From: Nathan Buckingham Date: Tue, 14 Dec 2021 09:28:42 -0500 Subject: [PATCH 0017/1846] fix up white space and unsused import --- .../org/dspace/storage/bitstore/S3BitStoreService.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java b/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java index 5af4603aadb7..9a363947033a 100644 --- a/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java +++ b/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java @@ -26,7 +26,6 @@ import com.amazonaws.services.s3.transfer.TransferManager; import com.amazonaws.services.s3.transfer.TransferManagerBuilder; import com.amazonaws.services.s3.transfer.Upload; -import com.amazonaws.services.s3.transfer.model.UploadResult; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpStatus; @@ -189,10 +188,10 @@ public void put(Bitstream bitstream, InputStream in) throws IOException { .withAlwaysCalculateMultipartMd5(true) .withS3Client(s3Service) .build(); - - + + Upload upload = tm.upload(bucketName, key, scratchFile); - + upload.waitForUploadResult(); bitstream.setSizeBytes(contentLength); From 5ae5398b99220e8cf96cc59a77a22b4151433284 Mon Sep 17 00:00:00 2001 From: Nathan Buckingham Date: Wed, 15 Dec 2021 14:19:24 -0500 Subject: [PATCH 0018/1846] Change to use constant instead of string md5 --- .../java/org/dspace/storage/bitstore/S3BitStoreService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java b/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java index 9a363947033a..d1a292d97907 100644 --- a/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java +++ b/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java @@ -182,7 +182,7 @@ public void put(Bitstream bitstream, InputStream in) throws IOException { long contentLength = scratchFile.length(); // The ETag may or may not be and MD5 digest of the object data. // Therefore, we precalculate before uploading - String localChecksum = org.dspace.curate.Utils.checksum(scratchFile, "MD5"); + String localChecksum = org.dspace.curate.Utils.checksum(scratchFile, CSA); TransferManager tm = TransferManagerBuilder.standard() .withAlwaysCalculateMultipartMd5(true) From 9664296af6da68fad84aa1f318fbf7caf30a3a47 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Tue, 1 Feb 2022 14:48:51 +0100 Subject: [PATCH 0019/1846] 86201: Fix RelationshipService place handling Correctly take into account the place of other Relationships and/or MDVs when creating/modifying/deleting Relationships Simplify RelationshipService public API to avoid having to call updatePlaceInRelationship explicitly Additional tests to cover issues with the previous implementation --- .../dspace/app/bulkedit/MetadataImport.java | 9 +- .../content/DSpaceObjectServiceImpl.java | 22 +- .../content/RelationshipServiceImpl.java | 431 ++- .../dspace/content/dao/RelationshipDAO.java | 22 - .../content/dao/impl/RelationshipDAOImpl.java | 32 - .../content/service/RelationshipService.java | 64 +- .../dspace/builder/RelationshipBuilder.java | 3 +- .../RelationshipMetadataServiceIT.java | 43 - .../RelationshipServiceImplPlaceTest.java | 2363 ++++++++++++++++- .../content/RelationshipServiceImplTest.java | 26 - .../content/dao/RelationshipDAOImplTest.java | 22 - .../content/service/ItemServiceTest.java | 202 ++ .../RelationshipRestRepository.java | 41 +- .../rest/RelationshipRestRepositoryIT.java | 178 ++ 14 files changed, 3178 insertions(+), 280 deletions(-) create mode 100644 dspace-api/src/test/java/org/dspace/content/service/ItemServiceTest.java diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java index 0db0cc45be19..6b0330fc5a7f 100644 --- a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java +++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java @@ -924,11 +924,10 @@ private void addRelationship(Context c, Item item, String typeName, String value rightItem = item; } - // Create the relationship - int leftPlace = relationshipService.findNextLeftPlaceByLeftItem(c, leftItem); - int rightPlace = relationshipService.findNextRightPlaceByRightItem(c, rightItem); - Relationship persistedRelationship = relationshipService.create(c, leftItem, rightItem, - foundRelationshipType, leftPlace, rightPlace); + // Create the relationship, appending to the end + Relationship persistedRelationship = relationshipService.create( + c, leftItem, rightItem, foundRelationshipType, -1, -1 + ); relationshipService.update(c, persistedRelationship); } diff --git a/dspace-api/src/main/java/org/dspace/content/DSpaceObjectServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/DSpaceObjectServiceImpl.java index c34291c3dd99..23f2edf44624 100644 --- a/dspace-api/src/main/java/org/dspace/content/DSpaceObjectServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/DSpaceObjectServiceImpl.java @@ -636,8 +636,14 @@ public int compare(MetadataValue o1, MetadataValue o2) { }); for (MetadataValue metadataValue : metadataValues) { //Retrieve & store the place for each metadata value - if (StringUtils.startsWith(metadataValue.getAuthority(), Constants.VIRTUAL_AUTHORITY_PREFIX) && - ((RelationshipMetadataValue) metadataValue).isUseForPlace()) { + if ( + // For virtual MDVs with useForPlace=true, + // update both the place of the metadatum and the place of the Relationship. + // E.g. for an Author relationship, + // the place should be updated using the same principle as dc.contributor.author. + StringUtils.startsWith(metadataValue.getAuthority(), Constants.VIRTUAL_AUTHORITY_PREFIX) + && ((RelationshipMetadataValue) metadataValue).isUseForPlace() + ) { int mvPlace = getMetadataValuePlace(fieldToLastPlace, metadataValue); metadataValue.setPlace(mvPlace); String authority = metadataValue.getAuthority(); @@ -650,8 +656,16 @@ public int compare(MetadataValue o1, MetadataValue o2) { } relationshipService.update(context, relationship); - } else if (!StringUtils.startsWith(metadataValue.getAuthority(), - Constants.VIRTUAL_AUTHORITY_PREFIX)) { + } else if ( + // Otherwise, just set the place of the metadatum + // ...unless the metadatum in question is a relation.* metadatum. + // This case is a leftover from when a Relationship is removed and copied to metadata. + // If we let its place change the order of any remaining Relationships will be affected. + // todo: this makes it so these leftover MDVs can't be reordered later on + !StringUtils.equals( + metadataValue.getMetadataField().getMetadataSchema().getName(), "relation" + ) + ) { int mvPlace = getMetadataValuePlace(fieldToLastPlace, metadataValue); metadataValue.setPlace(mvPlace); } diff --git a/dspace-api/src/main/java/org/dspace/content/RelationshipServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/RelationshipServiceImpl.java index 1b419da81631..9b5b6d0b5217 100644 --- a/dspace-api/src/main/java/org/dspace/content/RelationshipServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/RelationshipServiceImpl.java @@ -8,10 +8,13 @@ package org.dspace.content; import java.sql.SQLException; +import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; @@ -24,6 +27,7 @@ import org.dspace.content.service.ItemService; import org.dspace.content.service.RelationshipService; import org.dspace.content.service.RelationshipTypeService; +import org.dspace.content.virtual.VirtualMetadataConfiguration; import org.dspace.content.virtual.VirtualMetadataPopulator; import org.dspace.core.Constants; import org.dspace.core.Context; @@ -97,7 +101,7 @@ public Relationship create(Context context, Relationship relationship) throws SQ // This order of execution should be handled in the creation (create, updateplace, update relationship) // for a proper place allocation Relationship relationshipToReturn = relationshipDAO.create(context, relationship); - updatePlaceInRelationship(context, relationshipToReturn); + updatePlaceInRelationship(context, relationshipToReturn, null, null, true, true); update(context, relationshipToReturn); updateItemsInRelationship(context, relationship); return relationshipToReturn; @@ -112,71 +116,364 @@ public Relationship create(Context context, Relationship relationship) throws SQ } @Override - public void updatePlaceInRelationship(Context context, Relationship relationship) - throws SQLException, AuthorizeException { - Item leftItem = relationship.getLeftItem(); - // Max value is used to ensure that these will get added to the back of the list and thus receive the highest - // (last) place as it's set to a -1 for creation - if (relationship.getLeftPlace() == -1) { - relationship.setLeftPlace(Integer.MAX_VALUE); + public Relationship move( + Context context, Relationship relationship, Integer newLeftPlace, Integer newRightPlace + ) throws SQLException, AuthorizeException { + if (authorizeService.authorizeActionBoolean(context, relationship.getLeftItem(), Constants.WRITE) || + authorizeService.authorizeActionBoolean(context, relationship.getRightItem(), Constants.WRITE)) { + + // Don't do anything if neither the leftPlace nor rightPlace was updated + if (newLeftPlace != null || newRightPlace != null) { + // This order of execution should be handled in the creation (create, updateplace, update relationship) + // for a proper place allocation + updatePlaceInRelationship(context, relationship, newLeftPlace, newRightPlace, false, false); + update(context, relationship); + updateItemsInRelationship(context, relationship); + } + + return relationship; + } else { + throw new AuthorizeException( + "You do not have write rights on this relationship's items"); } + } + + @Override + public Relationship move( + Context context, Relationship relationship, Item newLeftItem, Item newRightItem + ) throws SQLException, AuthorizeException { + // If the new Item is the same as the current Item, don't move + newLeftItem = newLeftItem != relationship.getLeftItem() ? newLeftItem : null; + newRightItem = newRightItem != relationship.getRightItem() ? newRightItem : null; + + // Don't do anything if neither the leftItem nor rightItem was updated + if (newLeftItem != null || newRightItem != null) { + // First move the Relationship to the back within the current Item's lists + // This ensures that we won't have any gaps once we move the Relationship to a different Item + move( + context, relationship, + newLeftItem != null ? -1 : null, + newRightItem != null ? -1 : null + ); + + boolean insertLeft = false; + boolean insertRight = false; + + // If Item has been changed, mark the previous Item as modified to make sure we discard the old relation.* + // metadata on the next update. + // Set the Relationship's Items to the new ones, appending to the end + if (newLeftItem != null) { + relationship.getLeftItem().setMetadataModified(); + relationship.setLeftItem(newLeftItem); + relationship.setLeftPlace(-1); + insertLeft = true; + } + if (newRightItem != null) { + relationship.getRightItem().setMetadataModified(); + relationship.setRightItem(newRightItem); + relationship.setRightPlace(-1); + insertRight = true; + } + + // This order of execution should be handled in the creation (create, updateplace, update relationship) + // for a proper place allocation + updatePlaceInRelationship(context, relationship, null, null, insertLeft, insertRight); + update(context, relationship); + updateItemsInRelationship(context, relationship); + } + return relationship; + } + + /** + * This method will update the place for the Relationship and all other relationships found by the items and + * relationship type of the given Relationship. + * + * @param context The relevant DSpace context + * @param relationship The Relationship object that will have its place updated and that will be used + * to retrieve the other relationships whose place might need to be updated. + * @param newLeftPlace If the Relationship in question is to be moved, the leftPlace it is to be moved to. + * Set this to null if the Relationship has not been moved, i.e. it has just been created, + * deleted or when its Items have been modified. + * @param newRightPlace If the Relationship in question is to be moved, the rightPlace it is to be moved to. + * Set this to null if the Relationship has not been moved, i.e. it has just been created, + * deleted or when its Items have been modified. + * @param insertLeft Whether the Relationship in question should be inserted into the left Item. + * Should be set to true when creating or moving to a different Item. + * @param insertRight Whether the Relationship in question should be inserted into the right Item. + * Should be set to true when creating or moving to a different Item. + * @throws SQLException If something goes wrong + * @throws AuthorizeException + * If the user is not authorized to update the Relationship or its Items + */ + private void updatePlaceInRelationship( + Context context, Relationship relationship, + Integer newLeftPlace, Integer newRightPlace, boolean insertLeft, boolean insertRight + ) throws SQLException, AuthorizeException { + Item leftItem = relationship.getLeftItem(); Item rightItem = relationship.getRightItem(); - if (relationship.getRightPlace() == -1) { - relationship.setRightPlace(Integer.MAX_VALUE); - } - List leftRelationships = findByItemAndRelationshipType(context, - leftItem, - relationship.getRelationshipType(), true); - List rightRelationships = findByItemAndRelationshipType(context, - rightItem, - relationship.getRelationshipType(), - false); - - // These relationships are only deleted from the temporary lists incase they're present in them so that we can + + List leftRelationships = findByItemAndRelationshipType( + context, leftItem, relationship.getRelationshipType(), true + ); + List rightRelationships = findByItemAndRelationshipType( + context, rightItem, relationship.getRelationshipType(), false + ); + + // These relationships are only deleted from the temporary lists in case they're present in them so that we can // properly perform our place calculation later down the line in this method. - if (leftRelationships.contains(relationship)) { - leftRelationships.remove(relationship); - } - if (rightRelationships.contains(relationship)) { - rightRelationships.remove(relationship); - } + boolean deletedFromLeft = !leftRelationships.contains(relationship); + boolean deletedFromRight = !rightRelationships.contains(relationship); + leftRelationships.remove(relationship); + rightRelationships.remove(relationship); + + List leftMetadata = getSiblingMetadata(leftItem, relationship, true); + List rightMetadata = getSiblingMetadata(rightItem, relationship, false); + + // For new relationships added to the end, this will be -1. + // For new relationships added at a specific position, this will contain that position. + // For existing relationships, this will contain the place before it was moved. + // For deleted relationships, this will contain the place before it was deleted. + int oldLeftPlace = relationship.getLeftPlace(); + int oldRightPlace = relationship.getRightPlace(); + + boolean movedUpLeft = resolveRelationshipPlace( + relationship, true, leftRelationships, leftMetadata, oldLeftPlace, newLeftPlace + ); + boolean movedUpRight = resolveRelationshipPlace( + relationship, false, rightRelationships, rightMetadata, oldRightPlace, newRightPlace + ); + context.turnOffAuthorisationSystem(); - //If useForPlace for the leftwardType is false for the relationshipType, - // we need to sort the relationships here based on leftplace. - if (!virtualMetadataPopulator.isUseForPlaceTrueForRelationshipType(relationship.getRelationshipType(), true)) { - if (!leftRelationships.isEmpty()) { - leftRelationships.sort(Comparator.comparingInt(Relationship::getLeftPlace)); - for (int i = 0; i < leftRelationships.size(); i++) { - leftRelationships.get(i).setLeftPlace(i); - } - relationship.setLeftPlace(leftRelationships.size()); + + shiftSiblings( + relationship, true, oldLeftPlace, movedUpLeft, insertLeft, deletedFromLeft, + leftRelationships, leftMetadata + ); + shiftSiblings( + relationship, false, oldRightPlace, movedUpRight, insertRight, deletedFromRight, + rightRelationships, rightMetadata + ); + + updateItem(context, leftItem); + updateItem(context, rightItem); + + context.restoreAuthSystemState(); + } + + /** + * Return the MDVs in the Item's MDF corresponding to the given Relationship. + * Return an empty list if the Relationship isn't mapped to any MDF + * or if the mapping is configured with useForPlace=false. + * + * This returns actual metadata (not virtual) which in the same metadata field as the useForPlace. + * For a publication with 2 author relationships and 3 plain text dc.contributor.author values, + * it would return the 3 plain text dc.contributor.author values. + * For a person related to publications, it would return an empty list. + */ + private List getSiblingMetadata( + Item item, Relationship relationship, boolean isLeft + ) { + List metadata = new ArrayList<>(); + if (virtualMetadataPopulator.isUseForPlaceTrueForRelationshipType(relationship.getRelationshipType(), isLeft)) { + HashMap mapping; + if (isLeft) { + mapping = virtualMetadataPopulator.getMap().get(relationship.getRelationshipType().getLeftwardType()); } else { - relationship.setLeftPlace(0); + mapping = virtualMetadataPopulator.getMap().get(relationship.getRelationshipType().getRightwardType()); + } + if (mapping != null) { + for (String mdf : mapping.keySet()) { + metadata.addAll( + // Make sure we're only looking at database MDVs; if the relationship currently overlaps + // one of these, its virtual MDV will overwrite the database MDV in itemService.getMetadata() + // The relationship pass should be sufficient to move any sibling virtual MDVs. + item.getMetadata() + .stream() + .filter(mdv -> mdv.getMetadataField().toString().equals(mdf.replace(".", "_"))) + .collect(Collectors.toList()) + ); + } } - } else { - updateItem(context, leftItem); - } + return metadata; + } - //If useForPlace for the rightwardType is false for the relationshipType, - // we need to sort the relationships here based on the rightplace. - if (!virtualMetadataPopulator.isUseForPlaceTrueForRelationshipType(relationship.getRelationshipType(), false)) { - if (!rightRelationships.isEmpty()) { - rightRelationships.sort(Comparator.comparingInt(Relationship::getRightPlace)); - for (int i = 0; i < rightRelationships.size(); i++) { - rightRelationships.get(i).setRightPlace(i); + /** + * Set the left/right place of a Relationship + * - To a new place in case it's being moved + * - Resolve -1 to the actual last place based on the places of its sibling Relationships and/or MDVs + * and determine if it has been moved up in the list. + * + * Examples: + * - Insert a Relationship at place 3 + * newPlace starts out as null and is not updated. Return movedUp=false + * - Insert a Relationship at place -1 + * newPlace starts out as null and is resolved to e.g. 6. Update the Relationship and return movedUp=false + * - Move a Relationship from place 4 to 2 + * Update the Relationship and return movedUp=false. + * - Move a Relationship from place 2 to -1 + * newPlace starts out as -1 and is resolved to e.g. 5. Update the relationship and return movedUp=true. + * - Remove a relationship from place 1 + * Return movedUp=false + * + * @param relationship the Relationship that's being updated + * @param isLeft whether to consider the left side of the Relationship. + * This method should be called twice, once with isLeft=true and once with isLeft=false. + * Make sure this matches the provided relationships/metadata/oldPlace/newPlace. + * @param relationships the list of sibling Relationships + * @param metadata the list of sibling MDVs + * @param oldPlace the previous place for this Relationship, in case it has been moved. + * Otherwise, the current place of a deleted Relationship + * or the place a Relationship has been inserted. + * @param newPlace The new place for this Relationship. Will be null on insert/delete. + * @return true if the Relationship was moved and newPlace > oldPlace + */ + private boolean resolveRelationshipPlace( + Relationship relationship, boolean isLeft, + List relationships, List metadata, + int oldPlace, Integer newPlace + ) { + boolean movedUp = false; + + if (newPlace != null) { + // We're moving an existing Relationship... + if (newPlace == -1) { + // ...to the end of the list + int nextPlace = getNextPlace(relationships, metadata, isLeft); + if (nextPlace == oldPlace) { + // If this Relationship is already at the end, do nothing. + newPlace = oldPlace; + } else { + // Subtract 1 from the next place since we're moving, not inserting and + // the total number of Relationships stays the same. + newPlace = nextPlace - 1; } - relationship.setRightPlace(rightRelationships.size()); - } else { - relationship.setRightPlace(0); } + if (newPlace > oldPlace) { + // ...up the list. We have to keep track of this in order to shift correctly later on + movedUp = true; + } + } else if (oldPlace == -1) { + // We're _not_ moving an existing Relationship. The newPlace is already set in the Relationship object. + // We only need to resolve it to the end of the list if it's set to -1, otherwise we can just keep it as is. + newPlace = getNextPlace(relationships, metadata, isLeft); + } - } else { - updateItem(context, rightItem); + if (newPlace != null) { + setPlace(relationship, isLeft, newPlace); + } + + return movedUp; + } + + /** + * Return the index of the next place in a list of Relationships and Metadata. + * By not relying on the size of both lists we can support one-to-many virtual MDV mappings. + * @param isLeft whether to take the left or right place of each Relationship + */ + private int getNextPlace(List relationships, List metadata, boolean isLeft) { + return Stream.concat( + metadata.stream().map(MetadataValue::getPlace), + relationships.stream().map(r -> getPlace(r, isLeft)) + ).max(Integer::compare) + .map(integer -> integer + 1) + .orElse(0); + } + /** + * Adjust the left/right place of sibling Relationships and MDVs + * + * Examples: with sibling Relationships R,S,T and metadata a,b,c + * - Insert T at place 1 aRbSc -> a T RbSc + * Shift all siblings with place >= 1 one place to the right + * - Delete R from place 2 aT R bSc -> aTbSc + * Shift all siblings with place > 2 one place to the left + * - Move S from place 3 to place 2 (movedUp=false) aTb S c -> aT S bc + * Shift all siblings with 2 < place <= 3 one place to the right + * - Move T from place 1 to place 3 (movedUp=true) a T Sbc -> aSb T c + * Shift all siblings with 1 < place <= 3 one place to the left + * + * @param relationship the Relationship that's being updated + * @param isLeft whether to consider the left side of the Relationship. + * This method should be called twice, once with isLeft=true and once with isLeft=false. + * Make sure this matches the provided relationships/metadata/oldPlace/newPlace. + * @param oldPlace the previous place for this Relationship, in case it has been moved. + * Otherwise, the current place of a deleted Relationship + * or the place a Relationship has been inserted. + * @param movedUp if this Relationship has been moved up the list, e.g. from place 2 to place 4 + * @param deleted whether this Relationship has been deleted + * @param relationships the list of sibling Relationships + * @param metadata the list of sibling MDVs + */ + private void shiftSiblings( + Relationship relationship, boolean isLeft, int oldPlace, boolean movedUp, boolean inserted, boolean deleted, + List relationships, List metadata + ) { + int newPlace = getPlace(relationship, isLeft); + + for (Relationship sibling : relationships) { + int siblingPlace = getPlace(sibling, isLeft); + if ( + (deleted && siblingPlace > newPlace) + // If the relationship was deleted, all relationships after it should shift left + // We must make the distinction between deletes and moves because for inserts oldPlace == newPlace + || (movedUp && siblingPlace <= newPlace && siblingPlace > oldPlace) + // If the relationship was moved up e.g. from place 2 to 5, all relationships + // with place > 2 (the old place) and <= to 5 should shift left + ) { + setPlace(sibling, isLeft, siblingPlace - 1); + } else if ( + (inserted && siblingPlace >= newPlace) + // If the relationship was inserted, all relationships starting from that place should shift right + // We must make the distinction between inserts and moves because for inserts oldPlace == newPlace + || (!movedUp && siblingPlace >= newPlace && siblingPlace < oldPlace) + // If the relationship was moved down e.g. from place 5 to 2, all relationships + // with place >= 2 and < 5 (the old place) should shift right + ) { + setPlace(sibling, isLeft, siblingPlace + 1); + } } - context.restoreAuthSystemState(); + for (MetadataValue mdv : metadata) { + int mdvPlace = mdv.getPlace(); + if ( + (deleted && mdvPlace > newPlace) + // If the relationship was deleted, all metadata after it should shift left + // We must make the distinction between deletes and moves because for inserts oldPlace == newPlace + // If the reltionship was copied to metadata on deletion: + // - the plain text can be after the relationship (in which case it's moved forward again) + // - or before the relationship (in which case it remains in place) + || (movedUp && mdvPlace <= newPlace && mdvPlace > oldPlace) + // If the relationship was moved up e.g. from place 2 to 5, all metadata + // with place > 2 (the old place) and <= to 5 should shift left + ) { + mdv.setPlace(mdvPlace - 1); + } else if ( + (inserted && mdvPlace >= newPlace) + // If the relationship was inserted, all relationships starting from that place should shift right + // We must make the distinction between inserts and moves because for inserts oldPlace == newPlace + || (!movedUp && mdvPlace >= newPlace && mdvPlace < oldPlace) + // If the relationship was moved down e.g. from place 5 to 2, all relationships + // with place >= 2 and < 5 (the old place) should shift right + ) { + mdv.setPlace(mdvPlace + 1); + } + } + } + + private int getPlace(Relationship relationship, boolean isLeft) { + if (isLeft) { + return relationship.getLeftPlace(); + } else { + return relationship.getRightPlace(); + } + } + private void setPlace(Relationship relationship, boolean isLeft, int place) { + if (isLeft) { + relationship.setLeftPlace(place); + } else { + relationship.setRightPlace(place); + } } @Override @@ -186,16 +483,6 @@ public void updateItem(Context context, Item relatedItem) itemService.update(context, relatedItem); } - @Override - public int findNextLeftPlaceByLeftItem(Context context, Item item) throws SQLException { - return relationshipDAO.findNextLeftPlaceByLeftItem(context, item); - } - - @Override - public int findNextRightPlaceByRightItem(Context context, Item item) throws SQLException { - return relationshipDAO.findNextRightPlaceByRightItem(context, item); - } - private boolean isRelationshipValidToCreate(Context context, Relationship relationship) throws SQLException { RelationshipType relationshipType = relationship.getRelationshipType(); @@ -375,7 +662,7 @@ private void deleteRelationshipAndCopyToItem(Context context, Relationship relat if (authorizeService.authorizeActionBoolean(context, relationship.getLeftItem(), Constants.WRITE) || authorizeService.authorizeActionBoolean(context, relationship.getRightItem(), Constants.WRITE)) { relationshipDAO.delete(context, relationship); - updatePlaceInRelationship(context, relationship); + updatePlaceInRelationship(context, relationship, null, null, false, false); updateItemsInRelationship(context, relationship); } else { throw new AuthorizeException( @@ -508,6 +795,9 @@ private boolean containsVirtualMetadata(String typeToSearchInVirtualMetadata) { /** * Converts virtual metadata from RelationshipMetadataValue objects to actual item metadata. + * The resulting MDVs are added in front or behind the Relationship's virtual MDVs. + * The Relationship's virtual MDVs may be shifted right, and all subsequent metadata will be shifted right. + * So this method ensures the places are still valid. * * @param context The relevant DSpace context * @param relationship The relationship containing the left and right items @@ -524,7 +814,15 @@ private void copyMetadataValues(Context context, Relationship relationship, bool relationshipMetadataService.findRelationshipMetadataValueForItemRelationship(context, relationship.getLeftItem(), entityTypeString, relationship, true); for (RelationshipMetadataValue relationshipMetadataValue : relationshipMetadataValues) { - itemService.addAndShiftRightMetadata(context, relationship.getLeftItem(), + // This adds the plain text metadata values on the same spot as the virtual values. + // This will be overruled in org.dspace.content.DSpaceObjectServiceImpl.update + // in the line below but it's not important whether the plain text or virtual values end up on top. + // The virtual values will eventually be deleted, and the others shifted + // This is required because addAndShiftRightMetadata has issues on metadata fields containing + // relationship values which are not useForPlace, while the relationhip type has useForPlace + // E.g. when using addAndShiftRightMetadata on relation.isAuthorOfPublication, it will break the order + // from dc.contributor.author + itemService.addMetadata(context, relationship.getLeftItem(), relationshipMetadataValue.getMetadataField(). getMetadataSchema().getName(), relationshipMetadataValue.getMetadataField().getElement(), @@ -533,6 +831,7 @@ private void copyMetadataValues(Context context, Relationship relationship, bool relationshipMetadataValue.getValue(), null, -1, relationshipMetadataValue.getPlace()); } + //This will ensure the new values no longer overlap, but won't break the order itemService.update(context, relationship.getLeftItem()); } if (copyToRightItem) { @@ -542,7 +841,7 @@ private void copyMetadataValues(Context context, Relationship relationship, bool relationshipMetadataService.findRelationshipMetadataValueForItemRelationship(context, relationship.getRightItem(), entityTypeString, relationship, true); for (RelationshipMetadataValue relationshipMetadataValue : relationshipMetadataValues) { - itemService.addAndShiftRightMetadata(context, relationship.getRightItem(), + itemService.addMetadata(context, relationship.getRightItem(), relationshipMetadataValue.getMetadataField(). getMetadataSchema().getName(), relationshipMetadataValue.getMetadataField().getElement(), diff --git a/dspace-api/src/main/java/org/dspace/content/dao/RelationshipDAO.java b/dspace-api/src/main/java/org/dspace/content/dao/RelationshipDAO.java index e28cd0b6ac7e..2e31ca88fced 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/RelationshipDAO.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/RelationshipDAO.java @@ -53,28 +53,6 @@ public interface RelationshipDAO extends GenericDAO { List findByItem(Context context, Item item, Integer limit, Integer offset, boolean excludeTilted) throws SQLException; - /** - * This method returns the next leftplace integer to use for a relationship with this item as the leftItem - * - * @param context The relevant DSpace context - * @param item The item to be matched on leftItem - * @return The next integer to be used for the leftplace of a relationship with the given item - * as a left item - * @throws SQLException If something goes wrong - */ - int findNextLeftPlaceByLeftItem(Context context, Item item) throws SQLException; - - /** - * This method returns the next rightplace integer to use for a relationship with this item as the rightItem - * - * @param context The relevant DSpace context - * @param item The item to be matched on rightItem - * @return The next integer to be used for the rightplace of a relationship with the given item - * as a right item - * @throws SQLException If something goes wrong - */ - int findNextRightPlaceByRightItem(Context context, Item item) throws SQLException; - /** * This method returns a list of Relationship objects for the given RelationshipType object. * It will construct a list of all Relationship objects that have the given RelationshipType object diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/RelationshipDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/RelationshipDAOImpl.java index db1aef96a200..feac778c86e0 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/impl/RelationshipDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/RelationshipDAOImpl.java @@ -85,38 +85,6 @@ public int countByItem(Context context, Item item) return count(context, criteriaQuery, criteriaBuilder, relationshipRoot); } - @Override - public int findNextLeftPlaceByLeftItem(Context context, Item item) throws SQLException { - CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); - CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, Relationship.class); - Root relationshipRoot = criteriaQuery.from(Relationship.class); - criteriaQuery.select(relationshipRoot); - criteriaQuery.where(criteriaBuilder.equal(relationshipRoot.get(Relationship_.leftItem), item)); - List list = list(context, criteriaQuery, false, Relationship.class, -1, -1); - list.sort((o1, o2) -> o2.getLeftPlace() - o1.getLeftPlace()); - if (!list.isEmpty()) { - return list.get(0).getLeftPlace() + 1; - } else { - return 0; - } - } - - @Override - public int findNextRightPlaceByRightItem(Context context, Item item) throws SQLException { - CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); - CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, Relationship.class); - Root relationshipRoot = criteriaQuery.from(Relationship.class); - criteriaQuery.select(relationshipRoot); - criteriaQuery.where(criteriaBuilder.equal(relationshipRoot.get(Relationship_.rightItem), item)); - List list = list(context, criteriaQuery, false, Relationship.class, -1, -1); - list.sort((o1, o2) -> o2.getRightPlace() - o1.getRightPlace()); - if (!list.isEmpty()) { - return list.get(0).getRightPlace() + 1; - } else { - return 0; - } - } - @Override public List findByRelationshipType(Context context, RelationshipType relationshipType) throws SQLException { diff --git a/dspace-api/src/main/java/org/dspace/content/service/RelationshipService.java b/dspace-api/src/main/java/org/dspace/content/service/RelationshipService.java index fab4616ef3d2..ad0f252b286b 100644 --- a/dspace-api/src/main/java/org/dspace/content/service/RelationshipService.java +++ b/dspace-api/src/main/java/org/dspace/content/service/RelationshipService.java @@ -78,26 +78,49 @@ List findByItem(Context context, Item item, Integer limit, Integer public Relationship create(Context context, Relationship relationship) throws SQLException, AuthorizeException; /** - * This method returns the next leftplace integer to use for a relationship with this item as the leftItem + * Move the given relationship to a new leftPlace and/or rightPlace. * - * @param context The relevant DSpace context - * @param item The item that has to be the leftItem of a relationship for it to qualify - * @return The next integer to be used for the leftplace of a relationship with the given item - * as a left item - * @throws SQLException If something goes wrong + * This will + * 1. verify whether the move is authorized + * 2. move the relationship to the specified left/right place + * 3. update the left/right place of other relationships and/or metadata in order to resolve the move without + * leaving any gaps + * + * At least one of the new places should be non-null, otherwise no changes will be made. + * + * @param context The relevant DSpace context + * @param relationship The Relationship to move + * @param newLeftPlace The value to set the leftPlace of this Relationship to + * @param newRightPlace The value to set the rightPlace of this Relationship to + * @return The moved relationship with updated place variables + * @throws SQLException If something goes wrong + * @throws AuthorizeException If the user is not authorized to update the Relationship or its Items */ - int findNextLeftPlaceByLeftItem(Context context, Item item) throws SQLException; + Relationship move(Context context, Relationship relationship, Integer newLeftPlace, Integer newRightPlace) + throws SQLException, AuthorizeException; /** - * This method returns the next rightplace integer to use for a relationship with this item as the rightItem + * Move the given relationship to a new leftItem and/or rightItem. * - * @param context The relevant DSpace context - * @param item The item that has to be the rightitem of a relationship for it to qualify - * @return The next integer to be used for the rightplace of a relationship with the given item - * as a right item - * @throws SQLException If something goes wrong + * This will + * 1. move the relationship to the last place in its current left or right Item. This ensures that we don't leave + * any gaps when moving the relationship to a new Item. + * If only one of the relationship's Items is changed,the order of relationships and metadatain the other + * will not be affected + * 2. insert the relationship into the new Item(s) + * + * At least one of the new Items should be non-null, otherwise no changes will be made. + * + * @param context The relevant DSpace context + * @param relationship The Relationship to move + * @param newLeftItem The value to set the leftItem of this Relationship to + * @param newRightItem The value to set the rightItem of this Relationship to + * @return The moved relationship with updated left/right Items variables + * @throws SQLException If something goes wrong + * @throws AuthorizeException If the user is not authorized to update the Relationship or its Items */ - int findNextRightPlaceByRightItem(Context context, Item item) throws SQLException; + Relationship move(Context context, Relationship relationship, Item newLeftItem, Item newRightItem) + throws SQLException, AuthorizeException; /** * This method returns a list of Relationships for which the leftItem or rightItem is equal to the given @@ -143,19 +166,6 @@ public List findByItemAndRelationshipType(Context context, Item it int limit, int offset) throws SQLException; - /** - * This method will update the place for the Relationship and all other relationships found by the items and - * relationship type of the given Relationship. It will give this Relationship the last place in both the - * left and right place determined by querying for the list of leftRelationships and rightRelationships - * by the leftItem, rightItem and relationshipType of the given Relationship. - * @param context The relevant DSpace context - * @param relationship The Relationship object that will have it's place updated and that will be used - * to retrieve the other relationships whose place might need to be updated - * @throws SQLException If something goes wrong - */ - public void updatePlaceInRelationship(Context context, Relationship relationship) - throws SQLException, AuthorizeException; - /** * This method will update the given item's metadata order. * If the relationships for the item have been modified and will calculate the place based on a diff --git a/dspace-api/src/test/java/org/dspace/builder/RelationshipBuilder.java b/dspace-api/src/test/java/org/dspace/builder/RelationshipBuilder.java index 874603341980..32aa09d868dd 100644 --- a/dspace-api/src/test/java/org/dspace/builder/RelationshipBuilder.java +++ b/dspace-api/src/test/java/org/dspace/builder/RelationshipBuilder.java @@ -117,7 +117,8 @@ private RelationshipBuilder create(Context context, Item leftItem, Item rightIte this.context = context; try { - relationship = relationshipService.create(context, leftItem, rightItem, relationshipType, 0, 0); + //place -1 will add it to the end + relationship = relationshipService.create(context, leftItem, rightItem, relationshipType, -1, -1); } catch (SQLException | AuthorizeException e) { log.warn("Failed to create relationship", e); } diff --git a/dspace-api/src/test/java/org/dspace/content/RelationshipMetadataServiceIT.java b/dspace-api/src/test/java/org/dspace/content/RelationshipMetadataServiceIT.java index 8ade8ca1f648..d9d672042ff4 100644 --- a/dspace-api/src/test/java/org/dspace/content/RelationshipMetadataServiceIT.java +++ b/dspace-api/src/test/java/org/dspace/content/RelationshipMetadataServiceIT.java @@ -599,49 +599,6 @@ public void testDeleteAuthorRelationshipCopyToBothItemsFromDefaultsInDb() throws .size(), equalTo(1)); } - @Test - public void testGetNextRightPlace() throws Exception { - assertThat(relationshipService.findNextRightPlaceByRightItem(context, rightItem), equalTo(0)); - initPublicationAuthor(); - - assertThat(relationshipService.findNextRightPlaceByRightItem(context, rightItem), equalTo(1)); - - context.turnOffAuthorisationSystem(); - Community community = CommunityBuilder.createCommunity(context).build(); - - Collection col = CollectionBuilder.createCollection(context, community).build(); - Item secondItem = ItemBuilder.createItem(context, col).withEntityType("Publication").build(); - RelationshipBuilder.createRelationshipBuilder(context, secondItem, rightItem, - isAuthorOfPublicationRelationshipType).build(); - context.restoreAuthSystemState(); - - assertThat(relationshipService.findNextRightPlaceByRightItem(context, rightItem), equalTo(2)); - } - - @Test - public void testGetNextLeftPlace() throws Exception { - assertThat(relationshipService.findNextLeftPlaceByLeftItem(context, leftItem), equalTo(0)); - initPublicationAuthor(); - - assertThat(relationshipService.findNextLeftPlaceByLeftItem(context, leftItem), equalTo(1)); - - context.turnOffAuthorisationSystem(); - Community community = CommunityBuilder.createCommunity(context).build(); - Collection col = CollectionBuilder.createCollection(context, community).build(); - - Item secondAuthor = ItemBuilder.createItem(context, col).withEntityType("Author") - .withPersonIdentifierFirstName("firstName") - .withPersonIdentifierLastName("familyName").build(); - - RelationshipBuilder.createRelationshipBuilder(context, leftItem, secondAuthor, - isAuthorOfPublicationRelationshipType).build(); - context.restoreAuthSystemState(); - - assertThat(relationshipService.findNextLeftPlaceByLeftItem(context, leftItem), equalTo(2)); - - - } - @Test public void testGetVirtualMetadata() throws SQLException, AuthorizeException { // Journal, JournalVolume, JournalIssue, Publication items, related to each other using the relationship types diff --git a/dspace-api/src/test/java/org/dspace/content/RelationshipServiceImplPlaceTest.java b/dspace-api/src/test/java/org/dspace/content/RelationshipServiceImplPlaceTest.java index 305de076a2f1..28631678df54 100644 --- a/dspace-api/src/test/java/org/dspace/content/RelationshipServiceImplPlaceTest.java +++ b/dspace-api/src/test/java/org/dspace/content/RelationshipServiceImplPlaceTest.java @@ -9,11 +9,15 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import java.sql.SQLException; +import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.dspace.AbstractUnitTest; import org.dspace.authorize.AuthorizeException; @@ -27,6 +31,7 @@ import org.dspace.content.service.RelationshipService; import org.dspace.content.service.RelationshipTypeService; import org.dspace.content.service.WorkspaceItemService; +import org.dspace.core.Constants; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -39,6 +44,8 @@ public class RelationshipServiceImplPlaceTest extends AbstractUnitTest { protected RelationshipService relationshipService = ContentServiceFactory.getInstance().getRelationshipService(); protected RelationshipTypeService relationshipTypeService = ContentServiceFactory.getInstance() .getRelationshipTypeService(); + protected RelationshipMetadataService relationshipMetadataService = + ContentServiceFactory.getInstance().getRelationshipMetadataService(); protected EntityTypeService entityTypeService = ContentServiceFactory.getInstance().getEntityTypeService(); protected CommunityService communityService = ContentServiceFactory.getInstance().getCommunityService(); protected CollectionService collectionService = ContentServiceFactory.getInstance().getCollectionService(); @@ -52,9 +59,32 @@ public class RelationshipServiceImplPlaceTest extends AbstractUnitTest { Item item; Item authorItem; + + Item author1; + Item author2; + Item author3; + Item author4; + Item author5; + Item author6; + Item publication1; + Item publication2; + Item publication3; + Item publication4; + Item publication5; + Item publication6; + Item project1; + Item project2; + Item project3; + Item project4; + Item project5; + Item project6; + RelationshipType isAuthorOfPublication; + RelationshipType isProjectOfPerson; + EntityType publicationEntityType; - EntityType authorEntityType; + EntityType projectEntityType; + EntityType personEntityType; String authorQualifier = "author"; String contributorElement = "contributor"; @@ -84,12 +114,116 @@ public void init() { itemService.addMetadata(context, authorItem, "person", "familyName", null, null, "familyName"); itemService.addMetadata(context, authorItem, "person", "givenName", null, null, "firstName"); + WorkspaceItem wi; + + wi = workspaceItemService.create(context, col, false); + author1 = installItemService.installItem(context, wi); + itemService.addMetadata(context, author1, "dspace", "entity", "type", null, "Person"); + itemService.addMetadata(context, author1, "person", "familyName", null, null, "Author"); + itemService.addMetadata(context, author1, "person", "givenName", null, null, "First"); + + wi = workspaceItemService.create(context, col, false); + author2 = installItemService.installItem(context, wi); + itemService.addMetadata(context, author2, "dspace", "entity", "type", null, "Person"); + itemService.addMetadata(context, author2, "person", "familyName", null, null, "Author"); + itemService.addMetadata(context, author2, "person", "givenName", null, null, "Second"); + + wi = workspaceItemService.create(context, col, false); + author3 = installItemService.installItem(context, wi); + itemService.addMetadata(context, author3, "dspace", "entity", "type", null, "Person"); + itemService.addMetadata(context, author3, "person", "familyName", null, null, "Author"); + itemService.addMetadata(context, author3, "person", "givenName", null, null, "Third"); + + wi = workspaceItemService.create(context, col, false); + author4 = installItemService.installItem(context, wi); + itemService.addMetadata(context, author4, "dspace", "entity", "type", null, "Person"); + itemService.addMetadata(context, author4, "person", "familyName", null, null, "Author"); + itemService.addMetadata(context, author4, "person", "givenName", null, null, "Fourth"); + + wi = workspaceItemService.create(context, col, false); + author5 = installItemService.installItem(context, wi); + itemService.addMetadata(context, author5, "dspace", "entity", "type", null, "Person"); + itemService.addMetadata(context, author5, "person", "familyName", null, null, "Author"); + itemService.addMetadata(context, author5, "person", "givenName", null, null, "Fifth"); + + wi = workspaceItemService.create(context, col, false); + author6 = installItemService.installItem(context, wi); + itemService.addMetadata(context, author6, "dspace", "entity", "type", null, "Person"); + itemService.addMetadata(context, author6, "person", "familyName", null, null, "Author"); + itemService.addMetadata(context, author6, "person", "givenName", null, null, "Sixth"); + + wi = workspaceItemService.create(context, col, false); + publication1 = installItemService.installItem(context, wi); + itemService.addMetadata(context, publication1, "dspace", "entity", "type", null, "Publication"); + itemService.addMetadata(context, publication1, "dc", "title", null, null, "Publication 1"); + + wi = workspaceItemService.create(context, col, false); + publication2 = installItemService.installItem(context, wi); + itemService.addMetadata(context, publication2, "dspace", "entity", "type", null, "Publication"); + itemService.addMetadata(context, publication2, "dc", "title", null, null, "Publication 2"); + + wi = workspaceItemService.create(context, col, false); + publication3 = installItemService.installItem(context, wi); + itemService.addMetadata(context, publication3, "dspace", "entity", "type", null, "Publication"); + itemService.addMetadata(context, publication3, "dc", "title", null, null, "Publication 3"); + + wi = workspaceItemService.create(context, col, false); + publication4 = installItemService.installItem(context, wi); + itemService.addMetadata(context, publication4, "dspace", "entity", "type", null, "Publication"); + itemService.addMetadata(context, publication4, "dc", "title", null, null, "Publication 4"); + + wi = workspaceItemService.create(context, col, false); + publication5 = installItemService.installItem(context, wi); + itemService.addMetadata(context, publication5, "dspace", "entity", "type", null, "Publication"); + itemService.addMetadata(context, publication5, "dc", "title", null, null, "Publication 5"); + + wi = workspaceItemService.create(context, col, false); + publication6 = installItemService.installItem(context, wi); + itemService.addMetadata(context, publication6, "dspace", "entity", "type", null, "Publication"); + itemService.addMetadata(context, publication6, "dc", "title", null, null, "Publication 6"); + + wi = workspaceItemService.create(context, col, false); + project1 = installItemService.installItem(context, wi); + itemService.addMetadata(context, project1, "dspace", "entity", "type", null, "Project"); + itemService.addMetadata(context, project1, "dc", "title", null, null, "Project 1"); + + wi = workspaceItemService.create(context, col, false); + project2 = installItemService.installItem(context, wi); + itemService.addMetadata(context, project2, "dspace", "entity", "type", null, "Project"); + itemService.addMetadata(context, project2, "dc", "title", null, null, "Project 2"); + + wi = workspaceItemService.create(context, col, false); + project3 = installItemService.installItem(context, wi); + itemService.addMetadata(context, project3, "dspace", "entity", "type", null, "Project"); + itemService.addMetadata(context, project3, "dc", "title", null, null, "Project 3"); + + wi = workspaceItemService.create(context, col, false); + project4 = installItemService.installItem(context, wi); + itemService.addMetadata(context, project4, "dspace", "entity", "type", null, "Project"); + itemService.addMetadata(context, project4, "dc", "title", null, null, "Project 4"); + + wi = workspaceItemService.create(context, col, false); + project5 = installItemService.installItem(context, wi); + itemService.addMetadata(context, project5, "dspace", "entity", "type", null, "Project"); + itemService.addMetadata(context, project5, "dc", "title", null, null, "Project 5"); + + wi = workspaceItemService.create(context, col, false); + project6 = installItemService.installItem(context, wi); + itemService.addMetadata(context, project6, "dspace", "entity", "type", null, "Project"); + itemService.addMetadata(context, project6, "dc", "title", null, null, "Project 6"); + + publicationEntityType = entityTypeService.create(context, "Publication"); - authorEntityType = entityTypeService.create(context, "Person"); + projectEntityType = entityTypeService.create(context, "Project"); + personEntityType = entityTypeService.create(context, "Person"); isAuthorOfPublication = relationshipTypeService - .create(context, publicationEntityType, authorEntityType, + .create(context, publicationEntityType, personEntityType, "isAuthorOfPublication", "isPublicationOfAuthor", null, null, null, null); + isProjectOfPerson = relationshipTypeService + .create(context, personEntityType, projectEntityType, + "isProjectOfPerson", "isPersonOfProject", + null, null, null, null); context.restoreAuthSystemState(); } catch (AuthorizeException ex) { @@ -226,7 +360,7 @@ public void AddMetadataAndRelationshipWithSpecificPlaceTest() throws Exception { itemService.addMetadata(context, secondAuthorItem, "person", "familyName", null, null, "familyNameTwo"); itemService.addMetadata(context, secondAuthorItem, "person", "givenName", null, null, "firstNameTwo"); Relationship relationshipTwo = relationshipService - .create(context, item, secondAuthorItem, isAuthorOfPublication, 5, -1); + .create(context, item, secondAuthorItem, isAuthorOfPublication, 1, -1); context.restoreAuthSystemState(); @@ -234,16 +368,19 @@ public void AddMetadataAndRelationshipWithSpecificPlaceTest() throws Exception { list = itemService.getMetadata(item, dcSchema, contributorElement, authorQualifier, Item.ANY); assertMetadataValue(authorQualifier, contributorElement, dcSchema, "test, one", null, 0, list.get(0)); - assertMetadataValue(authorQualifier, contributorElement, dcSchema, "test, two", null, 1, list.get(1)); - assertMetadataValue(authorQualifier, contributorElement, dcSchema, "familyName, firstName", - "virtual::" + relationship.getID(), 2, list.get(2)); - assertThat(relationship.getLeftPlace(), equalTo(2)); - assertMetadataValue(authorQualifier, contributorElement, dcSchema, "test, three", null, 3, list.get(3)); - assertMetadataValue(authorQualifier, contributorElement, dcSchema, "test, four", null, 4, list.get(4)); assertMetadataValue(authorQualifier, contributorElement, dcSchema, "familyNameTwo, firstNameTwo", - "virtual::" + relationshipTwo.getID(), 5, list.get(5)); - assertThat(relationshipTwo.getLeftPlace(), equalTo(5)); + "virtual::" + relationshipTwo.getID(), 1, list.get(1)); + assertThat(relationshipTwo.getLeftPlace(), equalTo(1)); + + assertMetadataValue(authorQualifier, contributorElement, dcSchema, "test, two", null, 2, list.get(2)); + + assertMetadataValue(authorQualifier, contributorElement, dcSchema, "familyName, firstName", + "virtual::" + relationship.getID(), 3, list.get(3)); + assertThat(relationship.getLeftPlace(), equalTo(3)); + + assertMetadataValue(authorQualifier, contributorElement, dcSchema, "test, three", null, 4, list.get(4)); + assertMetadataValue(authorQualifier, contributorElement, dcSchema, "test, four", null, 5, list.get(5)); } @@ -425,4 +562,2206 @@ private void assertMetadataValue(String authorQualifier, String contributorEleme } + /* RelationshipService#create */ + + @Test + public void createUseForPlaceRelationshipAppendingLeftNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to the same Publication, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, -1, -1); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r1, 0); + assertLeftPlace(r2, 1); + assertLeftPlace(r3, 2); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, List.of(r1, r2, r3)); + } + + @Test + public void createUseForPlaceRelationshipWithLeftPlaceAtTheStartNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add two Authors to the same Publication, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + + // Add another Author @ leftPlace 0. The existing relationships should get pushed one place forward + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, 0, -1); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r3, 0); + assertLeftPlace(r1, 1); + assertLeftPlace(r2, 2); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, List.of(r3, r1, r2)); + } + + @Test + public void createUseForPlaceRelationshipWithLeftPlaceInTheMiddleNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add two Authors to the same Publication, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + + // Add another Author @ leftPlace 1. The second relationship should get pushed by one place + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, 1, -1); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r1, 0); + assertLeftPlace(r3, 1); + assertLeftPlace(r2, 2); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, List.of(r1, r3, r2)); + } + + @Test + public void createUseForPlaceRelationshipWithLeftPlaceAtTheEndNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to the same Publication, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + + // Add another Author @ leftPlace 2. This should have the same effect as just appending it + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, 2, -1); + + context.restoreAuthSystemState(); + + // Check relationship order + // Check relationship order + assertLeftPlace(r1, 0); + assertLeftPlace(r2, 1); + assertLeftPlace(r3, 2); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, List.of(r1, r2, r3)); + } + + @Test + public void createUseForPlaceRelationshipAppendingLeftWithMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add a dc.contributor.author MDV + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 1"); + + // Add two Authors to the same Publication, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + + // Add another dc.contributor.author MDV + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 2"); + + // Add another Author to the same Publication, appending to the end + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, -1, -1); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r1, 1); + assertLeftPlace(r2, 2); + assertLeftPlace(r3, 4); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, List.of(r1, r2, r3)); + assertMetadataOrder(publication1, "dc.contributor.author", List.of( + "MDV 1", + "Author, First", + "Author, Second", + "MDV 2", + "Author, Third" + )); + } + + @Test + public void createUseForPlaceRelationshipWithLeftPlaceAtTheStartWithMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add a dc.contributor.author MDV + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 1"); + + // Add two Authors to the same Publication, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + + // Add another dc.contributor.author MDV + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 2"); + + // Add another Author @ leftPlace 0. All MDVs & relationships after it should get pushed by one place + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, 0, -1); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r3, 0); + assertLeftPlace(r1, 2); + assertLeftPlace(r2, 3); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, List.of(r3, r1, r2)); + assertMetadataOrder(publication1, "dc.contributor.author", List.of( + "Author, Third", + "MDV 1", + "Author, First", + "Author, Second", + "MDV 2" + )); + } + + @Test + public void createUseForPlaceRelationshipWithLeftPlaceInTheMiddleWithMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add a dc.contributor.author MDV + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 1"); + + // Add two Authors to the same Publication, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + + // Add another dc.contributor.author MDV + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 2"); + + // Add another Author @ leftPlace 2. All MDVs & relationships after it should get pushed by one place + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, 2, -1); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r1, 1); + assertLeftPlace(r3, 2); + assertLeftPlace(r2, 3); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, List.of(r1, r3, r2)); + assertMetadataOrder(publication1, "dc.contributor.author", List.of( + "MDV 1", + "Author, First", + "Author, Third", + "Author, Second", + "MDV 2" + )); + } + + @Test + public void createUseForPlaceRelationshipWithLeftPlaceAtTheEndWithMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add a dc.contributor.author MDV + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 1"); + + // Add two Authors to the same Publication, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + + // Add another dc.contributor.author MDV + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 2"); + + // Add another Author @ leftPlace 4. This should have the same effect as just appending it + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, 4, -1); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r1, 1); + assertLeftPlace(r2, 2); + assertLeftPlace(r3, 4); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, List.of(r1, r2, r3)); + assertMetadataOrder(publication1, "dc.contributor.author", List.of( + "MDV 1", + "Author, First", + "Author, Second", + "MDV 2", + "Author, Third" + )); + } + + @Test + public void createUseForPlaceRelationshipAppendingRightNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Publications to the same Author, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication2, author1, isAuthorOfPublication, -1, -1); + Relationship r3 = relationshipService.create(context, publication3, author1, isAuthorOfPublication, -1, -1); + + context.restoreAuthSystemState(); + + // Check relationship order + assertRightPlace(r1, 0); + assertRightPlace(r2, 1); + assertRightPlace(r3, 2); + assertRelationMetadataOrder(author1, isAuthorOfPublication, List.of(r1, r2, r3)); + } + + @Test + public void createUseForPlaceRelationshipWithRightPlaceAtTheStartNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add two Publications to the same Author, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication2, author1, isAuthorOfPublication, -1, -1); + + // Add another Publication @ rightPlace 0. The existing relationships should get pushed one place forward + Relationship r3 = relationshipService.create(context, publication3, author1, isAuthorOfPublication, -1, 0); + + context.restoreAuthSystemState(); + + // Check relationship order + assertRightPlace(r3, 0); + assertRightPlace(r1, 1); + assertRightPlace(r2, 2); + assertRelationMetadataOrder(author1, isAuthorOfPublication, List.of(r3, r1, r2)); + } + + @Test + public void createUseForPlaceRelationshipWithRightPlaceInTheMiddleNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add two Publications to the same Author, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication2, author1, isAuthorOfPublication, -1, -1); + + // Add another Publication @ rightPlace 1. The second relationship should get pushed by one place + Relationship r3 = relationshipService.create(context, publication3, author1, isAuthorOfPublication, -1, 1); + + context.restoreAuthSystemState(); + + // Check relationship order + assertRightPlace(r1, 0); + assertRightPlace(r3, 1); + assertRightPlace(r2, 2); + assertRelationMetadataOrder(author1, isAuthorOfPublication, List.of(r1, r3, r2)); + } + + @Test + public void createUseForPlaceRelationshipWithRightPlaceAtTheEndNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Publications to the same Author, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication2, author1, isAuthorOfPublication, -1, -1); + + // Add another Publication @ rightPlace 2. This should have the same effect as just appending it + Relationship r3 = relationshipService.create(context, publication3, author1, isAuthorOfPublication, -1, 2); + + context.restoreAuthSystemState(); + + // Check relationship order + assertRightPlace(r1, 0); + assertRightPlace(r2, 1); + assertRightPlace(r3, 2); + assertRelationMetadataOrder(author1, isAuthorOfPublication, List.of(r1, r2, r3)); + } + + @Test + public void createNonUseForPlaceRelationshipAppendingLeftTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Projects to the same Author, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author1, project2, isProjectOfPerson, -1, -1); + Relationship r3 = relationshipService.create(context, author1, project3, isProjectOfPerson, -1, -1); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r1, 0); + assertLeftPlace(r2, 1); + assertLeftPlace(r3, 2); + assertRelationMetadataOrder(author1, isProjectOfPerson, List.of(r1, r2, r3)); + } + + @Test + public void createNonUseForPlaceRelationshipWithLeftPlaceAtTheStartTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add two Projects to the same Author, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author1, project2, isProjectOfPerson, -1, -1); + + // Add another Project @ leftPlace 0. The existing relationships should get pushed one place forward + Relationship r3 = relationshipService.create(context, author1, project3, isProjectOfPerson, 0, -1); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r3, 0); + assertLeftPlace(r1, 1); + assertLeftPlace(r2, 2); + assertRelationMetadataOrder(author1, isProjectOfPerson, List.of(r3, r1, r2)); + } + + @Test + public void createNonUseForPlaceRelationshipWithLeftPlaceInTheMiddleTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add two Projects to the same Author, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author1, project2, isProjectOfPerson, -1, -1); + + // Add another Project @ leftPlace 1. The second relationship should get pushed by one place + Relationship r3 = relationshipService.create(context, author1, project3, isProjectOfPerson, 1, -1); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r1, 0); + assertLeftPlace(r3, 1); + assertLeftPlace(r2, 2); + assertRelationMetadataOrder(author1, isProjectOfPerson, List.of(r1, r3, r2)); + } + + @Test + public void createNonUseForPlaceRelationshipWithLeftPlaceAtTheEndTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add two Projects to the same Author, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author1, project2, isProjectOfPerson, -1, -1); + + // Add another Project @ leftPlace 2. This should have the same effect as just appending it + Relationship r3 = relationshipService.create(context, author1, project3, isProjectOfPerson, 2, -1); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r1, 0); + assertLeftPlace(r2, 1); + assertLeftPlace(r3, 2); + assertRelationMetadataOrder(author1, isProjectOfPerson, List.of(r1, r2, r3)); + } + + @Test + public void createNonUseForPlaceRelationshipAppendingRightTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to the same Project, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author2, project1, isProjectOfPerson, -1, -1); + Relationship r3 = relationshipService.create(context, author3, project1, isProjectOfPerson, -1, -1); + + context.restoreAuthSystemState(); + + // Check relationship order + assertRightPlace(r1, 0); + assertRightPlace(r2, 1); + assertRightPlace(r3, 2); + assertRelationMetadataOrder(project1, isProjectOfPerson, List.of(r1, r2, r3)); + } + + @Test + public void createNonUseForPlaceRelationshipWithRightPlaceAtTheStartTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add two Authors to the same Project, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author2, project1, isProjectOfPerson, -1, -1); + + // Add another Author @ rightPlace 0. The existing relationships should get pushed one place forward + Relationship r3 = relationshipService.create(context, author3, project1, isProjectOfPerson, -1, 0); + + context.restoreAuthSystemState(); + + // Check relationship order + assertRightPlace(r3, 0); + assertRightPlace(r1, 1); + assertRightPlace(r2, 2); + assertRelationMetadataOrder(project1, isProjectOfPerson, List.of(r3, r1, r2)); + } + + @Test + public void createNonUseForPlaceRelationshipWithRightPlaceInTheMiddleTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add two Authors to the same Project, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author2, project1, isProjectOfPerson, -1, -1); + + // Add another Author @ rightPlace 1. The second relationship should get pushed by one place + Relationship r3 = relationshipService.create(context, author3, project1, isProjectOfPerson, -1, 1); + + context.restoreAuthSystemState(); + + // Check relationship order + assertRightPlace(r1, 0); + assertRightPlace(r3, 1); + assertRightPlace(r2, 2); + assertRelationMetadataOrder(project1, isProjectOfPerson, List.of(r1, r3, r2)); + } + + @Test + public void createNonUseForPlaceRelationshipWithRightPlaceAtTheEndTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add two Authors to the same Project, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author2, project1, isProjectOfPerson, -1, -1); + + // Add another Author @ rightPlace 2. This should have the same effect as just appending it + Relationship r3 = relationshipService.create(context, author3, project1, isProjectOfPerson, -1, 2); + + context.restoreAuthSystemState(); + + // Check relationship order + assertRightPlace(r1, 0); + assertRightPlace(r2, 1); + assertRightPlace(r3, 2); + assertRelationMetadataOrder(project1, isProjectOfPerson, List.of(r1, r2, r3)); + } + + /* RelationshipService#move */ + + @Test + public void moveUseForPlaceRelationshipToCurrentLeftPlaceNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to the same Publication, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, -1, -1); + + relationshipService.move(context, r1, 0, null); + relationshipService.move(context, r2, 1, null); + relationshipService.move(context, r3, 2, null); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r1, 0); + assertLeftPlace(r2, 1); + assertLeftPlace(r3, 2); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, List.of(r1, r2, r3)); + } + + @Test + public void moveUseForPlaceRelationshipToLeftPlaceAtTheStartNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to the same Publication, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, -1, -1); + + relationshipService.move(context, r3, 0, null); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r3, 0); + assertLeftPlace(r1, 1); + assertLeftPlace(r2, 2); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, List.of(r3, r1, r2)); + } + + @Test + public void moveUseForPlaceRelationshipUpToLeftPlaceInTheMiddleNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to the same Publication, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, -1, -1); + + // Move the first Author to leftPlace=1 + relationshipService.move(context, r1, 1, null); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r2, 0); + assertLeftPlace(r1, 1); + assertLeftPlace(r3, 2); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, List.of(r2, r1, r3)); + } + + @Test + public void moveUseForPlaceRelationshipDownToLeftPlaceInTheMiddleNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to the same Publication, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, -1, -1); + + // Move the last Author to leftPlace=1 + relationshipService.move(context, r3, 1, null); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r1, 0); + assertLeftPlace(r3, 1); + assertLeftPlace(r2, 2); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, List.of(r1, r3, r2)); + } + + @Test + public void moveUseForPlaceRelationshipToLeftPlaceAtTheEndNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to the same Publication, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, -1, -1); + + // Move the first Author to the back + relationshipService.move(context, r1, -1, null); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r2, 0); + assertLeftPlace(r3, 1); + assertLeftPlace(r1, 2); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, List.of(r2, r3, r1)); + } + + @Test + public void moveUseForPlaceRelationshipToLeftPlaceAtTheEndOverlapNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to the same Publication, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, -1, -1); + + // Move the first Author to the back + relationshipService.move(context, r1, 2, null); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r2, 0); + assertLeftPlace(r3, 1); + assertLeftPlace(r1, 2); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, List.of(r2, r3, r1)); + } + + @Test + public void moveUseForPlaceRelationshipToCurrentLeftPlaceWithMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Initialize MDVs and Relationships + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 1"); + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 2"); + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, -1, -1); + + relationshipService.move(context, r1, 1, null); + relationshipService.move(context, r2, 2, null); + relationshipService.move(context, r3, 4, null); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r1, 1); + assertLeftPlace(r2, 2); + assertLeftPlace(r3, 4); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, List.of(r1, r2, r3)); + assertMetadataOrder(publication1, "dc.contributor.author", List.of( + "MDV 1", + "Author, First", + "Author, Second", + "MDV 2", + "Author, Third" + )); + } + + @Test + public void moveUseForPlaceRelationshipToLeftPlaceAtTheStartWithMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Initialize MDVs and Relationships + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 1"); + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 2"); + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, -1, -1); + + relationshipService.move(context, r3, 0, null); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r3, 0); + assertLeftPlace(r1, 2); + assertLeftPlace(r2, 3); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, List.of(r3, r1, r2)); + assertMetadataOrder(publication1, "dc.contributor.author", List.of( + "Author, Third", + "MDV 1", + "Author, First", + "Author, Second", + "MDV 2" + )); + } + + @Test + public void moveUseForPlaceRelationshipUpToLeftPlaceInTheMiddleWithTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Initialize MDVs and Relationships + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 1"); + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 2"); + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, -1, -1); + + // Move the first Author to leftPlace=3 + relationshipService.move(context, r1, 3, null); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r2, 1); + assertLeftPlace(r1, 3); + assertLeftPlace(r3, 4); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, List.of(r2, r1, r3)); + } + + @Test + public void moveUseForPlaceRelationshipDownToLeftPlaceInTheMiddleWithMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Initialize MDVs and Relationships + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 1"); + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 2"); + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, -1, -1); + + // Move the last Author to leftPlace=2 + relationshipService.move(context, r3, 2, null); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r1, 1); + assertLeftPlace(r3, 2); + assertLeftPlace(r2, 3); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, List.of(r1, r3, r2)); + } + + @Test + public void moveUseForPlaceRelationshipToLeftPlaceAtTheEndWithMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Initialize MDVs and Relationships + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 1"); + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 2"); + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, -1, -1); + + // Move the first Author to the back + relationshipService.move(context, r1, -1, null); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r2, 1); + assertLeftPlace(r3, 3); + assertLeftPlace(r1, 4); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, List.of(r2, r3, r1)); + } + + @Test + public void moveUseForPlaceRelationshipToLeftPlaceAtTheEndOverlapWithMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Initialize MDVs and Relationships + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 1"); + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 2"); + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, -1, -1); + + // Move the first Author to the back + relationshipService.move(context, r1, 4, null); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r2, 1); + assertLeftPlace(r3, 3); + assertLeftPlace(r1, 4); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, List.of(r2, r3, r1)); + } + + @Test + public void moveUseForPlaceRelationshipToCurrentRightPlaceNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Publications to the same Author, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication2, author1, isAuthorOfPublication, -1, -1); + Relationship r3 = relationshipService.create(context, publication3, author1, isAuthorOfPublication, -1, -1); + + + relationshipService.move(context, r1, null, 0); + relationshipService.move(context, r2, null, 1); + relationshipService.move(context, r3, null, 2); + + context.restoreAuthSystemState(); + + // Check relationship order + assertRightPlace(r1, 0); + assertRightPlace(r2, 1); + assertRightPlace(r3, 2); + assertRelationMetadataOrder(author1, isAuthorOfPublication, List.of(r1, r2, r3)); + } + + @Test + public void moveUseForPlaceRelationshipToRightPlaceAtTheStartNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Publications to the same Author, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication2, author1, isAuthorOfPublication, -1, -1); + Relationship r3 = relationshipService.create(context, publication3, author1, isAuthorOfPublication, -1, -1); + + + relationshipService.move(context, r3, null, 0); + + context.restoreAuthSystemState(); + + // Check relationship order + assertRightPlace(r3, 0); + assertRightPlace(r1, 1); + assertRightPlace(r2, 2); + assertRelationMetadataOrder(author1, isAuthorOfPublication, List.of(r3, r1, r2)); + } + + @Test + public void moveUseForPlaceRelationshipUpToRightPlaceInTheMiddleNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to the same Publication, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication2, author1, isAuthorOfPublication, -1, -1); + Relationship r3 = relationshipService.create(context, publication3, author1, isAuthorOfPublication, -1, -1); + + // Move the first Author to leftPlace=1 + relationshipService.move(context, r1, null, 1); + + context.restoreAuthSystemState(); + + // Check relationship order + assertRightPlace(r2, 0); + assertRightPlace(r1, 1); + assertRightPlace(r3, 2); + assertRelationMetadataOrder(author1, isAuthorOfPublication, List.of(r2, r1, r3)); + } + + @Test + public void moveUseForPlaceRelationshipDownToRightPlaceInTheMiddleNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to the same Publication, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication2, author1, isAuthorOfPublication, -1, -1); + Relationship r3 = relationshipService.create(context, publication3, author1, isAuthorOfPublication, -1, -1); + + // Move the last Author to leftPlace=1 + relationshipService.move(context, r3, null, 1); + + context.restoreAuthSystemState(); + + // Check relationship order + assertRightPlace(r1, 0); + assertRightPlace(r3, 1); + assertRightPlace(r2, 2); + assertRelationMetadataOrder(author1, isAuthorOfPublication, List.of(r1, r3, r2)); + } + + @Test + public void moveUseForPlaceRelationshipToRightPlaceAtTheEndNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to the same Publication, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication2, author1, isAuthorOfPublication, -1, -1); + Relationship r3 = relationshipService.create(context, publication3, author1, isAuthorOfPublication, -1, -1); + + // Move the first Author to the back + relationshipService.move(context, r1, null, -1); + + context.restoreAuthSystemState(); + + // Check relationship order + assertRightPlace(r2, 0); + assertRightPlace(r3, 1); + assertRightPlace(r1, 2); + assertRelationMetadataOrder(author1, isAuthorOfPublication, List.of(r2, r3, r1)); + } + + @Test + public void moveUseForPlaceRelationshipToRightPlaceAtTheEndOverlapNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to the same Publication, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication2, author1, isAuthorOfPublication, -1, -1); + Relationship r3 = relationshipService.create(context, publication3, author1, isAuthorOfPublication, -1, -1); + + // Move the first Author to the back + relationshipService.move(context, r1, null, 2); + + context.restoreAuthSystemState(); + + // Check relationship order + assertRightPlace(r2, 0); + assertRightPlace(r3, 1); + assertRightPlace(r1, 2); + assertRelationMetadataOrder(author1, isAuthorOfPublication, List.of(r2, r3, r1)); + } + + @Test + public void moveNonUseForPlaceRelationshipToCurrentLeftPlaceNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Projects to the same Author, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author1, project2, isProjectOfPerson, -1, -1); + Relationship r3 = relationshipService.create(context, author1, project3, isProjectOfPerson, -1, -1); + + // Move the last Project to the front + relationshipService.move(context, r1, 0, null); + relationshipService.move(context, r2, 1, null); + relationshipService.move(context, r3, 2, null); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r1, 0); + assertLeftPlace(r2, 1); + assertLeftPlace(r3, 2); + assertRelationMetadataOrder(author1, isProjectOfPerson, List.of(r1, r2, r3)); + } + + @Test + public void moveNonUseForPlaceRelationshipToLeftPlaceAtTheStartNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Projects to the same Author, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author1, project2, isProjectOfPerson, -1, -1); + Relationship r3 = relationshipService.create(context, author1, project3, isProjectOfPerson, -1, -1); + + // Move the last Project to the front + relationshipService.move(context, r3, 0, null); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r3, 0); + assertLeftPlace(r1, 1); + assertLeftPlace(r2, 2); + assertRelationMetadataOrder(author1, isProjectOfPerson, List.of(r3, r1, r2)); + } + + @Test + public void moveNonUseForPlaceRelationshipUpToLeftPlaceInTheMiddleNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Projects to the same Author, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author1, project2, isProjectOfPerson, -1, -1); + Relationship r3 = relationshipService.create(context, author1, project3, isProjectOfPerson, -1, -1); + + // Move the first Author to leftPlace=1 + relationshipService.move(context, r1, 1, null); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r2, 0); + assertLeftPlace(r1, 1); + assertLeftPlace(r3, 2); + assertRelationMetadataOrder(author1, isProjectOfPerson, List.of(r2, r1, r3)); + } + + @Test + public void moveNonUseForPlaceRelationshipDownToLeftPlaceInTheMiddleNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Projects to the same Author, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author1, project2, isProjectOfPerson, -1, -1); + Relationship r3 = relationshipService.create(context, author1, project3, isProjectOfPerson, -1, -1); + + // Move the last Author to leftPlace=1 + relationshipService.move(context, r3, 1, null); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r1, 0); + assertLeftPlace(r3, 1); + assertLeftPlace(r2, 2); + assertRelationMetadataOrder(author1, isProjectOfPerson, List.of(r1, r3, r2)); + } + + @Test + public void moveNonUseForPlaceRelationshipToLeftPlaceAtTheEndNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Projects to the same Author, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author1, project2, isProjectOfPerson, -1, -1); + Relationship r3 = relationshipService.create(context, author1, project3, isProjectOfPerson, -1, -1); + + // Move the first Author to the back + relationshipService.move(context, r1, -1, null); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r2, 0); + assertLeftPlace(r3, 1); + assertLeftPlace(r1, 2); + assertRelationMetadataOrder(author1, isProjectOfPerson, List.of(r2, r3, r1)); + } + + @Test + public void moveNonUseForPlaceRelationshipToLeftPlaceAtTheEndOverlapNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Projects to the same Author, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author1, project2, isProjectOfPerson, -1, -1); + Relationship r3 = relationshipService.create(context, author1, project3, isProjectOfPerson, -1, -1); + + // Move the first Author to the back + relationshipService.move(context, r1, 2, null); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r2, 0); + assertLeftPlace(r3, 1); + assertLeftPlace(r1, 2); + assertRelationMetadataOrder(author1, isProjectOfPerson, List.of(r2, r3, r1)); + } + + @Test + public void moveNonUseForPlaceRelationshipToCurrentRightPlaceNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to the same Project, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author2, project1, isProjectOfPerson, -1, -1); + Relationship r3 = relationshipService.create(context, author3, project1, isProjectOfPerson, -1, -1); + + + relationshipService.move(context, r1, null, 0); + relationshipService.move(context, r2, null, 1); + relationshipService.move(context, r3, null, 2); + + context.restoreAuthSystemState(); + + // Check relationship order + assertRightPlace(r1, 0); + assertRightPlace(r2, 1); + assertRightPlace(r3, 2); + assertRelationMetadataOrder(project1, isProjectOfPerson, List.of(r1, r2, r3)); + } + + @Test + public void moveNonUseForPlaceRelationshipToRightPlaceAtTheStartNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to the same Project, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author2, project1, isProjectOfPerson, -1, -1); + Relationship r3 = relationshipService.create(context, author3, project1, isProjectOfPerson, -1, -1); + + + relationshipService.move(context, r3, null, 0); + + context.restoreAuthSystemState(); + + // Check relationship order + assertRightPlace(r3, 0); + assertRightPlace(r1, 1); + assertRightPlace(r2, 2); + assertRelationMetadataOrder(project1, isProjectOfPerson, List.of(r3, r1, r2)); + } + + @Test + public void moveNonUseForPlaceRelationshipUpToRightPlaceInTheMiddleNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to the same Project, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author2, project1, isProjectOfPerson, -1, -1); + Relationship r3 = relationshipService.create(context, author3, project1, isProjectOfPerson, -1, -1); + + // Move the first Author to leftPlace=1 + relationshipService.move(context, r1, null, 1); + + context.restoreAuthSystemState(); + + // Check relationship order + assertRightPlace(r2, 0); + assertRightPlace(r1, 1); + assertRightPlace(r3, 2); + assertRelationMetadataOrder(project1, isProjectOfPerson, List.of(r2, r1, r3)); + } + + @Test + public void moveNonUseForPlaceRelationshipDownToRightPlaceInTheMiddleNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to the same Project, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author2, project1, isProjectOfPerson, -1, -1); + Relationship r3 = relationshipService.create(context, author3, project1, isProjectOfPerson, -1, -1); + + // Move the last Author to leftPlace=1 + relationshipService.move(context, r3, null, 1); + + context.restoreAuthSystemState(); + + // Check relationship order + assertRightPlace(r1, 0); + assertRightPlace(r3, 1); + assertRightPlace(r2, 2); + assertRelationMetadataOrder(project1, isProjectOfPerson, List.of(r1, r3, r2)); + } + + @Test + public void moveNonUseForPlaceRelationshipToRightPlaceAtTheEndNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to the same Project, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author2, project1, isProjectOfPerson, -1, -1); + Relationship r3 = relationshipService.create(context, author3, project1, isProjectOfPerson, -1, -1); + + // Move the first Author to the back + relationshipService.move(context, r1, null, -1); + + context.restoreAuthSystemState(); + + // Check relationship order + assertRightPlace(r2, 0); + assertRightPlace(r3, 1); + assertRightPlace(r1, 2); + assertRelationMetadataOrder(project1, isProjectOfPerson, List.of(r2, r3, r1)); + } + + /* RelationshipService#delete */ + + @Test + public void deleteUseForPlaceRelationshipFromLeftStartNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to the same Publication, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, -1, -1); + + // Delete the first Author + relationshipService.delete(context, r1); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r2, 0); + assertLeftPlace(r3, 1); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, List.of(r2, r3)); + } + + @Test + public void deleteUseForPlaceRelationshipFromLeftMiddleNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to the same Publication, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, -1, -1); + + // Delete the second Author + relationshipService.delete(context, r2); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r1, 0); + assertLeftPlace(r3, 1); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, List.of(r1, r3)); + } + + @Test + public void deleteUseForPlaceRelationshipFromLeftEndNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to the same Publication, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, -1, -1); + + // Delete the third Author + relationshipService.delete(context, r3); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r1, 0); + assertLeftPlace(r2, 1); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, List.of(r1, r2)); + } + + @Test + public void deleteUseForPlaceRelationshipFromLeftStartWithMetadataNoCopyTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Initialize MDVs and Relationships + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 1"); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 2"); + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, -1, -1); + + relationshipService.delete(context, r1, false, false); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r2, 1); + assertLeftPlace(r3, 3); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, List.of(r2, r3)); + assertMetadataOrder(publication1, "dc.contributor.author", List.of( + "MDV 1", + "Author, Second", + "MDV 2", + "Author, Third" + )); + } + + @Test + public void deleteUseForPlaceRelationshipFromLeftStartWithMetadataCopyTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Initialize MDVs and Relationships + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 1"); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 2"); + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, -1, -1); + + relationshipService.delete(context, r1, true, false); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r2, 2); + assertLeftPlace(r3, 4); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, Arrays.asList(null, r2, r3)); + assertMetadataOrder(publication1, "dc.contributor.author", List.of( + "Author, First", // this is not longer a relationship + "MDV 1", + "Author, Second", + "MDV 2", + "Author, Third" + )); + } + + @Test + public void deleteUseForPlaceRelationshipFromLeftMiddleWithMetadataNoCopyTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Initialize MDVs and Relationships + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 1"); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 2"); + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, -1, -1); + + relationshipService.delete(context, r2, false, false); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r1, 0); + assertLeftPlace(r3, 3); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, Arrays.asList(r1, r3)); + assertMetadataOrder(publication1, "dc.contributor.author", List.of( + "Author, First", + "MDV 1", + "MDV 2", + "Author, Third" + )); + } + + @Test + public void deleteUseForPlaceRelationshipFromLeftMiddleWithMetadataCopyTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Initialize MDVs and Relationships + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 1"); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 2"); + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, -1, -1); + + relationshipService.delete(context, r2, true, false); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r1, 0); + assertLeftPlace(r3, 4); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, Arrays.asList(r1, null, r3)); + assertMetadataOrder(publication1, "dc.contributor.author", List.of( + "Author, First", + "MDV 1", + "Author, Second", // this is not longer a relationship + "MDV 2", + "Author, Third" + )); + } + + @Test + public void deleteUseForPlaceRelationshipFromLeftEndWithMetadataNoCopyTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Initialize MDVs and Relationships + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 1"); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 2"); + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, -1, -1); + + + relationshipService.delete(context, r3, false, false); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r1, 0); + assertLeftPlace(r2, 2); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, Arrays.asList(r1, r2)); + assertMetadataOrder(publication1, "dc.contributor.author", List.of( + "Author, First", + "MDV 1", + "Author, Second", + "MDV 2" + )); + } + + @Test + public void deleteUseForPlaceRelationshipFromLeftEndWithMetadataCopyTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Initialize MDVs and Relationships + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 1"); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 2"); + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, -1, -1); + + relationshipService.delete(context, r3, true, false); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r1, 0); + assertLeftPlace(r2, 2); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, Arrays.asList(r1, r2, null)); + assertMetadataOrder(publication1, "dc.contributor.author", List.of( + "Author, First", + "MDV 1", + "Author, Second", + "MDV 2", + "Author, Third" // this is not longer a relationship + )); + } + + @Test + public void deleteUseForPlaceRelationshipFromRightStartNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Publications to the same Author, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication2, author1, isAuthorOfPublication, -1, -1); + Relationship r3 = relationshipService.create(context, publication3, author1, isAuthorOfPublication, -1, -1); + + // Delete the first Publication + relationshipService.delete(context, r1); + + context.restoreAuthSystemState(); + + // Check relationship order + assertRightPlace(r2, 0); + assertRightPlace(r3, 1); + assertRelationMetadataOrder(author1, isAuthorOfPublication, List.of(r2, r3)); + } + + @Test + public void deleteUseForPlaceRelationshipFromRightMiddleNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Publications to the same Author, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication2, author1, isAuthorOfPublication, -1, -1); + Relationship r3 = relationshipService.create(context, publication3, author1, isAuthorOfPublication, -1, -1); + + // Delete the second Publication + relationshipService.delete(context, r2); + + context.restoreAuthSystemState(); + + // Check relationship order + assertRightPlace(r1, 0); + assertRightPlace(r3, 1); + assertRelationMetadataOrder(author1, isAuthorOfPublication, List.of(r1, r3)); + } + + @Test + public void deleteUseForPlaceRelationshipFromRightEndNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Publications to the same Author, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication2, author1, isAuthorOfPublication, -1, -1); + Relationship r3 = relationshipService.create(context, publication3, author1, isAuthorOfPublication, -1, -1); + + // Delete the third Publication + relationshipService.delete(context, r3); + + context.restoreAuthSystemState(); + + // Check relationship order + assertRightPlace(r1, 0); + assertRightPlace(r2, 1); + assertRelationMetadataOrder(author1, isAuthorOfPublication, List.of(r1, r2)); + } + + @Test + public void deleteNonUseForPlaceRelationshipFromLeftStartNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Projects to the same Author, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author1, project2, isProjectOfPerson, -1, -1); + Relationship r3 = relationshipService.create(context, author1, project3, isProjectOfPerson, -1, -1); + + // Delete the first Author + relationshipService.delete(context, r1); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r2, 0); + assertLeftPlace(r3, 1); + assertRelationMetadataOrder(author1, isProjectOfPerson, List.of(r2, r3)); + } + + @Test + public void deleteNonUseForPlaceRelationshipFromLeftMiddleNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Projects to the same Author, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author1, project2, isProjectOfPerson, -1, -1); + Relationship r3 = relationshipService.create(context, author1, project3, isProjectOfPerson, -1, -1); + + // Delete the second Author + relationshipService.delete(context, r2); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r1, 0); + assertLeftPlace(r3, 1); + assertRelationMetadataOrder(author1, isProjectOfPerson, List.of(r1, r3)); + } + + @Test + public void deleteNonUseForPlaceRelationshipFromLeftEndNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Projects to the same Author, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author1, project2, isProjectOfPerson, -1, -1); + Relationship r3 = relationshipService.create(context, author1, project3, isProjectOfPerson, -1, -1); + + // Delete the third Author + relationshipService.delete(context, r3); + + context.restoreAuthSystemState(); + + // Check relationship order + assertLeftPlace(r1, 0); + assertLeftPlace(r2, 1); + assertRelationMetadataOrder(author1, isProjectOfPerson, List.of(r1, r2)); + } + + @Test + public void deleteNonUseForPlaceRelationshipFromRightStartNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to the same Project, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author2, project1, isProjectOfPerson, -1, -1); + Relationship r3 = relationshipService.create(context, author3, project1, isProjectOfPerson, -1, -1); + + // Delete the first Publication + relationshipService.delete(context, r1); + + context.restoreAuthSystemState(); + + // Check relationship order + assertRightPlace(r2, 0); + assertRightPlace(r3, 1); + assertRelationMetadataOrder(project1, isProjectOfPerson, List.of(r2, r3)); + } + + @Test + public void deleteNonUseForPlaceRelationshipFromRightMiddleNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to the same Project, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author2, project1, isProjectOfPerson, -1, -1); + Relationship r3 = relationshipService.create(context, author3, project1, isProjectOfPerson, -1, -1); + + // Delete the second Publication + relationshipService.delete(context, r2); + + context.restoreAuthSystemState(); + + // Check relationship order + assertRightPlace(r1, 0); + assertRightPlace(r3, 1); + assertRelationMetadataOrder(project1, isProjectOfPerson, List.of(r1, r3)); + } + + @Test + public void deleteNonUseForPlaceRelationshipFromRightEndNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to the same Project, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author2, project1, isProjectOfPerson, -1, -1); + Relationship r3 = relationshipService.create(context, author3, project1, isProjectOfPerson, -1, -1); + + // Delete the third Publication + relationshipService.delete(context, r3); + + context.restoreAuthSystemState(); + + // Check relationship order + assertRightPlace(r1, 0); + assertRightPlace(r2, 1); + assertRelationMetadataOrder(project1, isProjectOfPerson, List.of(r1, r2)); + } + + @Test + public void changeLeftItemInUseForPlaceRelationshipAtTheStartNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to publication1, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, -1, -1); + + // Add three Authors to publication2, appending to the end + Relationship r4 = relationshipService.create(context, publication2, author4, isAuthorOfPublication, -1, -1); + Relationship r5 = relationshipService.create(context, publication2, author5, isAuthorOfPublication, -1, -1); + Relationship r6 = relationshipService.create(context, publication2, author6, isAuthorOfPublication, -1, -1); + + // Move r1 to publication 2 + relationshipService.move(context, r1, publication2, null); + + context.restoreAuthSystemState(); + + // Check relationship order for publication1 + assertLeftPlace(r2, 0); // should both move down as the first Relationship was removed + assertLeftPlace(r3, 1); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, List.of(r2, r3)); + + // Check relationship order for publication2 + assertLeftPlace(r4, 0); // previous Relationships should stay where they were + assertLeftPlace(r5, 1); + assertLeftPlace(r6, 2); + assertLeftPlace(r1, 3); // moved Relationship should be appended to the end + assertRelationMetadataOrder(publication2, isAuthorOfPublication, List.of(r4, r5, r6, r1)); + } + + @Test + public void changeLeftItemInUseForPlaceRelationshipInTheMiddleNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to publication1, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, -1, -1); + + // Add three Authors to publication2, appending to the end + Relationship r4 = relationshipService.create(context, publication2, author4, isAuthorOfPublication, -1, -1); + Relationship r5 = relationshipService.create(context, publication2, author5, isAuthorOfPublication, -1, -1); + Relationship r6 = relationshipService.create(context, publication2, author6, isAuthorOfPublication, -1, -1); + + // Move r2 to publication 2 + relationshipService.move(context, r2, publication2, null); + + context.restoreAuthSystemState(); + + // Check relationship order for publication1 + assertLeftPlace(r1, 0); + assertLeftPlace(r3, 1); // should move down as the second Relationship was removed + assertRelationMetadataOrder(publication1, isAuthorOfPublication, List.of(r1, r3)); + + // Check relationship order for publication2 + assertLeftPlace(r4, 0); // previous Relationships should stay where they were + assertLeftPlace(r5, 1); + assertLeftPlace(r6, 2); + assertLeftPlace(r2, 3); // moved Relationship should be appended to the end + assertRelationMetadataOrder(publication2, isAuthorOfPublication, List.of(r4, r5, r6, r2)); + } + + @Test + public void changeLeftItemInUseForPlaceRelationshipAtTheEndNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to publication1, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, -1, -1); + + // Add three Authors to publication2, appending to the end + Relationship r4 = relationshipService.create(context, publication2, author4, isAuthorOfPublication, -1, -1); + Relationship r5 = relationshipService.create(context, publication2, author5, isAuthorOfPublication, -1, -1); + Relationship r6 = relationshipService.create(context, publication2, author6, isAuthorOfPublication, -1, -1); + + // Move r3 to publication 2 + relationshipService.move(context, r3, publication2, null); + + context.restoreAuthSystemState(); + + // Check relationship order for publication1 + assertLeftPlace(r1, 0); + assertLeftPlace(r2, 1); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, List.of(r1, r2)); + + // Check relationship order for publication2 + assertLeftPlace(r4, 0); // previous Relationships should stay where they were + assertLeftPlace(r5, 1); + assertLeftPlace(r6, 2); + assertLeftPlace(r3, 3); // moved Relationship should be appended to the end + assertRelationMetadataOrder(publication2, isAuthorOfPublication, List.of(r4, r5, r6, r3)); + } + + @Test + public void changeLeftItemInUseForPlaceRelationshipAtTheStartWithMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to publication1, with regular MDVs in between + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 1"); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 2"); + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, -1, -1); + + // Add three Authors to publication2, with regular MDVs in between + itemService.addMetadata(context, publication2, dcSchema, contributorElement, authorQualifier, null, "MDV 3"); + Relationship r4 = relationshipService.create(context, publication2, author4, isAuthorOfPublication, -1, -1); + Relationship r5 = relationshipService.create(context, publication2, author5, isAuthorOfPublication, -1, -1); + itemService.addMetadata(context, publication2, dcSchema, contributorElement, authorQualifier, null, "MDV 4"); + Relationship r6 = relationshipService.create(context, publication2, author6, isAuthorOfPublication, -1, -1); + + // Move r1 to publication 2 + relationshipService.move(context, r1, publication2, null); + + context.restoreAuthSystemState(); + + // Check relationship order for publication1 + assertLeftPlace(r2, 1); // should both move down as the first Relationship was removed + assertLeftPlace(r3, 3); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, List.of(r2, r3)); + assertMetadataOrder(publication1, "dc.contributor.author", List.of( + "MDV 1", + "Author, Second", + "MDV 2", + "Author, Third" + )); + + // Check relationship order for publication2 + assertLeftPlace(r4, 1); // previous Relationships should stay where they were + assertLeftPlace(r5, 2); + assertLeftPlace(r6, 4); + assertLeftPlace(r1, 5); // moved Relationship should be appended to the end + assertRelationMetadataOrder(publication2, isAuthorOfPublication, List.of(r4, r5, r6, r1)); + assertMetadataOrder(publication2, "dc.contributor.author", List.of( + "MDV 3", + "Author, Fourth", + "Author, Fifth", + "MDV 4", + "Author, Sixth", + "Author, First" + )); + } + + @Test + public void changeLeftItemInUseForPlaceRelationshipInTheMiddleWithMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to publication1, with regular MDVs in between + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 1"); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 2"); + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, -1, -1); + + // Add three Authors to publication2, with regular MDVs in between + itemService.addMetadata(context, publication2, dcSchema, contributorElement, authorQualifier, null, "MDV 3"); + Relationship r4 = relationshipService.create(context, publication2, author4, isAuthorOfPublication, -1, -1); + Relationship r5 = relationshipService.create(context, publication2, author5, isAuthorOfPublication, -1, -1); + itemService.addMetadata(context, publication2, dcSchema, contributorElement, authorQualifier, null, "MDV 4"); + Relationship r6 = relationshipService.create(context, publication2, author6, isAuthorOfPublication, -1, -1); + + // Move r2 to publication 2 + relationshipService.move(context, r2, publication2, null); + + context.restoreAuthSystemState(); + + // Check relationship order for publication1 + assertLeftPlace(r1, 0); // should both move down as the first Relationship was removed + assertLeftPlace(r3, 3); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, List.of(r1, r3)); + assertMetadataOrder(publication1, "dc.contributor.author", List.of( + "Author, First", + "MDV 1", + "MDV 2", + "Author, Third" + )); + + // Check relationship order for publication2 + assertLeftPlace(r4, 1); // previous Relationships should stay where they were + assertLeftPlace(r5, 2); + assertLeftPlace(r6, 4); + assertLeftPlace(r2, 5); // moved Relationship should be appended to the end + assertRelationMetadataOrder(publication2, isAuthorOfPublication, List.of(r4, r5, r6, r2)); + assertMetadataOrder(publication2, "dc.contributor.author", List.of( + "MDV 3", + "Author, Fourth", + "Author, Fifth", + "MDV 4", + "Author, Sixth", + "Author, Second" + )); + } + + @Test + public void changeLeftItemInUseForPlaceRelationshipAtTheEndWithMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to publication1, with regular MDVs in between + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 1"); + Relationship r2 = relationshipService.create(context, publication1, author2, isAuthorOfPublication, -1, -1); + itemService.addMetadata(context, publication1, dcSchema, contributorElement, authorQualifier, null, "MDV 2"); + Relationship r3 = relationshipService.create(context, publication1, author3, isAuthorOfPublication, -1, -1); + + // Add three Authors to publication2, with regular MDVs in between + itemService.addMetadata(context, publication2, dcSchema, contributorElement, authorQualifier, null, "MDV 3"); + Relationship r4 = relationshipService.create(context, publication2, author4, isAuthorOfPublication, -1, -1); + Relationship r5 = relationshipService.create(context, publication2, author5, isAuthorOfPublication, -1, -1); + itemService.addMetadata(context, publication2, dcSchema, contributorElement, authorQualifier, null, "MDV 4"); + Relationship r6 = relationshipService.create(context, publication2, author6, isAuthorOfPublication, -1, -1); + + // Move r3 to publication 2 + relationshipService.move(context, r3, publication2, null); + + context.restoreAuthSystemState(); + + // Check relationship order for publication1 + assertLeftPlace(r1, 0); + assertLeftPlace(r2, 2); + assertRelationMetadataOrder(publication1, isAuthorOfPublication, List.of(r1, r2)); + assertMetadataOrder(publication1, "dc.contributor.author", List.of( + "Author, First", + "MDV 1", + "Author, Second", + "MDV 2" + )); + + // Check relationship order for publication2 + assertLeftPlace(r4, 1); // previous Relationships should stay where they were + assertLeftPlace(r5, 2); + assertLeftPlace(r6, 4); + assertLeftPlace(r3, 5); // moved Relationship should be appended to the end + assertRelationMetadataOrder(publication2, isAuthorOfPublication, List.of(r4, r5, r6, r3)); + assertMetadataOrder(publication2, "dc.contributor.author", List.of( + "MDV 3", + "Author, Fourth", + "Author, Fifth", + "MDV 4", + "Author, Sixth", + "Author, Third" + )); + } + + @Test + public void changeRightItemInUseForPlaceRelationshipAtTheStartNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Publications to author1, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication2, author1, isAuthorOfPublication, -1, -1); + Relationship r3 = relationshipService.create(context, publication3, author1, isAuthorOfPublication, -1, -1); + + // Add three Publications to author2, appending to the end + Relationship r4 = relationshipService.create(context, publication4, author2, isAuthorOfPublication, -1, -1); + Relationship r5 = relationshipService.create(context, publication5, author2, isAuthorOfPublication, -1, -1); + Relationship r6 = relationshipService.create(context, publication6, author2, isAuthorOfPublication, -1, -1); + + // Move r1 to author2 + relationshipService.move(context, r1, null, author2); + + context.restoreAuthSystemState(); + + // Check relationship order for author1 + assertRightPlace(r2, 0); // should both move down as the first Relationship was removed + assertRightPlace(r3, 1); + assertRelationMetadataOrder(author1, isAuthorOfPublication, List.of(r2, r3)); + + // Check relationship order for author2 + assertRightPlace(r4, 0); // previous Relationships should stay where they were + assertRightPlace(r5, 1); + assertRightPlace(r6, 2); + assertRightPlace(r1, 3); // moved Relationship should be appended to the end + assertRelationMetadataOrder(author2, isAuthorOfPublication, List.of(r4, r5, r6, r1)); + } + + @Test + public void changeRightItemInUseForPlaceRelationshipInTheMiddleNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Publications to author1, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication2, author1, isAuthorOfPublication, -1, -1); + Relationship r3 = relationshipService.create(context, publication3, author1, isAuthorOfPublication, -1, -1); + + // Add three Publications to author2, appending to the end + Relationship r4 = relationshipService.create(context, publication4, author2, isAuthorOfPublication, -1, -1); + Relationship r5 = relationshipService.create(context, publication5, author2, isAuthorOfPublication, -1, -1); + Relationship r6 = relationshipService.create(context, publication6, author2, isAuthorOfPublication, -1, -1); + + // Move r2 to author2 + relationshipService.move(context, r2, null, author2); + + context.restoreAuthSystemState(); + + // Check relationship order for author1 + assertRightPlace(r1, 0); + assertRightPlace(r3, 1); // should move down as the first Relationship was removed + assertRelationMetadataOrder(author1, isAuthorOfPublication, List.of(r1, r3)); + + // Check relationship order for author2 + assertRightPlace(r4, 0); // previous Relationships should stay where they were + assertRightPlace(r5, 1); + assertRightPlace(r6, 2); + assertRightPlace(r2, 3); // moved Relationship should be appended to the end + assertRelationMetadataOrder(author2, isAuthorOfPublication, List.of(r4, r5, r6, r2)); + } + + @Test + public void changeRightItemInUseForPlaceRelationshipAtTheEndNoMetadataTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Publications to author1, appending to the end + Relationship r1 = relationshipService.create(context, publication1, author1, isAuthorOfPublication, -1, -1); + Relationship r2 = relationshipService.create(context, publication2, author1, isAuthorOfPublication, -1, -1); + Relationship r3 = relationshipService.create(context, publication3, author1, isAuthorOfPublication, -1, -1); + + // Add three Publications to author2, appending to the end + Relationship r4 = relationshipService.create(context, publication4, author2, isAuthorOfPublication, -1, -1); + Relationship r5 = relationshipService.create(context, publication5, author2, isAuthorOfPublication, -1, -1); + Relationship r6 = relationshipService.create(context, publication6, author2, isAuthorOfPublication, -1, -1); + + // Move r3 to author2 + relationshipService.move(context, r3, null, author2); + + context.restoreAuthSystemState(); + + // Check relationship order for author1 + assertRightPlace(r1, 0); + assertRightPlace(r2, 1); + assertRelationMetadataOrder(author1, isAuthorOfPublication, List.of(r1, r2)); + + // Check relationship order for author2 + assertRightPlace(r4, 0); // previous Relationships should stay where they were + assertRightPlace(r5, 1); + assertRightPlace(r6, 2); + assertRightPlace(r3, 3); // moved Relationship should be appended to the end + assertRelationMetadataOrder(author2, isAuthorOfPublication, List.of(r4, r5, r6, r3)); + } + + @Test + public void changeLeftItemInNonUseForPlaceRelationshipAtTheStart() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Projects to author1, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author1, project2, isProjectOfPerson, -1, -1); + Relationship r3 = relationshipService.create(context, author1, project3, isProjectOfPerson, -1, -1); + + // Add three Projects to author2, appending to the end + Relationship r4 = relationshipService.create(context, author2, project4, isProjectOfPerson, -1, -1); + Relationship r5 = relationshipService.create(context, author2, project5, isProjectOfPerson, -1, -1); + Relationship r6 = relationshipService.create(context, author2, project6, isProjectOfPerson, -1, -1); + + // Move r1 to publication 2 + relationshipService.move(context, r1, author2, null); + + context.restoreAuthSystemState(); + + // Check relationship order for author1 + assertLeftPlace(r2, 0); // should both move down as the first Relationship was removed + assertLeftPlace(r3, 1); + assertRelationMetadataOrder(author1, isProjectOfPerson, List.of(r2, r3)); + + // Check relationship order for author2 + assertLeftPlace(r4, 0); // previous Relationships should stay where they were + assertLeftPlace(r5, 1); + assertLeftPlace(r6, 2); + assertLeftPlace(r1, 3); // moved Relationship should be appended to the end + assertRelationMetadataOrder(author2, isProjectOfPerson, List.of(r4, r5, r6, r1)); + } + + @Test + public void changeLeftItemInNonUseNonForPlaceRelationshipInTheMiddle() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Projects to author1, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author1, project2, isProjectOfPerson, -1, -1); + Relationship r3 = relationshipService.create(context, author1, project3, isProjectOfPerson, -1, -1); + + // Add three Projects to author2, appending to the end + Relationship r4 = relationshipService.create(context, author2, project4, isProjectOfPerson, -1, -1); + Relationship r5 = relationshipService.create(context, author2, project5, isProjectOfPerson, -1, -1); + Relationship r6 = relationshipService.create(context, author2, project6, isProjectOfPerson, -1, -1); + + // Move r2 to author2 + relationshipService.move(context, r2, author2, null); + + context.restoreAuthSystemState(); + + // Check relationship order for author1 + assertLeftPlace(r1, 0); + assertLeftPlace(r3, 1); // should move down as the second Relationship was removed + assertRelationMetadataOrder(author1, isProjectOfPerson, List.of(r1, r3)); + + // Check relationship order for author2 + assertLeftPlace(r4, 0); // previous Relationships should stay where they were + assertLeftPlace(r5, 1); + assertLeftPlace(r6, 2); + assertLeftPlace(r2, 3); // moved Relationship should be appended to the end + assertRelationMetadataOrder(author2, isProjectOfPerson, List.of(r4, r5, r6, r2)); + } + + @Test + public void changeLeftItemInNonUseForPlaceRelationshipAtTheEnd() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Projects to author1, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author1, project2, isProjectOfPerson, -1, -1); + Relationship r3 = relationshipService.create(context, author1, project3, isProjectOfPerson, -1, -1); + + // Add three Projects to author2, appending to the end + Relationship r4 = relationshipService.create(context, author2, project4, isProjectOfPerson, -1, -1); + Relationship r5 = relationshipService.create(context, author2, project5, isProjectOfPerson, -1, -1); + Relationship r6 = relationshipService.create(context, author2, project6, isProjectOfPerson, -1, -1); + + // Move r3 to author2 + relationshipService.move(context, r3, author2, null); + + context.restoreAuthSystemState(); + + // Check relationship order for publication1 + assertLeftPlace(r1, 0); + assertLeftPlace(r2, 1); + assertRelationMetadataOrder(author1, isProjectOfPerson, List.of(r1, r2)); + + // Check relationship order for publication2 + assertLeftPlace(r4, 0); // previous Relationships should stay where they were + assertLeftPlace(r5, 1); + assertLeftPlace(r6, 2); + assertLeftPlace(r3, 3); // moved Relationship should be appended to the end + assertRelationMetadataOrder(author2, isProjectOfPerson, List.of(r4, r5, r6, r3)); + } + + @Test + public void changeRightItemInUseNonForPlaceRelationshipAtTheStartTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to project1, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author2, project1, isProjectOfPerson, -1, -1); + Relationship r3 = relationshipService.create(context, author3, project1, isProjectOfPerson, -1, -1); + + // Add three Authors to project2, appending to the end + Relationship r4 = relationshipService.create(context, author4, project2, isProjectOfPerson, -1, -1); + Relationship r5 = relationshipService.create(context, author5, project2, isProjectOfPerson, -1, -1); + Relationship r6 = relationshipService.create(context, author6, project2, isProjectOfPerson, -1, -1); + + // Move r1 to project2 + relationshipService.move(context, r1, null, project2); + + context.restoreAuthSystemState(); + + // Check relationship order for project1 + assertRightPlace(r2, 0); // should both move down as the first Relationship was removed + assertRightPlace(r3, 1); + assertRelationMetadataOrder(project1, isProjectOfPerson, List.of(r2, r3)); + + // Check relationship order for project2 + assertRightPlace(r4, 0); // previous Relationships should stay where they were + assertRightPlace(r5, 1); + assertRightPlace(r6, 2); + assertRightPlace(r1, 3); // moved Relationship should be appended to the end + assertRelationMetadataOrder(project2, isProjectOfPerson, List.of(r4, r5, r6, r1)); + } + + @Test + public void changeRightItemInNonUseForPlaceRelationshipInTheMiddleTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to project1, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author2, project1, isProjectOfPerson, -1, -1); + Relationship r3 = relationshipService.create(context, author3, project1, isProjectOfPerson, -1, -1); + + // Add three Authors to project2, appending to the end + Relationship r4 = relationshipService.create(context, author4, project2, isProjectOfPerson, -1, -1); + Relationship r5 = relationshipService.create(context, author5, project2, isProjectOfPerson, -1, -1); + Relationship r6 = relationshipService.create(context, author6, project2, isProjectOfPerson, -1, -1); + + // Move r2 to project2 + relationshipService.move(context, r2, null, project2); + + context.restoreAuthSystemState(); + + // Check relationship order for project1 + assertRightPlace(r1, 0); + assertRightPlace(r3, 1); // should move down as the first Relationship was removed + assertRelationMetadataOrder(project1, isProjectOfPerson, List.of(r1, r3)); + + // Check relationship order for project2 + assertRightPlace(r4, 0); // previous Relationships should stay where they were + assertRightPlace(r5, 1); + assertRightPlace(r6, 2); + assertRightPlace(r2, 3); // moved Relationship should be appended to the end + assertRelationMetadataOrder(project2, isProjectOfPerson, List.of(r4, r5, r6, r2)); + } + + @Test + public void changeRightItemInNonUseForPlaceRelationshipAtTheEndTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to project1, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author2, project1, isProjectOfPerson, -1, -1); + Relationship r3 = relationshipService.create(context, author3, project1, isProjectOfPerson, -1, -1); + + // Add three Authors to project2, appending to the end + Relationship r4 = relationshipService.create(context, author4, project2, isProjectOfPerson, -1, -1); + Relationship r5 = relationshipService.create(context, author5, project2, isProjectOfPerson, -1, -1); + Relationship r6 = relationshipService.create(context, author6, project2, isProjectOfPerson, -1, -1); + + // Move r3 to project2 + relationshipService.move(context, r3, null, project2); + + context.restoreAuthSystemState(); + + // Check relationship order for author1 + assertRightPlace(r1, 0); + assertRightPlace(r2, 1); + assertRelationMetadataOrder(project1, isProjectOfPerson, List.of(r1, r2)); + + // Check relationship order for author2 + assertRightPlace(r4, 0); // previous Relationships should stay where they were + assertRightPlace(r5, 1); + assertRightPlace(r6, 2); + assertRightPlace(r3, 3); // moved Relationship should be appended to the end + assertRelationMetadataOrder(project2, isProjectOfPerson, List.of(r4, r5, r6, r3)); + } + + @Test + public void changeLeftItemInNonUseForPlaceRelationshipAtTheStartToSameItemNoChanges() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Projects to author1, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author1, project2, isProjectOfPerson, -1, -1); + Relationship r3 = relationshipService.create(context, author1, project3, isProjectOfPerson, -1, -1); + + // Move r1 to author1 + relationshipService.move(context, r1, author1, null); + + context.restoreAuthSystemState(); + + // Check relationship order for author1 -> should remain unchanged + assertLeftPlace(r1, 0); + assertLeftPlace(r2, 1); + assertLeftPlace(r3, 2); + assertRelationMetadataOrder(author1, isProjectOfPerson, List.of(r1, r2, r3)); + } + + @Test + public void changeRightItemInNonUseForPlaceRelationshipAtTheStartToSameItemNoChanges() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Projects to author1, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author1, project2, isProjectOfPerson, -1, -1); + Relationship r3 = relationshipService.create(context, author1, project3, isProjectOfPerson, -1, -1); + + // Move r1 to author1 + relationshipService.move(context, r1, null, project1); + + context.restoreAuthSystemState(); + + // Check relationship order for author1 -> should remain unchanged + assertLeftPlace(r1, 0); + assertLeftPlace(r2, 1); + assertLeftPlace(r3, 2); + assertRelationMetadataOrder(author1, isProjectOfPerson, List.of(r1, r2, r3)); + } + + @Test + public void changeLeftItemInNonUseForPlaceRelationshipAtTheStartWithSiblingsInOldLeftItem() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Projects to author1, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author1, project2, isProjectOfPerson, -1, -1); + Relationship r3 = relationshipService.create(context, author1, project3, isProjectOfPerson, -1, -1); + + // Add three Authors to project1, appending to the end + Relationship r4 = relationshipService.create(context, author4, project1, isProjectOfPerson, -1, -1); + Relationship r5 = relationshipService.create(context, author5, project1, isProjectOfPerson, -1, -1); + Relationship r6 = relationshipService.create(context, author6, project1, isProjectOfPerson, -1, -1); + + // Move r1 to author2 + relationshipService.move(context, r1, author2, null); + + context.restoreAuthSystemState(); + + // Check relationship order for author1 -> should shift down by one + assertLeftPlace(r2, 0); + assertLeftPlace(r3, 1); + assertRelationMetadataOrder(author1, isProjectOfPerson, List.of(r2, r3)); + + // Check relationship order for project 1 -> should remain unchanged + assertRightPlace(r1, 0); + assertRightPlace(r4, 1); + assertRightPlace(r5, 2); + assertRightPlace(r6, 3); + assertRelationMetadataOrder(project1, isProjectOfPerson, List.of(r1, r4, r5, r6)); + } + + @Test + public void changeRightItemInNonUseForPlaceRelationshipAtTheStartWithSiblingsInOldRightItem() throws Exception { + context.turnOffAuthorisationSystem(); + + // Add three Authors to project1, appending to the end + Relationship r1 = relationshipService.create(context, author1, project1, isProjectOfPerson, -1, -1); + Relationship r2 = relationshipService.create(context, author2, project1, isProjectOfPerson, -1, -1); + Relationship r3 = relationshipService.create(context, author3, project1, isProjectOfPerson, -1, -1); + + // Add three Projects to author1, appending to the end + Relationship r4 = relationshipService.create(context, author1, project4, isProjectOfPerson, -1, -1); + Relationship r5 = relationshipService.create(context, author1, project5, isProjectOfPerson, -1, -1); + Relationship r6 = relationshipService.create(context, author1, project6, isProjectOfPerson, -1, -1); + + // Move r1 to project2 + relationshipService.move(context, r1, null, project2); + + context.restoreAuthSystemState(); + + // Check relationship order for project1 -> should remain unchanged + assertRightPlace(r2, 0); + assertRightPlace(r3, 1); + assertRelationMetadataOrder(project1, isProjectOfPerson, List.of(r2, r3)); + + // Check relationship order for author1 -> should remain unchanged + assertLeftPlace(r1, 0); + assertLeftPlace(r4, 1); + assertLeftPlace(r5, 2); + assertLeftPlace(r6, 3); + assertRelationMetadataOrder(author1, isProjectOfPerson, List.of(r1, r4, r5, r6)); + } + + + private void assertLeftPlace(Relationship relationship, int leftPlace) { + assertEquals(leftPlace, relationship.getLeftPlace()); + } + + private void assertRightPlace(Relationship relationship, int rightPlace) { + assertEquals(rightPlace, relationship.getRightPlace()); + } + + + private void assertRelationMetadataOrder( + Item item, RelationshipType relationshipType, List relationships + ) { + String element = getRelationshipTypeStringForEntity(relationshipType, item); + List mdvs = itemService.getMetadata( + item, + MetadataSchemaEnum.RELATION.getName(), element, null, + Item.ANY + ); + + assertEquals( + "Metadata authorities should match relationship IDs", + relationships.stream() + .map(r -> { + if (r != null) { + return Constants.VIRTUAL_AUTHORITY_PREFIX + r.getID(); + } else { + return null; // To match relationships that have been deleted and copied to MDVs + } + }) + .collect(Collectors.toList()), + mdvs.stream() + .map(MetadataValue::getAuthority) + .collect(Collectors.toList()) + ); + } + + private void assertMetadataOrder( + Item item, String metadataField, List metadataValues + ) { + List mdvs = itemService.getMetadataByMetadataString(item, metadataField); + + assertEquals( + metadataValues, + mdvs.stream() + .map(MetadataValue::getValue) + .collect(Collectors.toList()) + ); + } + + private String getRelationshipTypeStringForEntity(RelationshipType relationshipType, Item item) { + String entityType = relationshipMetadataService.getEntityTypeStringFromMetadata(item); + + if (StringUtils.equals(entityType, relationshipType.getLeftType().getLabel())) { + return relationshipType.getLeftwardType(); + } else if (StringUtils.equals(entityType, relationshipType.getRightType().getLabel())) { + return relationshipType.getRightwardType(); + } else { + throw new IllegalArgumentException( + entityType + "is not a valid entity for " + relationshipType.getLeftwardType() + ", must be either " + + relationshipType.getLeftType().getLabel() + " or " + relationshipType.getRightType().getLabel() + ); + } + } } diff --git a/dspace-api/src/test/java/org/dspace/content/RelationshipServiceImplTest.java b/dspace-api/src/test/java/org/dspace/content/RelationshipServiceImplTest.java index 5d6197e49460..49983bfe66e0 100644 --- a/dspace-api/src/test/java/org/dspace/content/RelationshipServiceImplTest.java +++ b/dspace-api/src/test/java/org/dspace/content/RelationshipServiceImplTest.java @@ -122,32 +122,6 @@ public void testFindByItem() throws Exception { } } - @Test - public void testFindLeftPlaceByLeftItem() throws Exception { - // Declare objects utilized in unit test - Item item = mock(Item.class); - - // Mock DAO to return mocked left place as 0 - when(relationshipDAO.findNextLeftPlaceByLeftItem(context, item)).thenReturn(0); - - // The left place reported from out mocked item should match the DAO's report of the left place - assertEquals("TestFindLeftPlaceByLeftItem 0", relationshipDAO.findNextLeftPlaceByLeftItem(context, item), - relationshipService.findNextLeftPlaceByLeftItem(context, item)); - } - - @Test - public void testFindRightPlaceByRightItem() throws Exception { - // Declare objects utilized in unit test - Item item = mock(Item.class); - - // Mock lower level DAO to return mocked right place as 0 - when(relationshipDAO.findNextRightPlaceByRightItem(context, item)).thenReturn(0); - - // The right place reported from out mocked item should match the DAO's report of the right place - assertEquals("TestFindRightPlaceByRightItem 0", relationshipDAO.findNextRightPlaceByRightItem(context, item), - relationshipService.findNextRightPlaceByRightItem(context, item)); - } - @Test public void testFindByItemAndRelationshipType() throws Exception { // Declare objects utilized in unit test diff --git a/dspace-api/src/test/java/org/dspace/content/dao/RelationshipDAOImplTest.java b/dspace-api/src/test/java/org/dspace/content/dao/RelationshipDAOImplTest.java index 2143090fcf9e..b6f5da6be065 100644 --- a/dspace-api/src/test/java/org/dspace/content/dao/RelationshipDAOImplTest.java +++ b/dspace-api/src/test/java/org/dspace/content/dao/RelationshipDAOImplTest.java @@ -138,28 +138,6 @@ public void testFindByItem() throws Exception { -1, -1, false)); } - /** - * Test findNextLeftPlaceByLeftItem should return 0 given our test left Item itemOne. - * - * @throws Exception - */ - @Test - public void testFindNextLeftPlaceByLeftItem() throws Exception { - assertEquals("TestNextLeftPlaceByLeftItem 0", 1, relationshipService.findNextLeftPlaceByLeftItem(context, - itemOne)); - } - - /** - * Test findNextRightPlaceByRightItem should return 0 given our test right Item itemTwo. - * - * @throws Exception - */ - @Test - public void testFindNextRightPlaceByRightItem() throws Exception { - assertEquals("TestNextRightPlaceByRightItem 0", 1, relationshipService.findNextRightPlaceByRightItem(context, - itemTwo)); - } - /** * Test findByRelationshipType should return our defined relationshipsList given our test RelationshipType * relationshipType diff --git a/dspace-api/src/test/java/org/dspace/content/service/ItemServiceTest.java b/dspace-api/src/test/java/org/dspace/content/service/ItemServiceTest.java new file mode 100644 index 000000000000..c66a6eed7669 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/content/service/ItemServiceTest.java @@ -0,0 +1,202 @@ +/** + * 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.content.service; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.fail; + +import java.sql.SQLException; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.Logger; +import org.dspace.AbstractUnitTest; +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.Item; +import org.dspace.content.MetadataValue; +import org.dspace.content.WorkspaceItem; +import org.dspace.content.factory.ContentServiceFactory; +import org.junit.Before; +import org.junit.Test; + +public class ItemServiceTest extends AbstractUnitTest { + private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(ItemServiceTest.class); + + protected RelationshipService relationshipService = ContentServiceFactory.getInstance().getRelationshipService(); + protected RelationshipTypeService relationshipTypeService = ContentServiceFactory.getInstance() + .getRelationshipTypeService(); + protected EntityTypeService entityTypeService = ContentServiceFactory.getInstance().getEntityTypeService(); + protected CommunityService communityService = ContentServiceFactory.getInstance().getCommunityService(); + protected CollectionService collectionService = ContentServiceFactory.getInstance().getCollectionService(); + protected ItemService itemService = ContentServiceFactory.getInstance().getItemService(); + protected InstallItemService installItemService = ContentServiceFactory.getInstance().getInstallItemService(); + protected WorkspaceItemService workspaceItemService = ContentServiceFactory.getInstance().getWorkspaceItemService(); + protected MetadataValueService metadataValueService = ContentServiceFactory.getInstance().getMetadataValueService(); + + Community community; + Collection col; + + Item item; + + String authorQualifier = "author"; + String contributorElement = "contributor"; + String dcSchema = "dc"; + + /** + * This method will be run before every test as per @Before. It will + * initialize resources required for the tests. + */ + @Before + @Override + public void init() { + super.init(); + try { + context.turnOffAuthorisationSystem(); + community = communityService.create(null, context); + + col = collectionService.create(context, community); + WorkspaceItem is = workspaceItemService.create(context, col, false); + WorkspaceItem authorIs = workspaceItemService.create(context, col, false); + + item = installItemService.installItem(context, is); + itemService.addMetadata(context, item, "dspace", "entity", "type", null, "Publication"); + + context.restoreAuthSystemState(); + } catch (AuthorizeException ex) { + log.error("Authorization Error in init", ex); + fail("Authorization Error in init: " + ex.getMessage()); + } catch (SQLException ex) { + log.error("SQL Error in init", ex); + fail("SQL Error in init: " + ex.getMessage()); + } + } + + @Test + public void InsertAndMoveMetadataShiftPlaceTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Here we add the first set of metadata to the item + itemService.addMetadata(context, item, dcSchema, contributorElement, authorQualifier, null, "test, one"); + itemService.addMetadata(context, item, dcSchema, contributorElement, authorQualifier, null, "test, two"); + itemService.addMetadata(context, item, dcSchema, contributorElement, authorQualifier, null, "test, three"); + + context.restoreAuthSystemState(); + + // The code below performs the mentioned assertions to ensure the place is correct + List list = itemService + .getMetadata(item, dcSchema, contributorElement, authorQualifier, Item.ANY); + assertThat(list.size(), equalTo(3)); + + assertMetadataValue(authorQualifier, contributorElement, dcSchema, "test, one", null, 0, list.get(0)); + assertMetadataValue(authorQualifier, contributorElement, dcSchema, "test, two", null, 1, list.get(1)); + assertMetadataValue(authorQualifier, contributorElement, dcSchema, "test, three", null, 2, list.get(2)); + + context.turnOffAuthorisationSystem(); + + // This is where we add metadata at place=1 + itemService.addAndShiftRightMetadata( + context, item, dcSchema, contributorElement, authorQualifier, null, "test, four", null, -1, 1 + ); + + // Here we retrieve the list of metadata again to perform the assertions on the places below as mentioned + list = itemService.getMetadata(item, dcSchema, contributorElement, authorQualifier, Item.ANY) + .stream() + .sorted(Comparator.comparingInt(MetadataValue::getPlace)) + .collect(Collectors.toList()); + assertThat(list.size(), equalTo(4)); + assertMetadataValue(authorQualifier, contributorElement, dcSchema, "test, one", null, 0, list.get(0)); + assertMetadataValue(authorQualifier, contributorElement, dcSchema, "test, four", null, 1, list.get(1)); + assertMetadataValue(authorQualifier, contributorElement, dcSchema, "test, two", null, 2, list.get(2)); + assertMetadataValue(authorQualifier, contributorElement, dcSchema, "test, three", null, 3, list.get(3)); + + // And move metadata from place=2 to place=0 + itemService.moveMetadata(context, item, dcSchema, contributorElement, authorQualifier, 2, 0); + + context.restoreAuthSystemState(); + + // Here we retrieve the list of metadata again to perform the assertions on the places below as mentioned + list = itemService.getMetadata(item, dcSchema, contributorElement, authorQualifier, Item.ANY) + .stream() + .sorted(Comparator.comparingInt(MetadataValue::getPlace)) + .collect(Collectors.toList()); + assertThat(list.size(), equalTo(4)); + assertMetadataValue(authorQualifier, contributorElement, dcSchema, "test, two", null, 0, list.get(0)); + assertMetadataValue(authorQualifier, contributorElement, dcSchema, "test, one", null, 1, list.get(1)); + assertMetadataValue(authorQualifier, contributorElement, dcSchema, "test, four", null, 2, list.get(2)); + assertMetadataValue(authorQualifier, contributorElement, dcSchema, "test, three", null, 3, list.get(3)); + } + + @Test + public void InsertAndMoveMetadataOnePlaceForwardTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Here we add the first set of metadata to the item + itemService.addMetadata(context, item, dcSchema, contributorElement, authorQualifier, null, "test, one"); + itemService.addMetadata(context, item, dcSchema, contributorElement, authorQualifier, null, "test, two"); + itemService.addMetadata(context, item, dcSchema, contributorElement, authorQualifier, null, "test, three"); + + context.restoreAuthSystemState(); + + // The code below performs the mentioned assertions to ensure the place is correct + List list = itemService + .getMetadata(item, dcSchema, contributorElement, authorQualifier, Item.ANY); + assertThat(list.size(), equalTo(3)); + + assertMetadataValue(authorQualifier, contributorElement, dcSchema, "test, one", null, 0, list.get(0)); + assertMetadataValue(authorQualifier, contributorElement, dcSchema, "test, two", null, 1, list.get(1)); + assertMetadataValue(authorQualifier, contributorElement, dcSchema, "test, three", null, 2, list.get(2)); + + context.turnOffAuthorisationSystem(); + + // This is where we add metadata at place=1 + itemService.addAndShiftRightMetadata( + context, item, dcSchema, contributorElement, authorQualifier, null, "test, four", null, -1, 1 + ); + + // Here we retrieve the list of metadata again to perform the assertions on the places below as mentioned + list = itemService.getMetadata(item, dcSchema, contributorElement, authorQualifier, Item.ANY) + .stream() + .sorted(Comparator.comparingInt(MetadataValue::getPlace)) + .collect(Collectors.toList()); + assertThat(list.size(), equalTo(4)); + assertMetadataValue(authorQualifier, contributorElement, dcSchema, "test, one", null, 0, list.get(0)); + assertMetadataValue(authorQualifier, contributorElement, dcSchema, "test, four", null, 1, list.get(1)); + assertMetadataValue(authorQualifier, contributorElement, dcSchema, "test, two", null, 2, list.get(2)); + assertMetadataValue(authorQualifier, contributorElement, dcSchema, "test, three", null, 3, list.get(3)); + + // And move metadata from place=1 to place=2 + itemService.moveMetadata(context, item, dcSchema, contributorElement, authorQualifier, 1, 2); + + context.restoreAuthSystemState(); + + // Here we retrieve the list of metadata again to perform the assertions on the places below as mentioned + list = itemService.getMetadata(item, dcSchema, contributorElement, authorQualifier, Item.ANY) + .stream() + .sorted(Comparator.comparingInt(MetadataValue::getPlace)) + .collect(Collectors.toList()); + assertThat(list.size(), equalTo(4)); + assertMetadataValue(authorQualifier, contributorElement, dcSchema, "test, one", null, 0, list.get(0)); + assertMetadataValue(authorQualifier, contributorElement, dcSchema, "test, two", null, 1, list.get(1)); + assertMetadataValue(authorQualifier, contributorElement, dcSchema, "test, four", null, 2, list.get(2)); + assertMetadataValue(authorQualifier, contributorElement, dcSchema, "test, three", null, 3, list.get(3)); + } + + private void assertMetadataValue(String authorQualifier, String contributorElement, String dcSchema, String value, + String authority, int place, MetadataValue metadataValue) { + assertThat(metadataValue.getValue(), equalTo(value)); + assertThat(metadataValue.getMetadataField().getMetadataSchema().getName(), equalTo(dcSchema)); + assertThat(metadataValue.getMetadataField().getElement(), equalTo(contributorElement)); + assertThat(metadataValue.getMetadataField().getQualifier(), equalTo(authorQualifier)); + assertThat(metadataValue.getAuthority(), equalTo(authority)); + assertThat(metadataValue.getPlace(), equalTo(place)); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/RelationshipRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/RelationshipRestRepository.java index d1483a6d7ab5..f238593aba55 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/RelationshipRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/RelationshipRestRepository.java @@ -165,25 +165,21 @@ public RelationshipRest put(Context context, String contextPath, Integer id, Lis } List dSpaceObjects = utils.constructDSpaceObjectList(context, stringList); if (dSpaceObjects.size() == 1 && dSpaceObjects.get(0).getType() == Constants.ITEM) { - Item replacementItemInRelationship = (Item) dSpaceObjects.get(0); - Item leftItem; - Item rightItem; + Item newLeftItem; + Item newRightItem; + if (itemToReplaceIsRight) { - leftItem = relationship.getLeftItem(); - rightItem = replacementItemInRelationship; + newLeftItem = null; + newRightItem = replacementItemInRelationship; } else { - leftItem = replacementItemInRelationship; - rightItem = relationship.getRightItem(); + newLeftItem = replacementItemInRelationship; + newRightItem = null; } - if (isAllowedToModifyRelationship(context, relationship, leftItem, rightItem)) { - relationship.setLeftItem(leftItem); - relationship.setRightItem(rightItem); - + if (isAllowedToModifyRelationship(context, relationship, newLeftItem, newRightItem)) { try { - relationshipService.updatePlaceInRelationship(context, relationship); - relationshipService.update(context, relationship); + relationshipService.move(context, relationship, newLeftItem, newRightItem); context.commit(); context.reloadEntity(relationship); } catch (AuthorizeException e) { @@ -242,15 +238,17 @@ protected RelationshipRest put(Context context, HttpServletRequest request, Stri relationship.setLeftwardValue(relationshipRest.getLeftwardValue()); relationship.setRightwardValue(relationshipRest.getRightwardValue()); + Integer newRightPlace = null; + Integer newLeftPlace = null; if (jsonNode.hasNonNull("rightPlace")) { - relationship.setRightPlace(relationshipRest.getRightPlace()); + newRightPlace = relationshipRest.getRightPlace(); } if (jsonNode.hasNonNull("leftPlace")) { - relationship.setLeftPlace(relationshipRest.getLeftPlace()); + newLeftPlace = relationshipRest.getLeftPlace(); } - relationshipService.update(context, relationship); + relationshipService.move(context, relationship, newLeftPlace, newRightPlace); context.commit(); context.reloadEntity(relationship); @@ -272,10 +270,13 @@ protected RelationshipRest put(Context context, HttpServletRequest request, Stri */ private boolean isAllowedToModifyRelationship(Context context, Relationship relationship, Item leftItem, Item rightItem) throws SQLException { - return (authorizeService.authorizeActionBoolean(context, leftItem, Constants.WRITE) || - authorizeService.authorizeActionBoolean(context, rightItem, Constants.WRITE)) && - (authorizeService.authorizeActionBoolean(context, relationship.getLeftItem(), Constants.WRITE) || - authorizeService.authorizeActionBoolean(context, relationship.getRightItem(), Constants.WRITE) + return ( + // Authorized to write new Items (if specified) + (leftItem == null || authorizeService.authorizeActionBoolean(context, leftItem, Constants.WRITE)) + || (rightItem == null || authorizeService.authorizeActionBoolean(context, rightItem, Constants.WRITE)) + // Authorized to write old Items + && (authorizeService.authorizeActionBoolean(context, relationship.getLeftItem(), Constants.WRITE) + || authorizeService.authorizeActionBoolean(context, relationship.getRightItem(), Constants.WRITE)) ); } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RelationshipRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RelationshipRestRepositoryIT.java index b64a5c95a8c6..bea3d2460fdd 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RelationshipRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RelationshipRestRepositoryIT.java @@ -23,11 +23,13 @@ import static org.junit.Assert.assertNull; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -41,6 +43,8 @@ import org.dspace.app.rest.matcher.PageMatcher; import org.dspace.app.rest.matcher.RelationshipMatcher; import org.dspace.app.rest.model.RelationshipRest; +import org.dspace.app.rest.model.patch.AddOperation; +import org.dspace.app.rest.model.patch.Operation; import org.dspace.app.rest.test.AbstractEntityIntegrationTest; import org.dspace.authorize.service.AuthorizeService; import org.dspace.builder.CollectionBuilder; @@ -151,6 +155,8 @@ public void setUp() throws Exception { .withTitle("Author2") .withIssueDate("2016-02-13") .withAuthor("Smith, Maria") + .withPersonIdentifierLastName("Smith") + .withPersonIdentifierFirstName("Maria") .withEntityType("Person") .build(); @@ -520,6 +526,80 @@ public void createRelationshipWithRightwardValueAndLeftWardValue() throws Except } } + @Test + public void createMultipleRelationshipsAppendToEndTest() throws Exception { + context.turnOffAuthorisationSystem(); + + authorizeService.addPolicy(context, publication1, Constants.WRITE, user1); + authorizeService.addPolicy(context, author1, Constants.WRITE, user1); + authorizeService.addPolicy(context, author2, Constants.WRITE, user1); + + context.setCurrentUser(user1); + context.restoreAuthSystemState(); + + + AtomicReference idRef = new AtomicReference<>(); + AtomicReference idRef2 = new AtomicReference<>(); + try { + String token = getAuthToken(user1.getEmail(), password); + + // Add a relationship @ leftPlace 2 + getClient(token).perform(post("/api/core/relationships") + .param("relationshipType", + isAuthorOfPublicationRelationshipType.getID() + .toString()) + .contentType(MediaType.parseMediaType + (org.springframework.data.rest.webmvc.RestMediaTypes + .TEXT_URI_LIST_VALUE)) + .content( + "https://localhost:8080/server/api/core/items/" + publication1 + .getID() + "\n" + + "https://localhost:8080/server/api/core/items/" + author1 + .getID())) + .andExpect(status().isCreated()) + .andDo(result -> idRef.set(read(result.getResponse().getContentAsString(), "$.id"))); + + getClient().perform(get("/api/core/relationships/" + idRef)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(idRef.get()))) + .andExpect(jsonPath("$.leftPlace", is(1))); + + getClient(token).perform(post("/api/core/relationships") + .param("relationshipType", + isAuthorOfPublicationRelationshipType.getID() + .toString()) + .contentType(MediaType.parseMediaType + (org.springframework.data.rest.webmvc.RestMediaTypes + .TEXT_URI_LIST_VALUE)) + .content( + "https://localhost:8080/server/api/core/items/" + publication1 + .getID() + "\n" + + "https://localhost:8080/server/api/core/items/" + author2 + .getID())) + .andExpect(status().isCreated()) + .andDo(result -> idRef2.set(read(result.getResponse().getContentAsString(), "$.id"))); + + getClient().perform(get("/api/core/relationships/" + idRef2)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(idRef2.get()))) + .andExpect(jsonPath("$.leftPlace", is(2))); + + // Check Item author order + getClient().perform(get("/api/core/items/" + publication1.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.metadata", allOf( + matchMetadata("dc.contributor.author", "Testy, TEst", 0), + matchMetadata("dc.contributor.author", "Smith, Donald", 1), + matchMetadata("dc.contributor.author", "Smith, Maria", 2) + ))); + } finally { + RelationshipBuilder.deleteRelationship(idRef.get()); + if (idRef2.get() != null) { + RelationshipBuilder.deleteRelationship(idRef2.get()); + } + } + } + @Test public void createRelationshipAndAddLeftWardValueAfterwards() throws Exception { context.turnOffAuthorisationSystem(); @@ -2516,6 +2596,104 @@ public void putRelationshipWithJson() throws Exception { } + @Test + public void putRelationshipWithJsonMoveInFrontOtherMetadata() throws Exception { + + String token = getAuthToken(admin.getEmail(), password); + Integer idRef = null; + Integer idRef2 = null; + try { + // Add a relationship + MvcResult mvcResult = getClient(token) + .perform(post("/api/core/relationships") + .param("relationshipType", isAuthorOfPublicationRelationshipType.getID().toString()) + .contentType(MediaType.parseMediaType( + org.springframework.data.rest.webmvc.RestMediaTypes.TEXT_URI_LIST_VALUE)) + .content( + "https://localhost:8080/server/api/core/items/" + publication1.getID() + "\n" + + "https://localhost:8080/server/api/core/items/" + author1.getID())) + .andExpect(status().isCreated()) + .andReturn(); + + ObjectMapper mapper = new ObjectMapper(); + String content = mvcResult.getResponse().getContentAsString(); + Map map = mapper.readValue(content, Map.class); + String id = String.valueOf(map.get("id")); + idRef = Integer.parseInt(id); + + // Add some more metadata + List ops = new ArrayList(); + ops.add(new AddOperation("/metadata/dc.contributor.author/-", "Metadata, First")); + ops.add(new AddOperation("/metadata/dc.contributor.author/-", "Metadata, Second")); + + getClient(token).perform(patch("/api/core/items/" + publication1.getID()) + .content(getPatchContent(ops)) + .contentType(javax.ws.rs.core.MediaType.APPLICATION_JSON_PATCH_JSON)); + + // Add another relationship + mvcResult = getClient(token) + .perform(post("/api/core/relationships") + .param("relationshipType", isAuthorOfPublicationRelationshipType.getID().toString()) + .contentType(MediaType.parseMediaType( + org.springframework.data.rest.webmvc.RestMediaTypes.TEXT_URI_LIST_VALUE)) + .content( + "https://localhost:8080/server/api/core/items/" + publication1.getID() + "\n" + + "https://localhost:8080/server/api/core/items/" + author2.getID())) + .andExpect(status().isCreated()) + .andReturn(); + + content = mvcResult.getResponse().getContentAsString(); + map = mapper.readValue(content, Map.class); + id = String.valueOf(map.get("id")); + idRef2 = Integer.parseInt(id); + + // Check Item author order + getClient().perform(get("/api/core/items/" + publication1.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.metadata", allOf( + matchMetadata("dc.contributor.author", "Testy, TEst", 0), + matchMetadata("dc.contributor.author", "Smith, Donald", 1), // first relationship + matchMetadata("dc.contributor.author", "Metadata, First", 2), + matchMetadata("dc.contributor.author", "Metadata, Second", 3), + matchMetadata("dc.contributor.author", "Smith, Maria", 4) // second relationship + ))); + + RelationshipRest relationshipRest = new RelationshipRest(); + relationshipRest.setLeftPlace(0); + relationshipRest.setRightPlace(1); + relationshipRest.setLeftwardValue(null); + relationshipRest.setRightwardValue(null); + + // Modify the place of the second relationship -> put it in front of all other metadata + getClient(token).perform(put("/api/core/relationships/" + idRef2) + .contentType(contentType) + .content(mapper.writeValueAsBytes(relationshipRest))) + .andExpect(status().isOk()); + + // Verify the place has changed to the new value + getClient(token).perform(get("/api/core/relationships/" + idRef2)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.leftPlace", is(0))) + .andExpect(jsonPath("$.rightPlace", is(1))); + + // Verify the other metadata have moved back + getClient().perform(get("/api/core/items/" + publication1.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.metadata", allOf( + matchMetadata("dc.contributor.author", "Smith, Maria", 0), // second relationship + matchMetadata("dc.contributor.author", "Testy, TEst", 1), + matchMetadata("dc.contributor.author", "Smith, Donald", 2), // first relationship + matchMetadata("dc.contributor.author", "Metadata, First", 3), + matchMetadata("dc.contributor.author", "Metadata, Second", 4) + ))); + + } finally { + RelationshipBuilder.deleteRelationship(idRef); + RelationshipBuilder.deleteRelationship(idRef2); + } + + } + @Test public void orgUnitAndOrgUnitRelationshipVirtualMetadataTest() throws Exception { context.turnOffAuthorisationSystem(); From e4857c0b1df05d95158b86b35f8f944b0e9b4a83 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Fri, 11 Feb 2022 18:34:25 +0100 Subject: [PATCH 0020/1846] [CST-5288] Introduced spring boot actuators --- dspace-server-webapp/pom.xml | 6 +++ .../configuration/ActuatorConfiguration.java | 39 +++++++++++++++++++ .../security/WebSecurityConfiguration.java | 2 +- .../src/main/resources/application.properties | 4 ++ 4 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/configuration/ActuatorConfiguration.java diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index e34d3ad1bc6f..6b703899ed18 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -279,6 +279,12 @@ spring-boot-starter-aop ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter-actuator + ${spring-boot.version} + com.flipkart.zjsonpatch diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/configuration/ActuatorConfiguration.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/configuration/ActuatorConfiguration.java new file mode 100644 index 000000000000..f0d78edd2f7b --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/configuration/ActuatorConfiguration.java @@ -0,0 +1,39 @@ +/** + * 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.rest.configuration; + +import java.util.Arrays; + +import org.dspace.app.rest.DiscoverableEndpointsService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.EventListener; +import org.springframework.hateoas.Link; + +/** + * Configuration class related to the actuator endpoints. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +@Configuration +public class ActuatorConfiguration { + + @Autowired + private DiscoverableEndpointsService discoverableEndpointsService; + + @Value("${management.endpoints.web.base-path:/actuator}") + private String actuatorBasePath; + + @EventListener(ApplicationReadyEvent.class) + public void registerActuatorEndpoints() { + discoverableEndpointsService.register(this, Arrays.asList(new Link(actuatorBasePath, "actuator"))); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java index 38e8c1d7f4db..4696cf9d5fec 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java @@ -81,7 +81,7 @@ protected void configure(HttpSecurity http) throws Exception { // Configure authentication requirements for ${dspace.server.url}/api/ URL only // NOTE: REST API is hardcoded to respond on /api/. Other modules (OAI, SWORD, IIIF, etc) use other root paths. http.requestMatchers() - .antMatchers("/api/**", "/iiif/**") + .antMatchers("/api/**", "/iiif/**", "/actuator/**") .and() // Enable Spring Security authorization on these paths .authorizeRequests() diff --git a/dspace-server-webapp/src/main/resources/application.properties b/dspace-server-webapp/src/main/resources/application.properties index c695fe2fbae6..789de70bcc08 100644 --- a/dspace-server-webapp/src/main/resources/application.properties +++ b/dspace-server-webapp/src/main/resources/application.properties @@ -132,3 +132,7 @@ spring.servlet.multipart.max-file-size = 512MB # Maximum size of a multipart request (i.e. max total size of all files in one request) (default = 10MB) spring.servlet.multipart.max-request-size = 512MB + +#Spring Boot actuator configuration +management.endpoint.health.show-details = when-authorized +management.endpoint.health.roles = ADMIN From 980359a0a93035ac00e1d85bad91a606357f1055 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Mon, 14 Feb 2022 13:44:45 +0100 Subject: [PATCH 0021/1846] [CST-5288] Added custom health indicator to check GeoIP configuration status --- .../org/dspace/statistics/GeoIpService.java | 57 +++++++++++++++ .../statistics/SolrLoggerServiceImpl.java | 27 ++----- .../config/spring/api/solr-services.xml | 2 + .../configuration/ActuatorConfiguration.java | 18 +++++ .../app/rest/health/GeoIpHealthIndicator.java | 40 ++++++++++ .../src/main/resources/application.properties | 4 - .../dspace/app/rest/HealthIndicatorsIT.java | 72 ++++++++++++++++++ .../rest/health/GeoIpHealthIndicatorTest.java | 73 +++++++++++++++++++ .../link/search/HealthIndicatorMatcher.java | 46 ++++++++++++ dspace/config/dspace.cfg | 13 ++++ dspace/config/spring/api/solr-services.xml | 2 + 11 files changed, 329 insertions(+), 25 deletions(-) create mode 100644 dspace-api/src/main/java/org/dspace/statistics/GeoIpService.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/health/GeoIpHealthIndicator.java create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/HealthIndicatorsIT.java create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/health/GeoIpHealthIndicatorTest.java create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/link/search/HealthIndicatorMatcher.java diff --git a/dspace-api/src/main/java/org/dspace/statistics/GeoIpService.java b/dspace-api/src/main/java/org/dspace/statistics/GeoIpService.java new file mode 100644 index 000000000000..7f8a11e5ba13 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/statistics/GeoIpService.java @@ -0,0 +1,57 @@ +/** + * 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.statistics; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + +import com.maxmind.geoip2.DatabaseReader; +import org.apache.commons.lang3.StringUtils; +import org.dspace.services.ConfigurationService; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Service that handle the GeoIP database file. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +public class GeoIpService { + + @Autowired + private ConfigurationService configurationService; + + /** + * Returns an instance of {@link DatabaseReader} based on the configured db + * file, if any. + * + * @return the Database reader + * @throws IllegalStateException if the db file is not configured correctly + */ + public DatabaseReader getDatabaseReader() throws IllegalStateException { + String dbPath = configurationService.getProperty("usage-statistics.dbfile"); + if (StringUtils.isBlank(dbPath)) { + throw new IllegalStateException("The required 'dbfile' configuration is missing in solr-statistics.cfg!"); + } + + try { + File dbFile = new File(dbPath); + return new DatabaseReader.Builder(dbFile).build(); + } catch (FileNotFoundException fe) { + throw new IllegalStateException( + "The GeoLite Database file is missing (" + dbPath + ")! Solr Statistics cannot generate location " + + "based reports! Please see the DSpace installation instructions for instructions to install " + + "this file.",fe); + } catch (IOException e) { + throw new IllegalStateException( + "Unable to load GeoLite Database file (" + dbPath + ")! You may need to reinstall it. See the " + + "DSpace installation instructions for more details.", e); + } + } +} diff --git a/dspace-api/src/main/java/org/dspace/statistics/SolrLoggerServiceImpl.java b/dspace-api/src/main/java/org/dspace/statistics/SolrLoggerServiceImpl.java index 9cc032a998b9..4b2ae94e758e 100644 --- a/dspace-api/src/main/java/org/dspace/statistics/SolrLoggerServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/statistics/SolrLoggerServiceImpl.java @@ -8,7 +8,6 @@ package org.dspace.statistics; import java.io.File; -import java.io.FileNotFoundException; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; @@ -142,6 +141,8 @@ public class SolrLoggerServiceImpl implements SolrLoggerService, InitializingBea private ClientInfoService clientInfoService; @Autowired private SolrStatisticsCore solrStatisticsCore; + @Autowired + private GeoIpService geoIpService; /** URL to the current-year statistics core. Prior-year shards will have a year suffixed. */ private String statisticsCoreURL; @@ -179,26 +180,10 @@ public void afterPropertiesSet() throws Exception { //spiderIps = SpiderDetector.getSpiderIpAddresses(); DatabaseReader service = null; - // Get the db file for the location - String dbPath = configurationService.getProperty("usage-statistics.dbfile"); - if (dbPath != null) { - try { - File dbFile = new File(dbPath); - service = new DatabaseReader.Builder(dbFile).build(); - } catch (FileNotFoundException fe) { - log.error( - "The GeoLite Database file is missing (" + dbPath + ")! Solr Statistics cannot generate location " + - "based reports! Please see the DSpace installation instructions for instructions to install " + - "this file.", - fe); - } catch (IOException e) { - log.error( - "Unable to load GeoLite Database file (" + dbPath + ")! You may need to reinstall it. See the " + - "DSpace installation instructions for more details.", - e); - } - } else { - log.error("The required 'dbfile' configuration is missing in solr-statistics.cfg!"); + try { + service = geoIpService.getDatabaseReader(); + } catch (IllegalStateException ex) { + log.error(ex); } locationService = service; } diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/solr-services.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/solr-services.xml index 5f86c7359890..32ab90b2cc61 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/solr-services.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/solr-services.xml @@ -47,5 +47,7 @@ + + diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/configuration/ActuatorConfiguration.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/configuration/ActuatorConfiguration.java index f0d78edd2f7b..cb91ae4152b1 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/configuration/ActuatorConfiguration.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/configuration/ActuatorConfiguration.java @@ -10,9 +10,14 @@ import java.util.Arrays; import org.dspace.app.rest.DiscoverableEndpointsService; +import org.dspace.app.rest.health.GeoIpHealthIndicator; +import org.dspace.discovery.SolrSearchCore; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; +import org.springframework.boot.actuate.solr.SolrHealthIndicator; import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.event.EventListener; import org.springframework.hateoas.Link; @@ -36,4 +41,17 @@ public class ActuatorConfiguration { public void registerActuatorEndpoints() { discoverableEndpointsService.register(this, Arrays.asList(new Link(actuatorBasePath, "actuator"))); } + + @Bean + @ConditionalOnEnabledHealthIndicator("solr") + public SolrHealthIndicator solrHealthIndicator(SolrSearchCore solrSearchCore) { + return new SolrHealthIndicator(solrSearchCore.getSolr()); + } + + @Bean + @ConditionalOnEnabledHealthIndicator("geoIp") + public GeoIpHealthIndicator geoIpHealthIndicator() { + return new GeoIpHealthIndicator(); + } + } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/health/GeoIpHealthIndicator.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/health/GeoIpHealthIndicator.java new file mode 100644 index 000000000000..5c8ad92dbb82 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/health/GeoIpHealthIndicator.java @@ -0,0 +1,40 @@ +/** + * 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.rest.health; + +import org.dspace.statistics.GeoIpService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.health.AbstractHealthIndicator; +import org.springframework.boot.actuate.health.Health.Builder; +import org.springframework.boot.actuate.health.HealthIndicator; + +/** + * Implementation of {@link HealthIndicator} that verifies if the GeoIP database + * is configured correctly. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +public class GeoIpHealthIndicator extends AbstractHealthIndicator { + + @Autowired + private GeoIpService geoIpService; + + @Override + protected void doHealthCheck(Builder builder) throws Exception { + + try { + geoIpService.getDatabaseReader(); + builder.up(); + } catch (IllegalStateException ex) { + builder.outOfService().withDetail("reason", ex.getMessage()); + } + + } + +} diff --git a/dspace-server-webapp/src/main/resources/application.properties b/dspace-server-webapp/src/main/resources/application.properties index 789de70bcc08..c695fe2fbae6 100644 --- a/dspace-server-webapp/src/main/resources/application.properties +++ b/dspace-server-webapp/src/main/resources/application.properties @@ -132,7 +132,3 @@ spring.servlet.multipart.max-file-size = 512MB # Maximum size of a multipart request (i.e. max total size of all files in one request) (default = 10MB) spring.servlet.multipart.max-request-size = 512MB - -#Spring Boot actuator configuration -management.endpoint.health.show-details = when-authorized -management.endpoint.health.roles = ADMIN diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/HealthIndicatorsIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/HealthIndicatorsIT.java new file mode 100644 index 000000000000..c28e172e61ad --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/HealthIndicatorsIT.java @@ -0,0 +1,72 @@ +/** + * 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.rest; + +import static org.dspace.app.rest.link.search.HealthIndicatorMatcher.match; +import static org.dspace.app.rest.link.search.HealthIndicatorMatcher.matchDb; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.Map; + +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.junit.Test; +import org.springframework.boot.actuate.health.Status; + +/** + * Integration tests to verify the health indicators configuration. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +public class HealthIndicatorsIT extends AbstractControllerIntegrationTest { + + private static final String HEALTH_PATH = "/actuator/health"; + + @Test + public void testWithAnonymousUser() throws Exception { + + getClient().perform(get(HEALTH_PATH)) + .andExpect(status().isServiceUnavailable()) + .andExpect(jsonPath("$.status", is(Status.OUT_OF_SERVICE.getCode()))) + .andExpect(jsonPath("$.components").doesNotExist()); + + } + + @Test + public void testWithNotAdminUser() throws Exception { + + String token = getAuthToken(eperson.getEmail(), password); + + getClient(token).perform(get(HEALTH_PATH)) + .andExpect(status().isServiceUnavailable()) + .andExpect(jsonPath("$.status", is(Status.OUT_OF_SERVICE.getCode()))) + .andExpect(jsonPath("$.components").doesNotExist()); + + } + + @Test + public void testWithAdminUser() throws Exception { + + String token = getAuthToken(admin.getEmail(), password); + + getClient(token).perform(get(HEALTH_PATH)) + .andExpect(status().isServiceUnavailable()) + .andExpect(jsonPath("$.status", is(Status.OUT_OF_SERVICE.getCode()))) + .andExpect(jsonPath("$.components", allOf( + matchDb(Status.UP), + match("solr", Status.UP, Map.of("status", 0, "detectedPathType", "root")), + match("geoIp", Status.OUT_OF_SERVICE, + Map.of("reason", "The required 'dbfile' configuration is missing in solr-statistics.cfg!")) + ))); + + } +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/health/GeoIpHealthIndicatorTest.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/health/GeoIpHealthIndicatorTest.java new file mode 100644 index 000000000000..ba454a8f1eea --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/health/GeoIpHealthIndicatorTest.java @@ -0,0 +1,73 @@ +/** + * 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.rest.health; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.anEmptyMap; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.when; + +import java.util.Map; + +import com.maxmind.geoip2.DatabaseReader; +import org.dspace.statistics.GeoIpService; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.Status; + +/** + * Unit tests for {@link GeoIpHealthIndicator}. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +@RunWith(MockitoJUnitRunner.class) +public class GeoIpHealthIndicatorTest { + + @Mock + private GeoIpService geoIpService; + + @InjectMocks + private GeoIpHealthIndicator geoIpHealthIndicator; + + @Mock + private DatabaseReader databaseReader; + + @Test + public void testWithGeoIpConfiguredCorrectly() { + when(geoIpService.getDatabaseReader()).thenReturn(databaseReader); + + Health health = geoIpHealthIndicator.health(); + + assertThat(health.getStatus(), is(Status.UP)); + assertThat(health.getDetails(), anEmptyMap()); + } + + @Test + public void testWithGeoIpWrongConfiguration() { + when(geoIpService.getDatabaseReader()).thenThrow(new IllegalStateException("Missing db file")); + + Health health = geoIpHealthIndicator.health(); + + assertThat(health.getStatus(), is(Status.OUT_OF_SERVICE)); + assertThat(health.getDetails(), is(Map.of("reason", "Missing db file"))); + } + + @Test + public void testWithUnexpectedError() { + when(geoIpService.getDatabaseReader()).thenThrow(new RuntimeException("Generic error")); + + Health health = geoIpHealthIndicator.health(); + + assertThat(health.getStatus(), is(Status.DOWN)); + } +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/link/search/HealthIndicatorMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/link/search/HealthIndicatorMatcher.java new file mode 100644 index 000000000000..d2ee1e2c70ab --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/link/search/HealthIndicatorMatcher.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.rest.link.search; + +import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.is; + +import java.util.Map; + +import org.hamcrest.Matcher; +import org.springframework.boot.actuate.health.Status; + +/** + * Matcher for the health indicators. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +public final class HealthIndicatorMatcher { + + private HealthIndicatorMatcher() { + + } + + public static Matcher matchDb(Status status) { + return allOf( + hasJsonPath("$.db"), + hasJsonPath("$.db.status", is(status.getCode())), + hasJsonPath("$.db.components", allOf( + match("dspaceDataSource", status, Map.of("database", "H2", "result", 1, "validationQuery", "SELECT 1")), + match("dataSource", status, Map.of("database", "H2", "result", 1, "validationQuery", "SELECT 1"))))); + } + + public static Matcher match(String name, Status status, Map details) { + return allOf( + hasJsonPath("$." + name), + hasJsonPath("$." + name + ".status", is(status.getCode())), + hasJsonPath("$." + name + ".details", is(details))); + } +} diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index 02c618abf420..dcd4dc8e117c 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -1553,6 +1553,19 @@ mail.helpdesk.name = Help Desk request.item.helpdesk.override = false +#---------------------------------------------------------------# +#------------SPRING BOOT ACTUATOR CONFIGURATION-----------------# +#---------------------------------------------------------------# + +management.endpoint.health.show-details = when-authorized +management.endpoint.health.roles = ADMIN + +management.health.ping.enabled = false +management.health.diskSpace.enabled = false + +info.app.name = ${dspace.name} + + #------------------------------------------------------------------# #-------------------MODULE CONFIGURATIONS--------------------------# #------------------------------------------------------------------# diff --git a/dspace/config/spring/api/solr-services.xml b/dspace/config/spring/api/solr-services.xml index 698a82418422..80e9449d4c60 100644 --- a/dspace/config/spring/api/solr-services.xml +++ b/dspace/config/spring/api/solr-services.xml @@ -31,5 +31,7 @@ + + From a6fa1f17f70a6745f3ffd34b94654fa0e2b5a113 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Mon, 14 Feb 2022 16:35:54 +0100 Subject: [PATCH 0022/1846] [CST-5288] Made the actuator security configuration dynamic --- .../dspace/app/rest/security/WebSecurityConfiguration.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java index 4696cf9d5fec..d054fcd9e71f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java @@ -11,6 +11,7 @@ import org.dspace.authenticate.service.AuthenticationService; import org.dspace.services.RequestService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; @@ -63,6 +64,9 @@ public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { @Autowired private DSpaceAccessDeniedHandler accessDeniedHandler; + @Value("${management.endpoints.web.base-path:/actuator}") + private String actuatorBasePath; + @Override public void configure(WebSecurity webSecurity) throws Exception { // Define URL patterns which Spring Security will ignore entirely. @@ -81,7 +85,7 @@ protected void configure(HttpSecurity http) throws Exception { // Configure authentication requirements for ${dspace.server.url}/api/ URL only // NOTE: REST API is hardcoded to respond on /api/. Other modules (OAI, SWORD, IIIF, etc) use other root paths. http.requestMatchers() - .antMatchers("/api/**", "/iiif/**", "/actuator/**") + .antMatchers("/api/**", "/iiif/**", actuatorBasePath + "/**") .and() // Enable Spring Security authorization on these paths .authorizeRequests() From 87d91c75bb73b7baf24ef453f014e5c60a3ece40 Mon Sep 17 00:00:00 2001 From: Antoine Snyers Date: Fri, 25 Feb 2022 15:44:29 +0100 Subject: [PATCH 0023/1846] Add ITs to verify all streams from retrieve() are closed --- .../app/rest/BitstreamRestControllerIT.java | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java index f07aae876f32..39931fc3ee10 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java @@ -24,6 +24,11 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.springframework.data.rest.webmvc.RestMediaTypes.TEXT_URI_LIST_VALUE; import static org.springframework.http.MediaType.parseMediaType; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -73,10 +78,13 @@ import org.dspace.statistics.SolrLoggerServiceImpl; import org.dspace.statistics.factory.StatisticsServiceFactory; import org.dspace.statistics.service.SolrLoggerService; +import org.dspace.storage.bitstore.factory.StorageServiceFactory; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.util.ReflectionTestUtils; /** * Integration test to test the /api/core/bitstreams/[id]/* endpoints @@ -968,4 +976,110 @@ public void updateBitstreamFormatAdmin() throws Exception { bitstreamService.getMetadataByMetadataString(bitstream, "dc.format") )); } + + + @Test + public void closeInputStreamsRegularDownload() throws Exception { + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community and one collections. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Collection col1 = + CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + + //2. A public item with a bitstream + String bitstreamContent = "0123456789"; + + try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) { + + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("Public item 1") + .withIssueDate("2017-10-17") + .withAuthor("Smith, Donald").withAuthor("Doe, John") + .build(); + + bitstream = BitstreamBuilder + .createBitstream(context, publicItem1, is) + .withName("Test bitstream") + .withDescription("This is a bitstream to test range requests") + .withMimeType("text/plain") + .build(); + } + context.restoreAuthSystemState(); + + var bitstreamStorageService = StorageServiceFactory.getInstance().getBitstreamStorageService(); + var inputStream = bitstreamStorageService.retrieve(context, bitstream); + var inputStreamSpy = spy(inputStream); + var bitstreamStorageServiceSpy = spy(bitstreamStorageService); + ReflectionTestUtils.setField(bitstreamService, "bitstreamStorageService", bitstreamStorageServiceSpy); + doReturn(inputStreamSpy).when(bitstreamStorageServiceSpy).retrieve(any(), eq(bitstream)); + + //** WHEN ** + //We download the bitstream + getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content")) + //** THEN ** + .andExpect(status().isOk()); + + Mockito.verify(bitstreamStorageServiceSpy, times(1)).retrieve(any(), eq(bitstream)); + Mockito.verify(inputStreamSpy, times(1)).close(); + } + + @Test + public void closeInputStreamsDownloadWithCoverPage() throws Exception { + configurationService.setProperty("citation-page.enable_globally", true); + citationDocumentService.afterPropertiesSet(); + context.turnOffAuthorisationSystem(); + + + //** GIVEN ** + //1. A community-collection structure with one parent community and one collections. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Collection col1 = + CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + + //2. A public item with a bitstream + File originalPdf = new File(testProps.getProperty("test.bitstream")); + + try (InputStream is = new FileInputStream(originalPdf)) { + + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("Public item citation cover page test 1") + .withIssueDate("2017-10-17") + .withAuthor("Smith, Donald").withAuthor("Doe, John") + .build(); + + bitstream = BitstreamBuilder + .createBitstream(context, publicItem1, is) + .withName("Test bitstream") + .withDescription("This is a bitstream to test the citation cover page.") + .withMimeType("application/pdf") + .build(); + } + context.restoreAuthSystemState(); + + var bitstreamStorageService = StorageServiceFactory.getInstance().getBitstreamStorageService(); + var inputStreamSpy = spy(bitstreamStorageService.retrieve(context, bitstream)); + var inputStreamSpy2 = spy(bitstreamStorageService.retrieve(context, bitstream)); + var bitstreamStorageServiceSpy = spy(bitstreamStorageService); + ReflectionTestUtils.setField(bitstreamService, "bitstreamStorageService", bitstreamStorageServiceSpy); + doReturn(inputStreamSpy, inputStreamSpy2).when(bitstreamStorageServiceSpy).retrieve(any(), eq(bitstream)); + + //** WHEN ** + //We download the bitstream + getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content")) + //** THEN ** + .andExpect(status().isOk()); + + Mockito.verify(bitstreamStorageServiceSpy, times(2)).retrieve(any(), eq(bitstream)); + Mockito.verify(inputStreamSpy, times(1)).close(); + Mockito.verify(inputStreamSpy2, times(1)).close(); + } + } From 3f9506e62307e7ea876c0ee590a6bbd03c146501 Mon Sep 17 00:00:00 2001 From: Antoine Snyers Date: Mon, 28 Feb 2022 09:54:03 +0100 Subject: [PATCH 0024/1846] Prevent creating the citation page twice --- .../java/org/dspace/curate/CitationPage.java | 4 +- .../CitationDocumentServiceImpl.java | 5 +- .../service/CitationDocumentService.java | 3 +- .../app/rest/BitstreamRestController.java | 16 ++---- .../app/rest/utils/BitstreamResource.java | 49 ++++++++++++++----- .../app/rest/BitstreamRestControllerIT.java | 6 +-- 6 files changed, 47 insertions(+), 36 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/curate/CitationPage.java b/dspace-api/src/main/java/org/dspace/curate/CitationPage.java index dbdd0701455d..1b267286a4de 100644 --- a/dspace-api/src/main/java/org/dspace/curate/CitationPage.java +++ b/dspace-api/src/main/java/org/dspace/curate/CitationPage.java @@ -7,6 +7,7 @@ */ package org.dspace.curate; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.sql.SQLException; @@ -154,7 +155,8 @@ protected void performItem(Item item) throws SQLException { try { //Create the cited document InputStream citedInputStream = - citationDocument.makeCitedDocument(Curator.curationContext(), bitstream).getLeft(); + new ByteArrayInputStream( + citationDocument.makeCitedDocument(Curator.curationContext(), bitstream).getLeft()); //Add the cited document to the approiate bundle this.addCitedPageToItem(citedInputStream, bundle, pBundle, dBundle, displayMap, item, bitstream); diff --git a/dspace-api/src/main/java/org/dspace/disseminate/CitationDocumentServiceImpl.java b/dspace-api/src/main/java/org/dspace/disseminate/CitationDocumentServiceImpl.java index d51a3dfc7f3d..c20961db7544 100644 --- a/dspace-api/src/main/java/org/dspace/disseminate/CitationDocumentServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/disseminate/CitationDocumentServiceImpl.java @@ -8,7 +8,6 @@ package org.dspace.disseminate; import java.awt.Color; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; @@ -297,7 +296,7 @@ public boolean canGenerateCitationVersion(Context context, Bitstream bitstream) } @Override - public Pair makeCitedDocument(Context context, Bitstream bitstream) + public Pair makeCitedDocument(Context context, Bitstream bitstream) throws IOException, SQLException, AuthorizeException { PDDocument document = new PDDocument(); PDDocument sourceDocument = new PDDocument(); @@ -318,7 +317,7 @@ public Pair makeCitedDocument(Context context, Bitstream bits document.save(out); byte[] data = out.toByteArray(); - return Pair.of(new ByteArrayInputStream(data), Long.valueOf(data.length)); + return Pair.of(data, Long.valueOf(data.length)); } } finally { diff --git a/dspace-api/src/main/java/org/dspace/disseminate/service/CitationDocumentService.java b/dspace-api/src/main/java/org/dspace/disseminate/service/CitationDocumentService.java index 4a59de3f5fe1..0566fc525c06 100644 --- a/dspace-api/src/main/java/org/dspace/disseminate/service/CitationDocumentService.java +++ b/dspace-api/src/main/java/org/dspace/disseminate/service/CitationDocumentService.java @@ -8,7 +8,6 @@ package org.dspace.disseminate.service; import java.io.IOException; -import java.io.InputStream; import java.sql.SQLException; import org.apache.commons.lang3.tuple.Pair; @@ -84,7 +83,7 @@ public interface CitationDocumentService { * @throws SQLException if database error * @throws AuthorizeException if authorization error */ - public Pair makeCitedDocument(Context context, Bitstream bitstream) + public Pair makeCitedDocument(Context context, Bitstream bitstream) throws IOException, SQLException, AuthorizeException; /** diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamRestController.java index 75d3c9886cf1..42f06639284e 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamRestController.java @@ -12,7 +12,6 @@ import static org.springframework.web.bind.annotation.RequestMethod.PUT; import java.io.IOException; -import java.io.InputStream; import java.sql.SQLException; import java.util.List; import java.util.UUID; @@ -22,7 +21,6 @@ import org.apache.catalina.connector.ClientAbortException; import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.Pair; import org.apache.logging.log4j.Logger; import org.dspace.app.rest.converter.ConverterService; import org.dspace.app.rest.exception.DSpaceBadRequestException; @@ -133,19 +131,12 @@ public ResponseEntity retrieve(@PathVariable UUID uuid, HttpServletResponse resp } try { - long filesize; - if (citationDocumentService.isCitationEnabledForBitstream(bit, context)) { - final Pair citedDocument = citationDocumentService.makeCitedDocument(context, bit); - filesize = citedDocument.getRight(); - citedDocument.getLeft().close(); - } else { - filesize = bit.getSizeBytes(); - } + long filesize = bit.getSizeBytes(); + var citationEnabledForBitstream = citationDocumentService.isCitationEnabledForBitstream(bit, context); HttpHeadersInitializer httpHeadersInitializer = new HttpHeadersInitializer() .withBufferSize(BUFFER_SIZE) .withFileName(name) - .withLength(filesize) .withChecksum(bit.getChecksum()) .withMimetype(mimetype) .with(request) @@ -162,10 +153,9 @@ public ResponseEntity retrieve(@PathVariable UUID uuid, HttpServletResponse resp } - org.dspace.app.rest.utils.BitstreamResource bitstreamResource = new org.dspace.app.rest.utils.BitstreamResource( - bit, name, uuid, filesize, currentUser != null ? currentUser.getID() : null); + bit, name, uuid, currentUser != null ? currentUser.getID() : null, citationEnabledForBitstream); //We have all the data we need, close the connection to the database so that it doesn't stay open during //download/streaming diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamResource.java index 5b8c54629c46..6de71fed59a2 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamResource.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamResource.java @@ -7,11 +7,14 @@ */ package org.dspace.app.rest.utils; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.sql.SQLException; import java.util.UUID; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.tuple.Pair; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Bitstream; import org.dspace.content.factory.ContentServiceFactory; @@ -35,8 +38,10 @@ public class BitstreamResource extends AbstractResource { private Bitstream bitstream; private String name; private UUID uuid; - private long sizeBytes; private UUID currentUserUUID; + private boolean shouldGenerateCoverPage; + private byte[] file; + private Long fileSize; private BitstreamService bitstreamService = ContentServiceFactory.getInstance().getBitstreamService(); private EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService(); @@ -44,12 +49,30 @@ public class BitstreamResource extends AbstractResource { new DSpace().getServiceManager() .getServicesByType(CitationDocumentService.class).get(0); - public BitstreamResource(Bitstream bitstream, String name, UUID uuid, long sizeBytes, UUID currentUserUUID) { + public BitstreamResource(Bitstream bitstream, String name, UUID uuid, UUID currentUserUUID, + boolean shouldGenerateCoverPage) { this.bitstream = bitstream; this.name = name; this.uuid = uuid; - this.sizeBytes = sizeBytes; this.currentUserUUID = currentUserUUID; + this.shouldGenerateCoverPage = shouldGenerateCoverPage; + } + + private Pair getFileData(Context context, Bitstream bitstream) throws SQLException, + AuthorizeException, IOException { + if (file == null || fileSize == null) { + if (shouldGenerateCoverPage) { + var citedDocument = citationDocumentService.makeCitedDocument(context, bitstream); + this.file = citedDocument.getLeft(); + this.fileSize = citedDocument.getRight(); + } else { + var inputStream = bitstreamService.retrieve(context, bitstream); + this.file = IOUtils.toByteArray(inputStream); + inputStream.close(); + this.fileSize = bitstream.getSizeBytes(); + } + } + return Pair.of(file, fileSize); } @Override @@ -63,15 +86,8 @@ public InputStream getInputStream() throws IOException { try { EPerson currentUser = ePersonService.find(context, currentUserUUID); context.setCurrentUser(currentUser); - InputStream out; - - if (citationDocumentService.isCitationEnabledForBitstream(bitstream, context)) { - out = citationDocumentService.makeCitedDocument(context, bitstream).getLeft(); - } else { - out = bitstreamService.retrieve(context, bitstream); - } - - return out; + Bitstream bitstream = bitstreamService.find(context, uuid); + return new ByteArrayInputStream(getFileData(context, bitstream).getLeft()); } catch (SQLException | AuthorizeException e) { throw new IOException(e); } finally { @@ -90,6 +106,13 @@ public String getFilename() { @Override public long contentLength() throws IOException { - return sizeBytes; + try (Context context = new Context()) { + EPerson currentUser = ePersonService.find(context, currentUserUUID); + context.setCurrentUser(currentUser); + Bitstream bitstream = bitstreamService.find(context, uuid); + return getFileData(context, bitstream).getRight(); + } catch (SQLException | AuthorizeException e) { + throw new IOException(e); + } } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java index 39931fc3ee10..fa02037f4a40 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java @@ -1066,10 +1066,9 @@ public void closeInputStreamsDownloadWithCoverPage() throws Exception { var bitstreamStorageService = StorageServiceFactory.getInstance().getBitstreamStorageService(); var inputStreamSpy = spy(bitstreamStorageService.retrieve(context, bitstream)); - var inputStreamSpy2 = spy(bitstreamStorageService.retrieve(context, bitstream)); var bitstreamStorageServiceSpy = spy(bitstreamStorageService); ReflectionTestUtils.setField(bitstreamService, "bitstreamStorageService", bitstreamStorageServiceSpy); - doReturn(inputStreamSpy, inputStreamSpy2).when(bitstreamStorageServiceSpy).retrieve(any(), eq(bitstream)); + doReturn(inputStreamSpy).when(bitstreamStorageServiceSpy).retrieve(any(), eq(bitstream)); //** WHEN ** //We download the bitstream @@ -1077,9 +1076,8 @@ public void closeInputStreamsDownloadWithCoverPage() throws Exception { //** THEN ** .andExpect(status().isOk()); - Mockito.verify(bitstreamStorageServiceSpy, times(2)).retrieve(any(), eq(bitstream)); + Mockito.verify(bitstreamStorageServiceSpy, times(1)).retrieve(any(), eq(bitstream)); Mockito.verify(inputStreamSpy, times(1)).close(); - Mockito.verify(inputStreamSpy2, times(1)).close(); } } From d0aab90ffc9ff43f604c992e737cff239e562f84 Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Tue, 1 Mar 2022 02:30:41 +0100 Subject: [PATCH 0025/1846] 88049: Add latest_version_status column to relationship table --- .../java/org/dspace/content/Relationship.java | 33 +++ .../content/RelationshipServiceImpl.java | 20 +- .../content/service/RelationshipService.java | 31 ++- ...on_status_column_to_relationship_table.sql | 10 + ...on_status_column_to_relationship_table.sql | 10 + ...on_status_column_to_relationship_table.sql | 10 + ...RelationshipServiceImplVersioningTest.java | 208 ++++++++++++++++++ 7 files changed, 315 insertions(+), 7 deletions(-) create mode 100644 dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.2_2022.02.28__add_last_version_status_column_to_relationship_table.sql create mode 100644 dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/oracle/V7.2_2022.02.28__add_last_version_status_column_to_relationship_table.sql create mode 100644 dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.2_2022.02.28__add_last_version_status_column_to_relationship_table.sql create mode 100644 dspace-api/src/test/java/org/dspace/content/RelationshipServiceImplVersioningTest.java diff --git a/dspace-api/src/main/java/org/dspace/content/Relationship.java b/dspace-api/src/main/java/org/dspace/content/Relationship.java index 81d13d6c1059..c42061dea87b 100644 --- a/dspace-api/src/main/java/org/dspace/content/Relationship.java +++ b/dspace-api/src/main/java/org/dspace/content/Relationship.java @@ -89,6 +89,15 @@ public class Relationship implements ReloadableEntity { @Column(name = "rightward_value") private String rightwardValue; + /** + * Whether the left and/or right side of a given relationship are the "latest". + * A side of a relationship is "latest" if the item on that side has either no other versions, + * or the item on that side is the most recent version that is relevant to the given relationship. + * This column affects what version of an item appears on search pages or the relationship listings of other items. + */ + @Column(name = "latest_version_status") + private LatestVersionStatus latestVersionStatus = LatestVersionStatus.BOTH; + /** * Protected constructor, create object using: * {@link org.dspace.content.service.RelationshipService#create(Context)} } @@ -216,6 +225,30 @@ public void setRightwardValue(String rightwardValue) { this.rightwardValue = rightwardValue; } + /** + * Getter for {@link #latestVersionStatus}. + * @return the latest version status of this relationship. + */ + public LatestVersionStatus getLatestVersionStatus() { + return latestVersionStatus; + } + + /** + * Setter for {@link #latestVersionStatus}. + * @param latestVersionStatus the new latest version status for this relationship. + */ + public void setLatestVersionStatus(LatestVersionStatus latestVersionStatus) { + this.latestVersionStatus = latestVersionStatus; + } + + public enum LatestVersionStatus { + // NOTE: SQL migration expects BOTH to be the first constant in this enum! + BOTH, // both items in this relationship are the "latest" + LEFT_ONLY, // the left-hand item of this relationship is the "latest", but the right-hand item is not + RIGHT_ONLY // the right-hand item of this relationship is the "latest", but the left-hand item is not + // NOTE: one side of any given relationship should ALWAYS be the "latest" + } + /** * Standard getter for the ID for this Relationship * @return The ID of this relationship diff --git a/dspace-api/src/main/java/org/dspace/content/RelationshipServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/RelationshipServiceImpl.java index 1c99878e81c5..f4f13410880c 100644 --- a/dspace-api/src/main/java/org/dspace/content/RelationshipServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/RelationshipServiceImpl.java @@ -20,6 +20,7 @@ import org.apache.logging.log4j.Logger; import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.service.AuthorizeService; +import org.dspace.content.Relationship.LatestVersionStatus; import org.dspace.content.dao.RelationshipDAO; import org.dspace.content.service.EntityTypeService; import org.dspace.content.service.ItemService; @@ -76,9 +77,10 @@ public Relationship create(Context c, Item leftItem, Item rightItem, Relationshi @Override - public Relationship create(Context c, Item leftItem, Item rightItem, RelationshipType relationshipType, - int leftPlace, int rightPlace, String leftwardValue, String rightwardValue) - throws AuthorizeException, SQLException { + public Relationship create( + Context c, Item leftItem, Item rightItem, RelationshipType relationshipType, int leftPlace, int rightPlace, + String leftwardValue, String rightwardValue, LatestVersionStatus latestVersionStatus + ) throws AuthorizeException, SQLException { Relationship relationship = new Relationship(); relationship.setLeftItem(leftItem); relationship.setRightItem(rightItem); @@ -87,9 +89,21 @@ public Relationship create(Context c, Item leftItem, Item rightItem, Relationshi relationship.setRightPlace(rightPlace); relationship.setLeftwardValue(leftwardValue); relationship.setRightwardValue(rightwardValue); + relationship.setLatestVersionStatus(latestVersionStatus); return create(c, relationship); } + @Override + public Relationship create( + Context c, Item leftItem, Item rightItem, RelationshipType relationshipType, int leftPlace, int rightPlace, + String leftwardValue, String rightwardValue + ) throws AuthorizeException, SQLException { + return create( + c, leftItem, rightItem, relationshipType, leftPlace, rightPlace, leftwardValue, rightwardValue, + LatestVersionStatus.BOTH + ); + } + @Override public Relationship create(Context context, Relationship relationship) throws SQLException, AuthorizeException { if (isRelationshipValidToCreate(context, relationship)) { diff --git a/dspace-api/src/main/java/org/dspace/content/service/RelationshipService.java b/dspace-api/src/main/java/org/dspace/content/service/RelationshipService.java index 2e0bb6f2be72..d64425132f8f 100644 --- a/dspace-api/src/main/java/org/dspace/content/service/RelationshipService.java +++ b/dspace-api/src/main/java/org/dspace/content/service/RelationshipService.java @@ -14,6 +14,7 @@ import org.dspace.authorize.AuthorizeException; import org.dspace.content.Item; import org.dspace.content.Relationship; +import org.dspace.content.Relationship.LatestVersionStatus; import org.dspace.content.RelationshipType; import org.dspace.core.Context; import org.dspace.service.DSpaceCRUDService; @@ -198,6 +199,27 @@ List findByRelationshipType(Context context, RelationshipType rela /** * This method is used to construct a Relationship object with all it's variables + * @param c The relevant DSpace context + * @param leftItem The leftItem Item object for the relationship + * @param rightItem The rightItem Item object for the relationship + * @param relationshipType The RelationshipType object for the relationship + * @param leftPlace The leftPlace integer for the relationship + * @param rightPlace The rightPlace integer for the relationship + * @param leftwardValue The leftwardValue string for the relationship + * @param rightwardValue The rightwardValue string for the relationship + * @param latestVersionStatus The latestVersionStatus value for the relationship + * @return The created Relationship object with the given properties + * @throws AuthorizeException If something goes wrong + * @throws SQLException If something goes wrong + */ + Relationship create( + Context c, Item leftItem, Item rightItem, RelationshipType relationshipType, int leftPlace, int rightPlace, + String leftwardValue, String rightwardValue, LatestVersionStatus latestVersionStatus + ) throws AuthorizeException, SQLException; + + /** + * This method is used to construct a Relationship object with all it's variables, + * except the latest version status * @param c The relevant DSpace context * @param leftItem The leftItem Item object for the relationship * @param rightItem The rightItem Item object for the relationship @@ -210,14 +232,15 @@ List findByRelationshipType(Context context, RelationshipType rela * @throws AuthorizeException If something goes wrong * @throws SQLException If something goes wrong */ - Relationship create(Context c, Item leftItem, Item rightItem, RelationshipType relationshipType, - int leftPlace, int rightPlace, String leftwardValue, String rightwardValue) - throws AuthorizeException, SQLException; + Relationship create( + Context c, Item leftItem, Item rightItem, RelationshipType relationshipType, int leftPlace, int rightPlace, + String leftwardValue, String rightwardValue + ) throws AuthorizeException, SQLException; /** * This method is used to construct a Relationship object with all it's variables, - * except the leftward and rightward labels + * except the leftward label, rightward label and latest version status * @param c The relevant DSpace context * @param leftItem The leftItem Item object for the relationship * @param rightItem The rightItem Item object for the relationship diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.2_2022.02.28__add_last_version_status_column_to_relationship_table.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.2_2022.02.28__add_last_version_status_column_to_relationship_table.sql new file mode 100644 index 000000000000..7bf3948d3a63 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.2_2022.02.28__add_last_version_status_column_to_relationship_table.sql @@ -0,0 +1,10 @@ +-- +-- 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/ +-- + +-- NOTE: default 0 ensures that existing relations have "latest_version_status" set to "both" (first constant in enum, see Relationship class) +ALTER TABLE relationship ADD COLUMN IF NOT EXISTS latest_version_status INTEGER DEFAULT 0 NOT NULL; diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/oracle/V7.2_2022.02.28__add_last_version_status_column_to_relationship_table.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/oracle/V7.2_2022.02.28__add_last_version_status_column_to_relationship_table.sql new file mode 100644 index 000000000000..3eb9ae6dd4f8 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/oracle/V7.2_2022.02.28__add_last_version_status_column_to_relationship_table.sql @@ -0,0 +1,10 @@ +-- +-- 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/ +-- + +-- NOTE: default 0 ensures that existing relations have "latest_version_status" set to "both" (first constant in enum, see Relationship class) +ALTER TABLE relationship ADD latest_version_status INTEGER DEFAULT 0 NOT NULL; diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.2_2022.02.28__add_last_version_status_column_to_relationship_table.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.2_2022.02.28__add_last_version_status_column_to_relationship_table.sql new file mode 100644 index 000000000000..7bf3948d3a63 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.2_2022.02.28__add_last_version_status_column_to_relationship_table.sql @@ -0,0 +1,10 @@ +-- +-- 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/ +-- + +-- NOTE: default 0 ensures that existing relations have "latest_version_status" set to "both" (first constant in enum, see Relationship class) +ALTER TABLE relationship ADD COLUMN IF NOT EXISTS latest_version_status INTEGER DEFAULT 0 NOT NULL; diff --git a/dspace-api/src/test/java/org/dspace/content/RelationshipServiceImplVersioningTest.java b/dspace-api/src/test/java/org/dspace/content/RelationshipServiceImplVersioningTest.java new file mode 100644 index 000000000000..a9e3bb04e723 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/content/RelationshipServiceImplVersioningTest.java @@ -0,0 +1,208 @@ +/** + * 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.content; + +import static org.junit.Assert.assertEquals; + +import org.dspace.AbstractIntegrationTestWithDatabase; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.EntityTypeBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.builder.RelationshipTypeBuilder; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.RelationshipService; +import org.junit.Before; +import org.junit.Test; + +public class RelationshipServiceImplVersioningTest extends AbstractIntegrationTestWithDatabase { + + private RelationshipService relationshipService; + + protected Community community; + protected Collection collection; + protected EntityType publicationEntityType; + protected EntityType personEntityType; + protected RelationshipType relationshipType; + protected Item publication1; + protected Item publication2; + protected Item publication3; + protected Item person1; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + + relationshipService = ContentServiceFactory.getInstance().getRelationshipService(); + + context.turnOffAuthorisationSystem(); + + community = CommunityBuilder.createCommunity(context) + .withName("community") + .build(); + + collection = CollectionBuilder.createCollection(context, community) + .withName("collection") + .build(); + + publicationEntityType = EntityTypeBuilder.createEntityTypeBuilder(context, "Publication") + .build(); + + personEntityType = EntityTypeBuilder.createEntityTypeBuilder(context, "Person") + .build(); + + relationshipType = RelationshipTypeBuilder.createRelationshipTypeBuilder( + context, publicationEntityType, personEntityType, + "isAuthorOfPublication", "isPublicationOfAuthor", + null, null, null, null + ) + .withCopyToLeft(false) + .withCopyToRight(false) + .build(); + + publication1 = ItemBuilder.createItem(context, collection) + .withTitle("publication1") + .withMetadata("dspace", "entity", "type", publicationEntityType.getLabel()) + .build(); + + publication2 = ItemBuilder.createItem(context, collection) + .withTitle("publication2") + .withMetadata("dspace", "entity", "type", publicationEntityType.getLabel()) + .build(); + + publication3 = ItemBuilder.createItem(context, collection) + .withTitle("publication3") + .withMetadata("dspace", "entity", "type", publicationEntityType.getLabel()) + .build(); + + person1 = ItemBuilder.createItem(context, collection) + .withTitle("person1") + .withMetadata("dspace", "entity", "type", personEntityType.getLabel()) + .build(); + + context.restoreAuthSystemState(); + } + + @Test + public void testRelationshipLatestVersionStatusDefault() throws Exception { + // create method #1 + context.turnOffAuthorisationSystem(); + Relationship relationship1 = relationshipService.create( + context, publication1, person1, relationshipType, 3, 5, "left", "right" + ); + context.restoreAuthSystemState(); + assertEquals(Relationship.LatestVersionStatus.BOTH, relationship1.getLatestVersionStatus()); + Relationship relationship2 = relationshipService.find(context, relationship1.getID()); + assertEquals(Relationship.LatestVersionStatus.BOTH, relationship2.getLatestVersionStatus()); + + // create method #2 + context.turnOffAuthorisationSystem(); + Relationship relationship3 = relationshipService.create( + context, publication2, person1, relationshipType, 3, 5 + ); + context.restoreAuthSystemState(); + assertEquals(Relationship.LatestVersionStatus.BOTH, relationship3.getLatestVersionStatus()); + Relationship relationship4 = relationshipService.find(context, relationship3.getID()); + assertEquals(Relationship.LatestVersionStatus.BOTH, relationship4.getLatestVersionStatus()); + + // create method #3 + Relationship inputRelationship = new Relationship(); + inputRelationship.setLeftItem(publication3); + inputRelationship.setRightItem(person1); + inputRelationship.setRelationshipType(relationshipType); + context.turnOffAuthorisationSystem(); + Relationship relationship5 = relationshipService.create(context, inputRelationship); + context.restoreAuthSystemState(); + assertEquals(Relationship.LatestVersionStatus.BOTH, relationship5.getLatestVersionStatus()); + Relationship relationship6 = relationshipService.find(context, relationship5.getID()); + assertEquals(Relationship.LatestVersionStatus.BOTH, relationship6.getLatestVersionStatus()); + } + + @Test + public void testRelationshipLatestVersionStatusBoth() throws Exception { + // create method #1 + context.turnOffAuthorisationSystem(); + Relationship relationship1 = relationshipService.create( + context, publication1, person1, relationshipType, 3, 5, "left", "right", + Relationship.LatestVersionStatus.BOTH // set latest version status + ); + context.restoreAuthSystemState(); + assertEquals(Relationship.LatestVersionStatus.BOTH, relationship1.getLatestVersionStatus()); + Relationship relationship2 = relationshipService.find(context, relationship1.getID()); + assertEquals(Relationship.LatestVersionStatus.BOTH, relationship2.getLatestVersionStatus()); + + // create method #2 + Relationship inputRelationship = new Relationship(); + inputRelationship.setLeftItem(publication2); + inputRelationship.setRightItem(person1); + inputRelationship.setRelationshipType(relationshipType); + inputRelationship.setLatestVersionStatus(Relationship.LatestVersionStatus.BOTH); // set latest version status + context.turnOffAuthorisationSystem(); + Relationship relationship3 = relationshipService.create(context, inputRelationship); + context.restoreAuthSystemState(); + assertEquals(Relationship.LatestVersionStatus.BOTH, relationship3.getLatestVersionStatus()); + Relationship relationship4 = relationshipService.find(context, relationship3.getID()); + assertEquals(Relationship.LatestVersionStatus.BOTH, relationship4.getLatestVersionStatus()); + } + + @Test + public void testRelationshipLatestVersionStatusLeftOnly() throws Exception { + // create method #1 + context.turnOffAuthorisationSystem(); + Relationship relationship1 = relationshipService.create( + context, publication1, person1, relationshipType, 3, 5, "left", "right", + Relationship.LatestVersionStatus.LEFT_ONLY // set latest version status + ); + context.restoreAuthSystemState(); + assertEquals(Relationship.LatestVersionStatus.LEFT_ONLY, relationship1.getLatestVersionStatus()); + Relationship relationship2 = relationshipService.find(context, relationship1.getID()); + assertEquals(Relationship.LatestVersionStatus.LEFT_ONLY, relationship2.getLatestVersionStatus()); + + // create method #2 + Relationship inputRelationship = new Relationship(); + inputRelationship.setLeftItem(publication2); + inputRelationship.setRightItem(person1); + inputRelationship.setRelationshipType(relationshipType); + inputRelationship.setLatestVersionStatus(Relationship.LatestVersionStatus.LEFT_ONLY); // set LVS + context.turnOffAuthorisationSystem(); + Relationship relationship3 = relationshipService.create(context, inputRelationship); + context.restoreAuthSystemState(); + assertEquals(Relationship.LatestVersionStatus.LEFT_ONLY, relationship3.getLatestVersionStatus()); + Relationship relationship4 = relationshipService.find(context, relationship3.getID()); + assertEquals(Relationship.LatestVersionStatus.LEFT_ONLY, relationship4.getLatestVersionStatus()); + } + + @Test + public void testRelationshipLatestVersionStatusRightOnly() throws Exception { + // create method #1 + context.turnOffAuthorisationSystem(); + Relationship relationship1 = relationshipService.create( + context, publication1, person1, relationshipType, 3, 5, "left", "right", + Relationship.LatestVersionStatus.RIGHT_ONLY // set latest version status + ); + context.restoreAuthSystemState(); + assertEquals(Relationship.LatestVersionStatus.RIGHT_ONLY, relationship1.getLatestVersionStatus()); + Relationship relationship2 = relationshipService.find(context, relationship1.getID()); + assertEquals(Relationship.LatestVersionStatus.RIGHT_ONLY, relationship2.getLatestVersionStatus()); + + // create method #2 + Relationship inputRelationship = new Relationship(); + inputRelationship.setLeftItem(publication2); + inputRelationship.setRightItem(person1); + inputRelationship.setRelationshipType(relationshipType); + inputRelationship.setLatestVersionStatus(Relationship.LatestVersionStatus.RIGHT_ONLY); // set LVS + context.turnOffAuthorisationSystem(); + Relationship relationship3 = relationshipService.create(context, inputRelationship); + context.restoreAuthSystemState(); + assertEquals(Relationship.LatestVersionStatus.RIGHT_ONLY, relationship3.getLatestVersionStatus()); + Relationship relationship4 = relationshipService.find(context, relationship3.getID()); + assertEquals(Relationship.LatestVersionStatus.RIGHT_ONLY, relationship4.getLatestVersionStatus()); + } + +} From a5f0c03a27e8b24d61e1f9c43173ac25bbf88575 Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Wed, 2 Mar 2022 00:58:32 +0100 Subject: [PATCH 0026/1846] 88051: Support filtering non-latest relationships --- .../content/RelationshipServiceImpl.java | 68 +++-- .../dspace/content/dao/RelationshipDAO.java | 95 ++++--- .../content/dao/impl/RelationshipDAOImpl.java | 240 ++++++++++++------ .../content/service/RelationshipService.java | 82 ++++++ .../org/dspace/core/AbstractHibernateDAO.java | 19 +- 5 files changed, 368 insertions(+), 136 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/RelationshipServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/RelationshipServiceImpl.java index f4f13410880c..52f5f966a2de 100644 --- a/dspace-api/src/main/java/org/dspace/content/RelationshipServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/RelationshipServiceImpl.java @@ -293,14 +293,22 @@ public List findByItem(Context context, Item item) throws SQLExcep } @Override - public List findByItem(Context context, Item item, Integer limit, Integer offset, - boolean excludeTilted) throws SQLException { + public List findByItem( + Context context, Item item, Integer limit, Integer offset, boolean excludeTilted + ) throws SQLException { + return findByItem(context, item, limit, offset, excludeTilted, true); + } - List list = relationshipDAO.findByItem(context, item, limit, offset, excludeTilted); + @Override + public List findByItem( + Context context, Item item, Integer limit, Integer offset, boolean excludeTilted, boolean excludeNonLinked + ) throws SQLException { + List list = + relationshipDAO.findByItem(context, item, limit, offset, excludeTilted, excludeNonLinked); list.sort((o1, o2) -> { int relationshipType = o1.getRelationshipType().getLeftwardType() - .compareTo(o2.getRelationshipType().getLeftwardType()); + .compareTo(o2.getRelationshipType().getLeftwardType()); if (relationshipType != 0) { return relationshipType; } else { @@ -652,22 +660,38 @@ public List findByItemAndRelationshipType(Context context, Item it public List findByItemAndRelationshipType(Context context, Item item, RelationshipType relationshipType) throws SQLException { - return relationshipDAO.findByItemAndRelationshipType(context, item, relationshipType, -1, -1); + return findByItemAndRelationshipType(context, item, relationshipType, -1, -1, true); } @Override public List findByItemAndRelationshipType(Context context, Item item, RelationshipType relationshipType, int limit, int offset) throws SQLException { - return relationshipDAO.findByItemAndRelationshipType(context, item, relationshipType, limit, offset); + return findByItemAndRelationshipType(context, item, relationshipType, limit, offset, true); } @Override - public List findByItemAndRelationshipType(Context context, Item item, - RelationshipType relationshipType, boolean isLeft, - int limit, int offset) - throws SQLException { - return relationshipDAO.findByItemAndRelationshipType(context, item, relationshipType, isLeft, limit, offset); + public List findByItemAndRelationshipType( + Context context, Item item, RelationshipType relationshipType, int limit, int offset, boolean excludeNonLatest + ) throws SQLException { + return relationshipDAO + .findByItemAndRelationshipType(context, item, relationshipType, limit, offset, excludeNonLatest); + } + + @Override + public List findByItemAndRelationshipType( + Context context, Item item, RelationshipType relationshipType, boolean isLeft, int limit, int offset + ) throws SQLException { + return findByItemAndRelationshipType(context, item, relationshipType, isLeft, limit, offset, true); + } + + @Override + public List findByItemAndRelationshipType( + Context context, Item item, RelationshipType relationshipType, boolean isLeft, int limit, int offset, + boolean excludeNonLatest + ) throws SQLException { + return relationshipDAO + .findByItemAndRelationshipType(context, item, relationshipType, isLeft, limit, offset, excludeNonLatest); } @Override @@ -704,7 +728,12 @@ public int countTotal(Context context) throws SQLException { @Override public int countByItem(Context context, Item item) throws SQLException { - return relationshipDAO.countByItem(context, item); + return countByItem(context, item, true); + } + + @Override + public int countByItem(Context context, Item item, boolean excludeNonLatest) throws SQLException { + return relationshipDAO.countByItem(context, item, excludeNonLatest); } @Override @@ -713,9 +742,18 @@ public int countByRelationshipType(Context context, RelationshipType relationshi } @Override - public int countByItemAndRelationshipType(Context context, Item item, RelationshipType relationshipType, - boolean isLeft) throws SQLException { - return relationshipDAO.countByItemAndRelationshipType(context, item, relationshipType, isLeft); + public int countByItemAndRelationshipType( + Context context, Item item, RelationshipType relationshipType, boolean isLeft + ) throws SQLException { + return countByItemAndRelationshipType(context, item, relationshipType, isLeft, true); + } + + @Override + public int countByItemAndRelationshipType( + Context context, Item item, RelationshipType relationshipType, boolean isLeft, boolean excludeNonLatest + ) throws SQLException { + return relationshipDAO + .countByItemAndRelationshipType(context, item, relationshipType, isLeft, excludeNonLatest); } @Override diff --git a/dspace-api/src/main/java/org/dspace/content/dao/RelationshipDAO.java b/dspace-api/src/main/java/org/dspace/content/dao/RelationshipDAO.java index 57b950a36be1..c7a0a032ede8 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/RelationshipDAO.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/RelationshipDAO.java @@ -28,31 +28,38 @@ public interface RelationshipDAO extends GenericDAO { /** * This method returns a list of Relationship objects that have the given Item object * as a leftItem or a rightItem - * @param context The relevant DSpace context - * @param item The item that should be either a leftItem or a rightItem of all - * the Relationship objects in the returned list - * @param excludeTilted If true, excludes tilted relationships - * @return The list of Relationship objects that contain either a left or a - * right item that is equal to the given item - * @throws SQLException If something goes wrong + * @param context The relevant DSpace context + * @param item The item that should be either a leftItem or a rightItem of all + * the Relationship objects in the returned list + * @param excludeTilted If true, excludes tilted relationships + * @param excludeNonLatest If true, excludes all relationships for which the other item has a more recent version + * that is relevant for this relationship + * @return The list of Relationship objects that contain either a left or a + * right item that is equal to the given item + * @throws SQLException If something goes wrong */ - List findByItem(Context context, Item item, boolean excludeTilted) throws SQLException; + List findByItem( + Context context, Item item, boolean excludeTilted, boolean excludeNonLatest + ) throws SQLException; /** * This method returns a list of Relationship objects that have the given Item object * as a leftItem or a rightItem - * @param context The relevant DSpace context - * @param item The item that should be either a leftItem or a rightItem of all - * the Relationship objects in the returned list - * @param limit paging limit - * @param offset paging offset - * @param excludeTilted If true, excludes tilted relationships - * @return The list of Relationship objects that contain either a left or a - * right item that is equal to the given item - * @throws SQLException If something goes wrong - */ - List findByItem(Context context, Item item, Integer limit, Integer offset, boolean excludeTilted) - throws SQLException; + * @param context The relevant DSpace context + * @param item The item that should be either a leftItem or a rightItem of all + * the Relationship objects in the returned list + * @param limit paging limit + * @param offset paging offset + * @param excludeTilted If true, excludes tilted relationships + * @param excludeNonLatest If true, excludes all relationships for which the other item has a more recent version + * that is relevant for this relationship + * @return The list of Relationship objects that contain either a left or a + * right item that is equal to the given item + * @throws SQLException If something goes wrong + */ + List findByItem( + Context context, Item item, Integer limit, Integer offset, boolean excludeTilted, boolean excludeNonLatest + ) throws SQLException; /** * This method returns the next leftplace integer to use for a relationship with this item as the leftItem @@ -108,34 +115,41 @@ List findByRelationshipType(Context context, RelationshipType rela * It will construct a list of all Relationship objects that have the given RelationshipType object * as the relationshipType property * @param context The relevant DSpace context + * @param item item to filter by * @param relationshipType The RelationshipType object to be checked on * @param limit paging limit * @param offset paging offset - * @param item item to filter by + * @param excludeNonLatest If true, excludes all relationships for which the other item has a more recent version + * that is relevant for this relationship * @return A list of Relationship objects that have the given RelationshipType object as the * relationshipType property * @throws SQLException If something goes wrong */ - List findByItemAndRelationshipType(Context context, Item item, RelationshipType relationshipType, - Integer limit, Integer offset) throws SQLException; + List findByItemAndRelationshipType( + Context context, Item item, RelationshipType relationshipType, Integer limit, Integer offset, + boolean excludeNonLatest + ) throws SQLException; /** * This method returns a list of Relationship objects for the given RelationshipType object. * It will construct a list of all Relationship objects that have the given RelationshipType object * as the relationshipType property * @param context The relevant DSpace context + * @param item item to filter by * @param relationshipType The RelationshipType object to be checked on + * @param isLeft Is item left or right * @param limit paging limit * @param offset paging offset - * @param item item to filter by - * @param isLeft Is item left or right + * @param excludeNonLatest If true, excludes all relationships for which the other item has a more recent version + * that is relevant for this relationship * @return A list of Relationship objects that have the given RelationshipType object as the * relationshipType property * @throws SQLException If something goes wrong */ - List findByItemAndRelationshipType(Context context, Item item, RelationshipType relationshipType, - boolean isLeft, Integer limit, Integer offset) - throws SQLException; + List findByItemAndRelationshipType( + Context context, Item item, RelationshipType relationshipType, boolean isLeft, Integer limit, Integer offset, + boolean excludeNonLatest + ) throws SQLException; /** * This method returns a list of Relationship objects for the given typeName @@ -183,28 +197,33 @@ List findByTypeName(Context context, String typeName, Integer limi /** * This method returns a count of Relationship objects that have the given Item object * as a leftItem or a rightItem - * @param context The relevant DSpace context - * @param item The item that should be either a leftItem or a rightItem of all - * the Relationship objects in the returned list + * @param context The relevant DSpace context + * @param item The item that should be either a leftItem or a rightItem of all + * the Relationship objects in the returned list + * @param excludeNonLatest if true, exclude relationships for which the opposite item is not the latest version + * that is relevant * @return The list of Relationship objects that contain either a left or a * right item that is equal to the given item * @throws SQLException If something goes wrong */ - int countByItem(Context context, Item item) throws SQLException; + int countByItem(Context context, Item item, boolean excludeNonLatest) throws SQLException; /** * Count total number of relationships (rows in relationship table) by an item and a relationship type and a boolean * indicating whether the item should be the leftItem or the rightItem * - * @param context context - * @param relationshipType relationship type to filter by - * @param item item to filter by - * @param isLeft Indicating whether the counted Relationships should have the given Item on the left side or not + * @param context context + * @param relationshipType relationship type to filter by + * @param item item to filter by + * @param isLeft indicating whether the counted Relationships should have the given Item on the left side + * @param excludeNonLatest if true, exclude relationships for which the opposite item is not the latest version + * that is relevant * @return total count * @throws SQLException if database error */ - int countByItemAndRelationshipType(Context context, Item item, RelationshipType relationshipType, boolean isLeft) - throws SQLException; + int countByItemAndRelationshipType( + Context context, Item item, RelationshipType relationshipType, boolean isLeft, boolean excludeNonLatest + ) throws SQLException; /** * Count total number of relationships (rows in relationship table) given a typeName diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/RelationshipDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/RelationshipDAOImpl.java index 48baf45f23f2..acd5af1509a4 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/impl/RelationshipDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/RelationshipDAOImpl.java @@ -14,6 +14,7 @@ import javax.persistence.Query; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; import org.dspace.content.Item; @@ -30,60 +31,147 @@ public class RelationshipDAOImpl extends AbstractHibernateDAO implements RelationshipDAO { @Override - public List findByItem(Context context, Item item, boolean excludeTilted) throws SQLException { - return findByItem(context, item, -1, -1, excludeTilted); + public List findByItem( + Context context, Item item, boolean excludeTilted, boolean excludeNonLatest + ) throws SQLException { + return findByItem(context, item, -1, -1, excludeTilted, excludeNonLatest); } @Override - public List findByItem(Context context, Item item, Integer limit, Integer offset, - boolean excludeTilted) throws SQLException { - + public List findByItem( + Context context, Item item, Integer limit, Integer offset, boolean excludeTilted, boolean excludeNonLatest + ) throws SQLException { CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); - CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, Relationship.class); + CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, Relationship.class); Root relationshipRoot = criteriaQuery.from(Relationship.class); criteriaQuery.select(relationshipRoot); + + criteriaQuery.where( + criteriaBuilder.or( + getLeftItemPredicate(criteriaBuilder, relationshipRoot, item, excludeTilted, excludeNonLatest), + getRightItemPredicate(criteriaBuilder, relationshipRoot, item, excludeTilted, excludeNonLatest) + ) + ); + + return list(context, criteriaQuery, false, Relationship.class, limit, offset); + } + + /** + * Get the predicate for a criteria query that selects relationships by their left item. + * @param criteriaBuilder the criteria builder. + * @param relationshipRoot the relationship root. + * @param item the item that is being searched for. + * @param excludeTilted if true, exclude tilted relationships. + * @param excludeNonLatest if true, exclude relationships for which the opposite item is not the latest version + * that is relevant. + * @return a predicate that satisfies the given restrictions. + */ + protected Predicate getLeftItemPredicate( + CriteriaBuilder criteriaBuilder, Root relationshipRoot, Item item, + boolean excludeTilted, boolean excludeNonLatest + ) { + List predicates = new ArrayList<>(); + + // match relationships based on the left item + predicates.add( + criteriaBuilder.equal(relationshipRoot.get(Relationship_.leftItem), item) + ); + if (excludeTilted) { - // If this item is the left item, - // return relationships for types which are not tilted right (tilted is either left nor null) - // If this item is the right item, - // return relationships for types which are not tilted left (tilted is either right nor null) - criteriaQuery - .where(criteriaBuilder.or( - criteriaBuilder.and( - criteriaBuilder.equal(relationshipRoot.get(Relationship_.leftItem), item), - criteriaBuilder.or( - criteriaBuilder.isNull(relationshipRoot.get(Relationship_.relationshipType) - .get(RelationshipType_.tilted)), - criteriaBuilder.notEqual(relationshipRoot - .get(Relationship_.relationshipType) - .get(RelationshipType_.tilted), RelationshipType.Tilted.RIGHT))), - criteriaBuilder.and( - criteriaBuilder.equal(relationshipRoot.get(Relationship_.rightItem), item), - criteriaBuilder.or( - criteriaBuilder.isNull(relationshipRoot.get(Relationship_.relationshipType) - .get(RelationshipType_.tilted)), - criteriaBuilder.notEqual(relationshipRoot - .get(Relationship_.relationshipType) - .get(RelationshipType_.tilted), RelationshipType.Tilted.LEFT))))); - } else { - criteriaQuery - .where(criteriaBuilder.or(criteriaBuilder.equal(relationshipRoot.get(Relationship_.leftItem), item), - criteriaBuilder.equal(relationshipRoot.get(Relationship_.rightItem), item))); + // if this item is the left item, + // return relationships for types which are NOT tilted right (tilted is either left nor null) + predicates.add( + criteriaBuilder.or( + criteriaBuilder.isNull( + relationshipRoot.get(Relationship_.relationshipType).get(RelationshipType_.tilted) + ), + criteriaBuilder.notEqual( + relationshipRoot.get(Relationship_.relationshipType).get(RelationshipType_.tilted), + RelationshipType.Tilted.RIGHT + ) + ) + ); } - return list(context, criteriaQuery, false, Relationship.class, limit, offset); + + if (excludeNonLatest) { + // if this item is the left item, + // return relationships for which the right item is the "latest" version that is relevant. + predicates.add( + criteriaBuilder.notEqual( + relationshipRoot.get(Relationship_.LATEST_VERSION_STATUS), + Relationship.LatestVersionStatus.LEFT_ONLY + ) + ); + } + + return criteriaBuilder.and(predicates.toArray(new Predicate[]{})); } - @Override - public int countByItem(Context context, Item item) - throws SQLException { + /** + * Get the predicate for a criteria query that selects relationships by their right item. + * @param criteriaBuilder the criteria builder. + * @param relationshipRoot the relationship root. + * @param item the item that is being searched for. + * @param excludeTilted if true, exclude tilted relationships. + * @param excludeNonLatest if true, exclude relationships for which the opposite item is not the latest version + * that is relevant. + * @return a predicate that satisfies the given restrictions. + */ + protected Predicate getRightItemPredicate( + CriteriaBuilder criteriaBuilder, Root relationshipRoot, Item item, + boolean excludeTilted, boolean excludeNonLatest + ) { + List predicates = new ArrayList<>(); + + // match relationships based on the right item + predicates.add( + criteriaBuilder.equal(relationshipRoot.get(Relationship_.rightItem), item) + ); + + if (excludeTilted) { + // if this item is the right item, + // return relationships for types which are NOT tilted left (tilted is either right nor null) + predicates.add( + criteriaBuilder.or( + criteriaBuilder.isNull( + relationshipRoot.get(Relationship_.relationshipType).get(RelationshipType_.tilted) + ), + criteriaBuilder.notEqual( + relationshipRoot.get(Relationship_.relationshipType).get(RelationshipType_.tilted), + RelationshipType.Tilted.LEFT + ) + ) + ); + } + if (excludeNonLatest) { + // if this item is the right item, + // return relationships for which the left item is the "latest" version that is relevant. + predicates.add( + criteriaBuilder.notEqual( + relationshipRoot.get(Relationship_.LATEST_VERSION_STATUS), + Relationship.LatestVersionStatus.RIGHT_ONLY + ) + ); + } + + return criteriaBuilder.and(predicates.toArray(new Predicate[]{})); + } + + @Override + public int countByItem(Context context, Item item, boolean excludeNonLatest) throws SQLException { CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, Relationship.class); Root relationshipRoot = criteriaQuery.from(Relationship.class); criteriaQuery.select(relationshipRoot); - criteriaQuery - .where(criteriaBuilder.or(criteriaBuilder.equal(relationshipRoot.get(Relationship_.leftItem), item), - criteriaBuilder.equal(relationshipRoot.get(Relationship_.rightItem), item))); + + criteriaQuery.where( + criteriaBuilder.or( + getLeftItemPredicate(criteriaBuilder, relationshipRoot, item, false, excludeNonLatest), + getRightItemPredicate(criteriaBuilder, relationshipRoot, item, false, excludeNonLatest) + ) + ); + return count(context, criteriaQuery, criteriaBuilder, relationshipRoot); } @@ -140,46 +228,50 @@ public List findByRelationshipType(Context context, RelationshipTy } @Override - public List findByItemAndRelationshipType(Context context, Item item, - RelationshipType relationshipType, Integer limit, - Integer offset) - throws SQLException { - + public List findByItemAndRelationshipType( + Context context, Item item, RelationshipType relationshipType, Integer limit, Integer offset, + boolean excludeNonLatest + ) throws SQLException { CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, Relationship.class); Root relationshipRoot = criteriaQuery.from(Relationship.class); criteriaQuery.select(relationshipRoot); - criteriaQuery - .where(criteriaBuilder.equal(relationshipRoot.get(Relationship_.relationshipType), - relationshipType), criteriaBuilder.or - (criteriaBuilder.equal(relationshipRoot.get(Relationship_.leftItem), item), - criteriaBuilder.equal(relationshipRoot.get(Relationship_.rightItem), item))); + + criteriaQuery.where( + criteriaBuilder.equal(relationshipRoot.get(Relationship_.relationshipType), relationshipType), + criteriaBuilder.or( + getLeftItemPredicate(criteriaBuilder, relationshipRoot, item, false, excludeNonLatest), + getRightItemPredicate(criteriaBuilder, relationshipRoot, item, false, excludeNonLatest) + ) + ); + return list(context, criteriaQuery, true, Relationship.class, limit, offset); } @Override - public List findByItemAndRelationshipType(Context context, Item item, - RelationshipType relationshipType, boolean isLeft, - Integer limit, Integer offset) - throws SQLException { - + public List findByItemAndRelationshipType( + Context context, Item item, RelationshipType relationshipType, boolean isLeft, Integer limit, Integer offset, + boolean excludeNonLatest + ) throws SQLException { CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, Relationship.class); Root relationshipRoot = criteriaQuery.from(Relationship.class); criteriaQuery.select(relationshipRoot); + if (isLeft) { - criteriaQuery - .where(criteriaBuilder.equal(relationshipRoot.get(Relationship_.relationshipType), - relationshipType), - criteriaBuilder.equal(relationshipRoot.get(Relationship_.leftItem), item)); + criteriaQuery.where( + criteriaBuilder.equal(relationshipRoot.get(Relationship_.relationshipType), relationshipType), + getLeftItemPredicate(criteriaBuilder, relationshipRoot, item, false, excludeNonLatest) + ); criteriaQuery.orderBy(criteriaBuilder.asc(relationshipRoot.get(Relationship_.leftPlace))); } else { - criteriaQuery - .where(criteriaBuilder.equal(relationshipRoot.get(Relationship_.relationshipType), - relationshipType), - criteriaBuilder.equal(relationshipRoot.get(Relationship_.rightItem), item)); + criteriaQuery.where( + criteriaBuilder.equal(relationshipRoot.get(Relationship_.relationshipType), relationshipType), + getRightItemPredicate(criteriaBuilder, relationshipRoot, item, false, excludeNonLatest) + ); criteriaQuery.orderBy(criteriaBuilder.asc(relationshipRoot.get(Relationship_.rightPlace))); } + return list(context, criteriaQuery, true, Relationship.class, limit, offset); } @@ -228,24 +320,26 @@ public int countRows(Context context) throws SQLException { } @Override - public int countByItemAndRelationshipType(Context context, Item item, RelationshipType relationshipType, - boolean isLeft) throws SQLException { - + public int countByItemAndRelationshipType( + Context context, Item item, RelationshipType relationshipType, boolean isLeft, boolean excludeNonLatest + ) throws SQLException { CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, Relationship.class); Root relationshipRoot = criteriaQuery.from(Relationship.class); criteriaQuery.select(relationshipRoot); + if (isLeft) { - criteriaQuery - .where(criteriaBuilder.equal(relationshipRoot.get(Relationship_.relationshipType), - relationshipType), - criteriaBuilder.equal(relationshipRoot.get(Relationship_.leftItem), item)); + criteriaQuery.where( + criteriaBuilder.equal(relationshipRoot.get(Relationship_.relationshipType), relationshipType), + getLeftItemPredicate(criteriaBuilder, relationshipRoot, item, false, excludeNonLatest) + ); } else { - criteriaQuery - .where(criteriaBuilder.equal(relationshipRoot.get(Relationship_.relationshipType), - relationshipType), - criteriaBuilder.equal(relationshipRoot.get(Relationship_.rightItem), item)); + criteriaQuery.where( + criteriaBuilder.equal(relationshipRoot.get(Relationship_.relationshipType), relationshipType), + getRightItemPredicate(criteriaBuilder, relationshipRoot, item, false, excludeNonLatest) + ); } + return count(context, criteriaQuery, criteriaBuilder, relationshipRoot); } diff --git a/dspace-api/src/main/java/org/dspace/content/service/RelationshipService.java b/dspace-api/src/main/java/org/dspace/content/service/RelationshipService.java index d64425132f8f..f54e6f07f88d 100644 --- a/dspace-api/src/main/java/org/dspace/content/service/RelationshipService.java +++ b/dspace-api/src/main/java/org/dspace/content/service/RelationshipService.java @@ -50,6 +50,25 @@ public interface RelationshipService extends DSpaceCRUDService { List findByItem(Context context, Item item, Integer limit, Integer offset, boolean excludeTilted) throws SQLException; + /** + * Retrieves the list of Relationships currently in the system for which the given Item is either + * a leftItem or a rightItem object + * @param context The relevant DSpace context + * @param item The Item that has to be the left or right item for the relationship to be + * included in the list + * @param limit paging limit + * @param offset paging offset + * @param excludeTilted If true, excludes tilted relationships + * @param excludeNonLatest If true, excludes all relationships for which the other item has a more recent version + * that is relevant for this relationship + * @return The list of relationships for which each relationship adheres to the above + * listed constraint + * @throws SQLException If something goes wrong + */ + List findByItem( + Context context, Item item, Integer limit, Integer offset, boolean excludeTilted, boolean excludeNonLatest + ) throws SQLException; + /** * Retrieves the full list of relationships currently in the system * @param context The relevant DSpace context @@ -129,6 +148,22 @@ public List findByItemAndRelationshipType(Context context, Item it RelationshipType relationshipType, int limit, int offset) throws SQLException; + /** + * This method returns a list of Relationships for which the leftItem or rightItem is equal to the given + * Item object and for which the RelationshipType object is equal to the relationshipType property + * @param context The relevant DSpace context + * @param item The Item object to be matched on the leftItem or rightItem for the relationship + * @param relationshipType The RelationshipType object that will be used to check the Relationship on + * @param excludeNonLatest If true, excludes all relationships for which the other item has a more recent version + * that is relevant for this relationship + * @return The list of Relationship objects that have the given Item object as leftItem or rightItem and + * for which the relationshipType property is equal to the given RelationshipType + * @throws SQLException If something goes wrong + */ + public List findByItemAndRelationshipType( + Context context, Item item, RelationshipType relationshipType, int limit, int offset, boolean excludeNonLatest + ) throws SQLException; + /** * This method returns a list of Relationships for which the leftItem or rightItem is equal to the given * Item object and for which the RelationshipType object is equal to the relationshipType property @@ -145,6 +180,24 @@ public List findByItemAndRelationshipType(Context context, Item it int limit, int offset) throws SQLException; + /** + * This method returns a list of Relationships for which the leftItem or rightItem is equal to the given + * Item object and for which the RelationshipType object is equal to the relationshipType property + * @param context The relevant DSpace context + * @param item The Item object to be matched on the leftItem or rightItem for the relationship + * @param relationshipType The RelationshipType object that will be used to check the Relationship on + * @param isLeft Is the item left or right + * @param excludeNonLatest If true, excludes all relationships for which the other item has a more recent version + * that is relevant for this relationship + * @return The list of Relationship objects that have the given Item object as leftItem or rightItem and + * for which the relationshipType property is equal to the given RelationshipType + * @throws SQLException If something goes wrong + */ + public List findByItemAndRelationshipType( + Context context, Item item, RelationshipType relationshipType, boolean isLeft, int limit, int offset, + boolean excludeNonLatest + ) throws SQLException; + /** * This method will update the place for the Relationship and all other relationships found by the items and * relationship type of the given Relationship. It will give this Relationship the last place in both the @@ -310,6 +363,20 @@ List findByTypeName(Context context, String typeName, Integer limi */ int countByItem(Context context, Item item) throws SQLException; + /** + * This method returns a count of Relationship objects that have the given Item object + * as a leftItem or a rightItem + * @param context The relevant DSpace context + * @param item The item that should be either a leftItem or a rightItem of all + * the Relationship objects in the returned list + * @param excludeNonLatest if true, exclude relationships for which the opposite item is not the latest version + * that is relevant + * @return The list of Relationship objects that contain either a left or a + * right item that is equal to the given item + * @throws SQLException If something goes wrong + */ + int countByItem(Context context, Item item, boolean excludeNonLatest) throws SQLException; + /** * Count total number of relationships (rows in relationship table) by a relationship type and a boolean indicating * whether the relationship should contain the item on the left side or not @@ -323,6 +390,21 @@ List findByTypeName(Context context, String typeName, Integer limi int countByItemAndRelationshipType(Context context, Item item, RelationshipType relationshipType, boolean isLeft) throws SQLException; + /** + * Count total number of relationships (rows in relationship table) by a relationship type and a boolean indicating + * whether the relationship should contain the item on the left side or not + * @param context context + * @param relationshipType relationship type to filter by + * @param isLeft Indicating whether the counted Relationships should have the given Item on the left side + * @param excludeNonLatest If true, excludes all relationships for which the other item has a more recent version + * that is relevant for this relationship + * @return total count with the given parameters + * @throws SQLException if database error + */ + int countByItemAndRelationshipType( + Context context, Item item, RelationshipType relationshipType, boolean isLeft, boolean excludeNonLatest + ) throws SQLException; + /** * Count total number of relationships (rows in relationship table) * by a relationship leftward or rightward typeName diff --git a/dspace-api/src/main/java/org/dspace/core/AbstractHibernateDAO.java b/dspace-api/src/main/java/org/dspace/core/AbstractHibernateDAO.java index 34a04056ce32..32ad747d765e 100644 --- a/dspace-api/src/main/java/org/dspace/core/AbstractHibernateDAO.java +++ b/dspace-api/src/main/java/org/dspace/core/AbstractHibernateDAO.java @@ -155,12 +155,11 @@ public Query createQuery(Context context, String query) throws SQLException { * @return A list of distinct results as depicted by the CriteriaQuery and parameters * @throws SQLException */ - public List list(Context context, CriteriaQuery criteriaQuery, boolean cacheable, Class clazz, int maxResults, - int offset) throws SQLException { + public List list( + Context context, CriteriaQuery criteriaQuery, boolean cacheable, Class clazz, int maxResults, int offset + ) throws SQLException { criteriaQuery.distinct(true); - @SuppressWarnings("unchecked") - List result = (List) executeCriteriaQuery(context, criteriaQuery, cacheable, maxResults, offset); - return result; + return executeCriteriaQuery(context, criteriaQuery, cacheable, maxResults, offset); } /** @@ -183,12 +182,12 @@ public List list(Context context, CriteriaQuery criteriaQuery, boolean cachea * @return A list of results determined by the CriteriaQuery and parameters * @throws SQLException */ - public List list(Context context, CriteriaQuery criteriaQuery, boolean cacheable, Class clazz, int maxResults, - int offset, boolean distinct) throws SQLException { + public List list( + Context context, CriteriaQuery criteriaQuery, boolean cacheable, Class clazz, int maxResults, int offset, + boolean distinct + ) throws SQLException { criteriaQuery.distinct(distinct); - @SuppressWarnings("unchecked") - List result = (List) executeCriteriaQuery(context, criteriaQuery, cacheable, maxResults, offset); - return result; + return executeCriteriaQuery(context, criteriaQuery, cacheable, maxResults, offset); } /** From 85d05e6ea8b8e53e204089b728a6f68d999d71ab Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Wed, 2 Mar 2022 13:48:50 +0100 Subject: [PATCH 0027/1846] 88051: Fix tests --- .../java/org/dspace/content/RelationshipServiceImplTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/test/java/org/dspace/content/RelationshipServiceImplTest.java b/dspace-api/src/test/java/org/dspace/content/RelationshipServiceImplTest.java index 5d6197e49460..fbcac3554fd0 100644 --- a/dspace-api/src/test/java/org/dspace/content/RelationshipServiceImplTest.java +++ b/dspace-api/src/test/java/org/dspace/content/RelationshipServiceImplTest.java @@ -113,7 +113,7 @@ public void testFindByItem() throws Exception { when(relationshipService.findByItem(context, cindy, -1, -1, false)).thenReturn(relationshipTest); // Mock the state of objects utilized in findByItem() to meet the success criteria of the invocation - when(relationshipDAO.findByItem(context, cindy, -1, -1, false)).thenReturn(relationshipTest); + when(relationshipDAO.findByItem(context, cindy, -1, -1, false, false)).thenReturn(relationshipTest); List results = relationshipService.findByItem(context, cindy); assertEquals("TestFindByItem 0", relationshipTest, results); From 0b31955f83c4e02ef74ecf7f42af4d8fddc16790 Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Wed, 2 Mar 2022 16:44:59 +0100 Subject: [PATCH 0028/1846] 88051: Write tests for excludeNonLatest flag --- .../dspace/builder/RelationshipBuilder.java | 6 + ...RelationshipServiceImplVersioningTest.java | 848 ++++++++++++++++++ 2 files changed, 854 insertions(+) diff --git a/dspace-api/src/test/java/org/dspace/builder/RelationshipBuilder.java b/dspace-api/src/test/java/org/dspace/builder/RelationshipBuilder.java index 874603341980..3c9c6800df4b 100644 --- a/dspace-api/src/test/java/org/dspace/builder/RelationshipBuilder.java +++ b/dspace-api/src/test/java/org/dspace/builder/RelationshipBuilder.java @@ -139,4 +139,10 @@ public RelationshipBuilder withLeftPlace(int leftPlace) { relationship.setLeftPlace(leftPlace); return this; } + + public RelationshipBuilder withLatestVersionStatus(Relationship.LatestVersionStatus latestVersionStatus) { + relationship.setLatestVersionStatus(latestVersionStatus); + return this; + } + } diff --git a/dspace-api/src/test/java/org/dspace/content/RelationshipServiceImplVersioningTest.java b/dspace-api/src/test/java/org/dspace/content/RelationshipServiceImplVersioningTest.java index a9e3bb04e723..5ad52a3417d2 100644 --- a/dspace-api/src/test/java/org/dspace/content/RelationshipServiceImplVersioningTest.java +++ b/dspace-api/src/test/java/org/dspace/content/RelationshipServiceImplVersioningTest.java @@ -8,21 +8,28 @@ package org.dspace.content; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.util.List; import org.dspace.AbstractIntegrationTestWithDatabase; import org.dspace.builder.CollectionBuilder; import org.dspace.builder.CommunityBuilder; import org.dspace.builder.EntityTypeBuilder; import org.dspace.builder.ItemBuilder; +import org.dspace.builder.RelationshipBuilder; import org.dspace.builder.RelationshipTypeBuilder; +import org.dspace.content.dao.RelationshipDAO; import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.service.RelationshipService; +import org.dspace.services.factory.DSpaceServicesFactory; import org.junit.Before; import org.junit.Test; public class RelationshipServiceImplVersioningTest extends AbstractIntegrationTestWithDatabase { private RelationshipService relationshipService; + private RelationshipDAO relationshipDAO; protected Community community; protected Collection collection; @@ -40,6 +47,8 @@ public void setUp() throws Exception { super.setUp(); relationshipService = ContentServiceFactory.getInstance().getRelationshipService(); + relationshipDAO = DSpaceServicesFactory.getInstance().getServiceManager() + .getServicesByType(RelationshipDAO.class).get(0); context.turnOffAuthorisationSystem(); @@ -205,4 +214,843 @@ public void testRelationshipLatestVersionStatusRightOnly() throws Exception { assertEquals(Relationship.LatestVersionStatus.RIGHT_ONLY, relationship4.getLatestVersionStatus()); } + protected void assertRelationship(Relationship expectedRelationship, List relationships) { + assertNotNull(relationships); + assertEquals(1, relationships.size()); + assertEquals(expectedRelationship, relationships.get(0)); + } + + protected void assertNoRelationship(List relationships) { + assertNotNull(relationships); + assertEquals(0, relationships.size()); + } + + @Test + public void testExcludeNonLatestBoth() throws Exception { + context.turnOffAuthorisationSystem(); + Relationship relationship1 = RelationshipBuilder + .createRelationshipBuilder(context, publication1, person1, relationshipType) + .withLatestVersionStatus(Relationship.LatestVersionStatus.BOTH) + .build(); + context.restoreAuthSystemState(); + + assertRelationship( + relationship1, + relationshipDAO.findByItem(context, publication1, false, false) + ); + assertRelationship( + relationship1, + relationshipDAO.findByItem(context, publication1, false, true) + ); + assertRelationship( + relationship1, + relationshipDAO.findByItem(context, person1, false, false) + ); + assertRelationship( + relationship1, + relationshipDAO.findByItem(context, person1, false, true) + ); + + assertRelationship( + relationship1, + relationshipDAO.findByItem(context, publication1, -1, -1, false, false) + ); + assertRelationship( + relationship1, + relationshipDAO.findByItem(context, publication1, -1, -1, false, true) + ); + assertRelationship( + relationship1, + relationshipDAO.findByItem(context, person1, -1, -1, false, false) + ); + assertRelationship( + relationship1, + relationshipDAO.findByItem(context, person1, -1, -1, false, true) + ); + + assertEquals(1, relationshipDAO.countByItem(context, publication1, false)); + assertEquals(1, relationshipDAO.countByItem(context, publication1, true)); + assertEquals(1, relationshipDAO.countByItem(context, person1, false)); + assertEquals(1, relationshipDAO.countByItem(context, person1, true)); + + assertRelationship( + relationship1, + relationshipDAO.findByItemAndRelationshipType(context, publication1, relationshipType, -1, -1, false) + ); + assertRelationship( + relationship1, + relationshipDAO.findByItemAndRelationshipType(context, publication1, relationshipType, -1, -1, true) + ); + assertRelationship( + relationship1, + relationshipDAO.findByItemAndRelationshipType(context, person1, relationshipType, -1, -1, false) + ); + assertRelationship( + relationship1, + relationshipDAO.findByItemAndRelationshipType(context, person1, relationshipType, -1, -1, true) + ); + + assertNoRelationship( + relationshipDAO.findByItemAndRelationshipType(context, publication1, relationshipType, false, -1, -1, false) + ); + assertNoRelationship( + relationshipDAO.findByItemAndRelationshipType(context, publication1, relationshipType, false, -1, -1, true) + ); + assertRelationship( + relationship1, + relationshipDAO.findByItemAndRelationshipType(context, publication1, relationshipType, true, -1, -1, false) + ); + assertRelationship( + relationship1, + relationshipDAO.findByItemAndRelationshipType(context, publication1, relationshipType, true, -1, -1, true) + ); + assertRelationship( + relationship1, + relationshipDAO.findByItemAndRelationshipType(context, person1, relationshipType, false, -1, -1, false) + ); + assertRelationship( + relationship1, + relationshipDAO.findByItemAndRelationshipType(context, person1, relationshipType, false, -1, -1, true) + ); + assertNoRelationship( + relationshipDAO.findByItemAndRelationshipType(context, person1, relationshipType, true, -1, -1, false) + ); + assertNoRelationship( + relationshipDAO.findByItemAndRelationshipType(context, person1, relationshipType, true, -1, -1, true) + ); + + assertEquals( + 0, relationshipDAO.countByItemAndRelationshipType(context, publication1, relationshipType, false, false) + ); + assertEquals( + 0, relationshipDAO.countByItemAndRelationshipType(context, publication1, relationshipType, false, true) + ); + assertEquals( + 1, relationshipDAO.countByItemAndRelationshipType(context, publication1, relationshipType, true, false) + ); + assertEquals( + 1, relationshipDAO.countByItemAndRelationshipType(context, publication1, relationshipType, true, true) + ); + assertEquals( + 1, relationshipDAO.countByItemAndRelationshipType(context, person1, relationshipType, false, false) + ); + assertEquals( + 1, relationshipDAO.countByItemAndRelationshipType(context, person1, relationshipType, false, true) + ); + assertEquals( + 0, relationshipDAO.countByItemAndRelationshipType(context, person1, relationshipType, true, false) + ); + assertEquals( + 0, relationshipDAO.countByItemAndRelationshipType(context, person1, relationshipType, true, true) + ); + + assertRelationship( + relationship1, + relationshipService.findByItem(context, publication1) + ); + assertRelationship( + relationship1, + relationshipService.findByItem(context, person1) + ); + + assertRelationship( + relationship1, + relationshipService.findByItem(context, publication1, -1, -1, false) + ); + assertRelationship( + relationship1, + relationshipService.findByItem(context, person1, -1, -1, false) + ); + + assertRelationship( + relationship1, + relationshipService.findByItem(context, publication1, -1, -1, false, false) + ); + assertRelationship( + relationship1, + relationshipService.findByItem(context, publication1, -1, -1, false, true) + ); + assertRelationship( + relationship1, + relationshipService.findByItem(context, person1, -1, -1, false, false) + ); + assertRelationship( + relationship1, + relationshipService.findByItem(context, person1, -1, -1, false, true) + ); + + assertRelationship( + relationship1, + relationshipService.findByItemAndRelationshipType(context, publication1, relationshipType) + ); + assertRelationship( + relationship1, + relationshipService.findByItemAndRelationshipType(context, person1, relationshipType) + ); + + assertRelationship( + relationship1, + relationshipService.findByItemAndRelationshipType(context, publication1, relationshipType, -1, -1) + ); + assertRelationship( + relationship1, + relationshipService.findByItemAndRelationshipType(context, person1, relationshipType, -1, -1) + ); + + assertRelationship( + relationship1, + relationshipService.findByItemAndRelationshipType(context, publication1, relationshipType, -1, -1, false) + ); + assertRelationship( + relationship1, + relationshipService.findByItemAndRelationshipType(context, publication1, relationshipType, -1, -1, true) + ); + assertRelationship( + relationship1, + relationshipService.findByItemAndRelationshipType(context, person1, relationshipType, -1, -1, false) + ); + assertRelationship( + relationship1, + relationshipService.findByItemAndRelationshipType(context, person1, relationshipType, -1, -1, true) + ); + + assertNoRelationship( + relationshipService.findByItemAndRelationshipType(context, publication1, relationshipType, false, -1, -1) + ); + assertRelationship( + relationship1, + relationshipService.findByItemAndRelationshipType(context, publication1, relationshipType, true, -1, -1) + ); + assertRelationship( + relationship1, + relationshipService.findByItemAndRelationshipType(context, person1, relationshipType, false, -1, -1) + ); + assertNoRelationship( + relationshipService.findByItemAndRelationshipType(context, person1, relationshipType, true, -1, -1) + ); + + assertNoRelationship( + relationshipService + .findByItemAndRelationshipType(context, publication1, relationshipType, false, -1, -1, false) + ); + assertNoRelationship( + relationshipService + .findByItemAndRelationshipType(context, publication1, relationshipType, false, -1, -1, true) + ); + assertRelationship( + relationship1, + relationshipService + .findByItemAndRelationshipType(context, publication1, relationshipType, true, -1, -1, false) + ); + assertRelationship( + relationship1, + relationshipService + .findByItemAndRelationshipType(context, publication1, relationshipType, true, -1, -1, true) + ); + assertRelationship( + relationship1, + relationshipService.findByItemAndRelationshipType(context, person1, relationshipType, false, -1, -1, false) + ); + assertRelationship( + relationship1, + relationshipService.findByItemAndRelationshipType(context, person1, relationshipType, false, -1, -1, true) + ); + assertNoRelationship( + relationshipService.findByItemAndRelationshipType(context, person1, relationshipType, true, -1, -1, false) + ); + assertNoRelationship( + relationshipService.findByItemAndRelationshipType(context, person1, relationshipType, true, -1, -1, true) + ); + + assertEquals(1, relationshipService.countByItem(context, publication1)); + assertEquals(1, relationshipService.countByItem(context, person1)); + + assertEquals(1, relationshipService.countByItem(context, publication1, false)); + assertEquals(1, relationshipService.countByItem(context, publication1, true)); + assertEquals(1, relationshipService.countByItem(context, person1, false)); + assertEquals(1, relationshipService.countByItem(context, person1, true)); + + assertEquals( + 0, relationshipService.countByItemAndRelationshipType(context, publication1, relationshipType, false) + ); + assertEquals( + 1, relationshipService.countByItemAndRelationshipType(context, publication1, relationshipType, true) + ); + assertEquals( + 1, relationshipService.countByItemAndRelationshipType(context, person1, relationshipType, false) + ); + assertEquals( + 0, relationshipService.countByItemAndRelationshipType(context, person1, relationshipType, true) + ); + + assertEquals( + 0, relationshipService.countByItemAndRelationshipType(context, publication1, relationshipType, false, false) + ); + assertEquals( + 0, relationshipService.countByItemAndRelationshipType(context, publication1, relationshipType, false, true) + ); + assertEquals( + 1, relationshipService.countByItemAndRelationshipType(context, publication1, relationshipType, true, false) + ); + assertEquals( + 1, relationshipService.countByItemAndRelationshipType(context, publication1, relationshipType, true, true) + ); + assertEquals( + 1, relationshipService.countByItemAndRelationshipType(context, person1, relationshipType, false, false) + ); + assertEquals( + 1, relationshipService.countByItemAndRelationshipType(context, person1, relationshipType, false, true) + ); + assertEquals( + 0, relationshipService.countByItemAndRelationshipType(context, person1, relationshipType, true, false) + ); + assertEquals( + 0, relationshipService.countByItemAndRelationshipType(context, person1, relationshipType, true, true) + ); + } + + @Test + public void testExcludeNonLatestLeftOnly() throws Exception { + context.turnOffAuthorisationSystem(); + Relationship relationship1 = RelationshipBuilder + .createRelationshipBuilder(context, publication1, person1, relationshipType) + .withLatestVersionStatus(Relationship.LatestVersionStatus.LEFT_ONLY) + .build(); + context.restoreAuthSystemState(); + + assertRelationship( + relationship1, + relationshipDAO.findByItem(context, publication1, false, false) + ); + assertNoRelationship( + relationshipDAO.findByItem(context, publication1, false, true) + ); + assertRelationship( + relationship1, + relationshipDAO.findByItem(context, person1, false, false) + ); + assertRelationship( + relationship1, + relationshipDAO.findByItem(context, person1, false, true) + ); + + assertRelationship( + relationship1, + relationshipDAO.findByItem(context, publication1, -1, -1, false, false) + ); + assertNoRelationship( + relationshipDAO.findByItem(context, publication1, -1, -1, false, true) + ); + assertRelationship( + relationship1, + relationshipDAO.findByItem(context, person1, -1, -1, false, false) + ); + assertRelationship( + relationship1, + relationshipDAO.findByItem(context, person1, -1, -1, false, true) + ); + + assertEquals(1, relationshipDAO.countByItem(context, publication1, false)); + assertEquals(0, relationshipDAO.countByItem(context, publication1, true)); + assertEquals(1, relationshipDAO.countByItem(context, person1, false)); + assertEquals(1, relationshipDAO.countByItem(context, person1, true)); + + assertRelationship( + relationship1, + relationshipDAO.findByItemAndRelationshipType(context, publication1, relationshipType, -1, -1, false) + ); + assertNoRelationship( + relationshipDAO.findByItemAndRelationshipType(context, publication1, relationshipType, -1, -1, true) + ); + assertRelationship( + relationship1, + relationshipDAO.findByItemAndRelationshipType(context, person1, relationshipType, -1, -1, false) + ); + assertRelationship( + relationship1, + relationshipDAO.findByItemAndRelationshipType(context, person1, relationshipType, -1, -1, true) + ); + + assertNoRelationship( + relationshipDAO.findByItemAndRelationshipType(context, publication1, relationshipType, false, -1, -1, false) + ); + assertNoRelationship( + relationshipDAO.findByItemAndRelationshipType(context, publication1, relationshipType, false, -1, -1, true) + ); + assertRelationship( + relationship1, + relationshipDAO.findByItemAndRelationshipType(context, publication1, relationshipType, true, -1, -1, false) + ); + assertNoRelationship( + relationshipDAO.findByItemAndRelationshipType(context, publication1, relationshipType, true, -1, -1, true) + ); + assertRelationship( + relationship1, + relationshipDAO.findByItemAndRelationshipType(context, person1, relationshipType, false, -1, -1, false) + ); + assertRelationship( + relationship1, + relationshipDAO.findByItemAndRelationshipType(context, person1, relationshipType, false, -1, -1, true) + ); + assertNoRelationship( + relationshipDAO.findByItemAndRelationshipType(context, person1, relationshipType, true, -1, -1, false) + ); + assertNoRelationship( + relationshipDAO.findByItemAndRelationshipType(context, person1, relationshipType, true, -1, -1, true) + ); + + assertEquals( + 0, relationshipDAO.countByItemAndRelationshipType(context, publication1, relationshipType, false, false) + ); + assertEquals( + 0, relationshipDAO.countByItemAndRelationshipType(context, publication1, relationshipType, false, true) + ); + assertEquals( + 1, relationshipDAO.countByItemAndRelationshipType(context, publication1, relationshipType, true, false) + ); + assertEquals( + 0, relationshipDAO.countByItemAndRelationshipType(context, publication1, relationshipType, true, true) + ); + assertEquals( + 1, relationshipDAO.countByItemAndRelationshipType(context, person1, relationshipType, false, false) + ); + assertEquals( + 1, relationshipDAO.countByItemAndRelationshipType(context, person1, relationshipType, false, true) + ); + assertEquals( + 0, relationshipDAO.countByItemAndRelationshipType(context, person1, relationshipType, true, false) + ); + assertEquals( + 0, relationshipDAO.countByItemAndRelationshipType(context, person1, relationshipType, true, true) + ); + + assertNoRelationship( + relationshipService.findByItem(context, publication1) + ); + assertRelationship( + relationship1, + relationshipService.findByItem(context, person1) + ); + + assertNoRelationship( + relationshipService.findByItem(context, publication1, -1, -1, false) + ); + assertRelationship( + relationship1, + relationshipService.findByItem(context, person1, -1, -1, false) + ); + + assertRelationship( + relationship1, + relationshipService.findByItem(context, publication1, -1, -1, false, false) + ); + assertNoRelationship( + relationshipService.findByItem(context, publication1, -1, -1, false, true) + ); + assertRelationship( + relationship1, + relationshipService.findByItem(context, person1, -1, -1, false, false) + ); + assertRelationship( + relationship1, + relationshipService.findByItem(context, person1, -1, -1, false, true) + ); + + assertNoRelationship( + relationshipService.findByItemAndRelationshipType(context, publication1, relationshipType) + ); + assertRelationship( + relationship1, + relationshipService.findByItemAndRelationshipType(context, person1, relationshipType) + ); + + assertNoRelationship( + relationshipService.findByItemAndRelationshipType(context, publication1, relationshipType, -1, -1) + ); + assertRelationship( + relationship1, + relationshipService.findByItemAndRelationshipType(context, person1, relationshipType, -1, -1) + ); + + assertRelationship( + relationship1, + relationshipService.findByItemAndRelationshipType(context, publication1, relationshipType, -1, -1, false) + ); + assertNoRelationship( + relationshipService.findByItemAndRelationshipType(context, publication1, relationshipType, -1, -1, true) + ); + assertRelationship( + relationship1, + relationshipService.findByItemAndRelationshipType(context, person1, relationshipType, -1, -1, false) + ); + assertRelationship( + relationship1, + relationshipService.findByItemAndRelationshipType(context, person1, relationshipType, -1, -1, true) + ); + + assertNoRelationship( + relationshipService.findByItemAndRelationshipType(context, publication1, relationshipType, false, -1, -1) + ); + assertNoRelationship( + relationshipService.findByItemAndRelationshipType(context, publication1, relationshipType, true, -1, -1) + ); + assertRelationship( + relationship1, + relationshipService.findByItemAndRelationshipType(context, person1, relationshipType, false, -1, -1) + ); + assertNoRelationship( + relationshipService.findByItemAndRelationshipType(context, person1, relationshipType, true, -1, -1) + ); + + assertNoRelationship( + relationshipService + .findByItemAndRelationshipType(context, publication1, relationshipType, false, -1, -1, false) + ); + assertNoRelationship( + relationshipService + .findByItemAndRelationshipType(context, publication1, relationshipType, false, -1, -1, true) + ); + assertRelationship( + relationship1, + relationshipService + .findByItemAndRelationshipType(context, publication1, relationshipType, true, -1, -1, false) + ); + assertNoRelationship( + relationshipService + .findByItemAndRelationshipType(context, publication1, relationshipType, true, -1, -1, true) + ); + assertRelationship( + relationship1, + relationshipService.findByItemAndRelationshipType(context, person1, relationshipType, false, -1, -1, false) + ); + assertRelationship( + relationship1, + relationshipService.findByItemAndRelationshipType(context, person1, relationshipType, false, -1, -1, true) + ); + assertNoRelationship( + relationshipService.findByItemAndRelationshipType(context, person1, relationshipType, true, -1, -1, false) + ); + assertNoRelationship( + relationshipService.findByItemAndRelationshipType(context, person1, relationshipType, true, -1, -1, true) + ); + + assertEquals(0, relationshipService.countByItem(context, publication1)); + assertEquals(1, relationshipService.countByItem(context, person1)); + + assertEquals(1, relationshipService.countByItem(context, publication1, false)); + assertEquals(0, relationshipService.countByItem(context, publication1, true)); + assertEquals(1, relationshipService.countByItem(context, person1, false)); + assertEquals(1, relationshipService.countByItem(context, person1, true)); + + assertEquals( + 0, relationshipService.countByItemAndRelationshipType(context, publication1, relationshipType, false) + ); + assertEquals( + 0, relationshipService.countByItemAndRelationshipType(context, publication1, relationshipType, true) + ); + assertEquals( + 1, relationshipService.countByItemAndRelationshipType(context, person1, relationshipType, false) + ); + assertEquals( + 0, relationshipService.countByItemAndRelationshipType(context, person1, relationshipType, true) + ); + + assertEquals( + 0, relationshipService.countByItemAndRelationshipType(context, publication1, relationshipType, false, false) + ); + assertEquals( + 0, relationshipService.countByItemAndRelationshipType(context, publication1, relationshipType, false, true) + ); + assertEquals( + 1, relationshipService.countByItemAndRelationshipType(context, publication1, relationshipType, true, false) + ); + assertEquals( + 0, relationshipService.countByItemAndRelationshipType(context, publication1, relationshipType, true, true) + ); + assertEquals( + 1, relationshipService.countByItemAndRelationshipType(context, person1, relationshipType, false, false) + ); + assertEquals( + 1, relationshipService.countByItemAndRelationshipType(context, person1, relationshipType, false, true) + ); + assertEquals( + 0, relationshipService.countByItemAndRelationshipType(context, person1, relationshipType, true, false) + ); + assertEquals( + 0, relationshipService.countByItemAndRelationshipType(context, person1, relationshipType, true, true) + ); + } + + @Test + public void testExcludeNonLatestRightOnly() throws Exception { + context.turnOffAuthorisationSystem(); + Relationship relationship1 = RelationshipBuilder + .createRelationshipBuilder(context, publication1, person1, relationshipType) + .withLatestVersionStatus(Relationship.LatestVersionStatus.RIGHT_ONLY) + .build(); + context.restoreAuthSystemState(); + + assertRelationship( + relationship1, + relationshipDAO.findByItem(context, publication1, false, false) + ); + assertRelationship( + relationship1, + relationshipDAO.findByItem(context, publication1, false, true) + ); + assertRelationship( + relationship1, + relationshipDAO.findByItem(context, person1, false, false) + ); + assertNoRelationship( + relationshipDAO.findByItem(context, person1, false, true) + ); + + assertRelationship( + relationship1, + relationshipDAO.findByItem(context, publication1, -1, -1, false, false) + ); + assertRelationship( + relationship1, + relationshipDAO.findByItem(context, publication1, -1, -1, false, true) + ); + assertRelationship( + relationship1, + relationshipDAO.findByItem(context, person1, -1, -1, false, false) + ); + assertNoRelationship( + relationshipDAO.findByItem(context, person1, -1, -1, false, true) + ); + + assertEquals(1, relationshipDAO.countByItem(context, publication1, false)); + assertEquals(1, relationshipDAO.countByItem(context, publication1, true)); + assertEquals(1, relationshipDAO.countByItem(context, person1, false)); + assertEquals(0, relationshipDAO.countByItem(context, person1, true)); + + assertRelationship( + relationship1, + relationshipDAO.findByItemAndRelationshipType(context, publication1, relationshipType, -1, -1, false) + ); + assertRelationship( + relationship1, + relationshipDAO.findByItemAndRelationshipType(context, publication1, relationshipType, -1, -1, true) + ); + assertRelationship( + relationship1, + relationshipDAO.findByItemAndRelationshipType(context, person1, relationshipType, -1, -1, false) + ); + assertNoRelationship( + relationshipDAO.findByItemAndRelationshipType(context, person1, relationshipType, -1, -1, true) + ); + + assertNoRelationship( + relationshipDAO.findByItemAndRelationshipType(context, publication1, relationshipType, false, -1, -1, false) + ); + assertNoRelationship( + relationshipDAO.findByItemAndRelationshipType(context, publication1, relationshipType, false, -1, -1, true) + ); + assertRelationship( + relationship1, + relationshipDAO.findByItemAndRelationshipType(context, publication1, relationshipType, true, -1, -1, false) + ); + assertRelationship( + relationship1, + relationshipDAO.findByItemAndRelationshipType(context, publication1, relationshipType, true, -1, -1, true) + ); + assertRelationship( + relationship1, + relationshipDAO.findByItemAndRelationshipType(context, person1, relationshipType, false, -1, -1, false) + ); + assertNoRelationship( + relationshipDAO.findByItemAndRelationshipType(context, person1, relationshipType, false, -1, -1, true) + ); + assertNoRelationship( + relationshipDAO.findByItemAndRelationshipType(context, person1, relationshipType, true, -1, -1, false) + ); + assertNoRelationship( + relationshipDAO.findByItemAndRelationshipType(context, person1, relationshipType, true, -1, -1, true) + ); + + assertEquals( + 0, relationshipDAO.countByItemAndRelationshipType(context, publication1, relationshipType, false, false) + ); + assertEquals( + 0, relationshipDAO.countByItemAndRelationshipType(context, publication1, relationshipType, false, true) + ); + assertEquals( + 1, relationshipDAO.countByItemAndRelationshipType(context, publication1, relationshipType, true, false) + ); + assertEquals( + 1, relationshipDAO.countByItemAndRelationshipType(context, publication1, relationshipType, true, true) + ); + assertEquals( + 1, relationshipDAO.countByItemAndRelationshipType(context, person1, relationshipType, false, false) + ); + assertEquals( + 0, relationshipDAO.countByItemAndRelationshipType(context, person1, relationshipType, false, true) + ); + assertEquals( + 0, relationshipDAO.countByItemAndRelationshipType(context, person1, relationshipType, true, false) + ); + assertEquals( + 0, relationshipDAO.countByItemAndRelationshipType(context, person1, relationshipType, true, true) + ); + + assertRelationship( + relationship1, + relationshipService.findByItem(context, publication1) + ); + assertNoRelationship( + relationshipService.findByItem(context, person1) + ); + + assertRelationship( + relationship1, + relationshipService.findByItem(context, publication1, -1, -1, false) + ); + assertNoRelationship( + relationshipService.findByItem(context, person1, -1, -1, false) + ); + + assertRelationship( + relationship1, + relationshipService.findByItem(context, publication1, -1, -1, false, false) + ); + assertRelationship( + relationship1, + relationshipService.findByItem(context, publication1, -1, -1, false, true) + ); + assertRelationship( + relationship1, + relationshipService.findByItem(context, person1, -1, -1, false, false) + ); + assertNoRelationship( + relationshipService.findByItem(context, person1, -1, -1, false, true) + ); + + assertRelationship( + relationship1, + relationshipService.findByItemAndRelationshipType(context, publication1, relationshipType) + ); + assertNoRelationship( + relationshipService.findByItemAndRelationshipType(context, person1, relationshipType) + ); + + assertRelationship( + relationship1, + relationshipService.findByItemAndRelationshipType(context, publication1, relationshipType, -1, -1) + ); + assertNoRelationship( + relationshipService.findByItemAndRelationshipType(context, person1, relationshipType, -1, -1) + ); + + assertRelationship( + relationship1, + relationshipService.findByItemAndRelationshipType(context, publication1, relationshipType, -1, -1, false) + ); + assertRelationship( + relationship1, + relationshipService.findByItemAndRelationshipType(context, publication1, relationshipType, -1, -1, true) + ); + assertRelationship( + relationship1, + relationshipService.findByItemAndRelationshipType(context, person1, relationshipType, -1, -1, false) + ); + assertNoRelationship( + relationshipService.findByItemAndRelationshipType(context, person1, relationshipType, -1, -1, true) + ); + + assertNoRelationship( + relationshipService.findByItemAndRelationshipType(context, publication1, relationshipType, false, -1, -1) + ); + assertRelationship( + relationship1, + relationshipService.findByItemAndRelationshipType(context, publication1, relationshipType, true, -1, -1) + ); + assertNoRelationship( + relationshipService.findByItemAndRelationshipType(context, person1, relationshipType, false, -1, -1) + ); + assertNoRelationship( + relationshipService.findByItemAndRelationshipType(context, person1, relationshipType, true, -1, -1) + ); + + assertNoRelationship( + relationshipService + .findByItemAndRelationshipType(context, publication1, relationshipType, false, -1, -1, false) + ); + assertNoRelationship( + relationshipService + .findByItemAndRelationshipType(context, publication1, relationshipType, false, -1, -1, true) + ); + assertRelationship( + relationship1, + relationshipService + .findByItemAndRelationshipType(context, publication1, relationshipType, true, -1, -1, false) + ); + assertRelationship( + relationship1, + relationshipService + .findByItemAndRelationshipType(context, publication1, relationshipType, true, -1, -1, true) + ); + assertRelationship( + relationship1, + relationshipService.findByItemAndRelationshipType(context, person1, relationshipType, false, -1, -1, false) + ); + assertNoRelationship( + relationshipService.findByItemAndRelationshipType(context, person1, relationshipType, false, -1, -1, true) + ); + assertNoRelationship( + relationshipService.findByItemAndRelationshipType(context, person1, relationshipType, true, -1, -1, false) + ); + assertNoRelationship( + relationshipService.findByItemAndRelationshipType(context, person1, relationshipType, true, -1, -1, true) + ); + + assertEquals(1, relationshipService.countByItem(context, publication1)); + assertEquals(0, relationshipService.countByItem(context, person1)); + + assertEquals(1, relationshipService.countByItem(context, publication1, false)); + assertEquals(1, relationshipService.countByItem(context, publication1, true)); + assertEquals(1, relationshipService.countByItem(context, person1, false)); + assertEquals(0, relationshipService.countByItem(context, person1, true)); + + assertEquals( + 0, relationshipService.countByItemAndRelationshipType(context, publication1, relationshipType, false) + ); + assertEquals( + 1, relationshipService.countByItemAndRelationshipType(context, publication1, relationshipType, true) + ); + assertEquals( + 0, relationshipService.countByItemAndRelationshipType(context, person1, relationshipType, false) + ); + assertEquals( + 0, relationshipService.countByItemAndRelationshipType(context, person1, relationshipType, true) + ); + + assertEquals( + 0, relationshipService.countByItemAndRelationshipType(context, publication1, relationshipType, false, false) + ); + assertEquals( + 0, relationshipService.countByItemAndRelationshipType(context, publication1, relationshipType, false, true) + ); + assertEquals( + 1, relationshipService.countByItemAndRelationshipType(context, publication1, relationshipType, true, false) + ); + assertEquals( + 1, relationshipService.countByItemAndRelationshipType(context, publication1, relationshipType, true, true) + ); + assertEquals( + 1, relationshipService.countByItemAndRelationshipType(context, person1, relationshipType, false, false) + ); + assertEquals( + 0, relationshipService.countByItemAndRelationshipType(context, person1, relationshipType, false, true) + ); + assertEquals( + 0, relationshipService.countByItemAndRelationshipType(context, person1, relationshipType, true, false) + ); + assertEquals( + 0, relationshipService.countByItemAndRelationshipType(context, person1, relationshipType, true, true) + ); + } + } From 18e4fed9edf8f27fe2dc8051bef754623b2cbbe9 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Thu, 17 Feb 2022 14:51:00 -0600 Subject: [PATCH 0029/1846] Enable default value for solr.authority.server --- dspace/config/dspace.cfg | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index a7b957bef6e9..31b4f8a6ce3f 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -1413,9 +1413,12 @@ plugin.selfnamed.org.dspace.content.authority.ChoiceAuthority = \ ## See manual or org.dspace.content.authority.Choices source for descriptions. authority.minconfidence = ambiguous +# Solr Authority index location +# Default is ${solr.server}/authority, unless solr.multicorePrefix is specified +solr.authority.server=${solr.server}/${solr.multicorePrefix}authority + # Configuration settings for ORCID based authority control. -# Uncomment the lines below to enable configuration. -#solr.authority.server=${solr.server}/${solr.multicorePrefix}authority +# Uncomment the lines below to enable configuration #choices.plugin.dc.contributor.author = SolrAuthorAuthority #choices.presentation.dc.contributor.author = authorLookup #authority.controlled.dc.contributor.author = true From fb4eb05e5dc9d2fa13761ab7932ad791723161ea Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Thu, 3 Mar 2022 10:29:32 +0100 Subject: [PATCH 0030/1846] 88051: Fix tests --- ...RelationshipServiceImplVersioningTest.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/dspace-api/src/test/java/org/dspace/content/RelationshipServiceImplVersioningTest.java b/dspace-api/src/test/java/org/dspace/content/RelationshipServiceImplVersioningTest.java index 5ad52a3417d2..c4f718e8363e 100644 --- a/dspace-api/src/test/java/org/dspace/content/RelationshipServiceImplVersioningTest.java +++ b/dspace-api/src/test/java/org/dspace/content/RelationshipServiceImplVersioningTest.java @@ -131,6 +131,13 @@ public void testRelationshipLatestVersionStatusDefault() throws Exception { assertEquals(Relationship.LatestVersionStatus.BOTH, relationship5.getLatestVersionStatus()); Relationship relationship6 = relationshipService.find(context, relationship5.getID()); assertEquals(Relationship.LatestVersionStatus.BOTH, relationship6.getLatestVersionStatus()); + + // clean up + context.turnOffAuthorisationSystem(); + relationshipService.delete(context, relationship1); + relationshipService.delete(context, relationship3); + relationshipService.delete(context, relationship5); + context.restoreAuthSystemState(); } @Test @@ -158,6 +165,12 @@ public void testRelationshipLatestVersionStatusBoth() throws Exception { assertEquals(Relationship.LatestVersionStatus.BOTH, relationship3.getLatestVersionStatus()); Relationship relationship4 = relationshipService.find(context, relationship3.getID()); assertEquals(Relationship.LatestVersionStatus.BOTH, relationship4.getLatestVersionStatus()); + + // clean up + context.turnOffAuthorisationSystem(); + relationshipService.delete(context, relationship1); + relationshipService.delete(context, relationship3); + context.restoreAuthSystemState(); } @Test @@ -185,6 +198,12 @@ public void testRelationshipLatestVersionStatusLeftOnly() throws Exception { assertEquals(Relationship.LatestVersionStatus.LEFT_ONLY, relationship3.getLatestVersionStatus()); Relationship relationship4 = relationshipService.find(context, relationship3.getID()); assertEquals(Relationship.LatestVersionStatus.LEFT_ONLY, relationship4.getLatestVersionStatus()); + + // clean up + context.turnOffAuthorisationSystem(); + relationshipService.delete(context, relationship1); + relationshipService.delete(context, relationship3); + context.restoreAuthSystemState(); } @Test @@ -212,6 +231,12 @@ public void testRelationshipLatestVersionStatusRightOnly() throws Exception { assertEquals(Relationship.LatestVersionStatus.RIGHT_ONLY, relationship3.getLatestVersionStatus()); Relationship relationship4 = relationshipService.find(context, relationship3.getID()); assertEquals(Relationship.LatestVersionStatus.RIGHT_ONLY, relationship4.getLatestVersionStatus()); + + // clean up + context.turnOffAuthorisationSystem(); + relationshipService.delete(context, relationship1); + relationshipService.delete(context, relationship3); + context.restoreAuthSystemState(); } protected void assertRelationship(Relationship expectedRelationship, List relationships) { From e783576476f57e9eae7ad2f34df7b3e16ea672d2 Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Thu, 3 Mar 2022 14:35:04 +0100 Subject: [PATCH 0031/1846] 88051: Fix unnecessary stubbing exception --- .../java/org/dspace/content/RelationshipServiceImplTest.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/dspace-api/src/test/java/org/dspace/content/RelationshipServiceImplTest.java b/dspace-api/src/test/java/org/dspace/content/RelationshipServiceImplTest.java index fbcac3554fd0..4fb47376fb0e 100644 --- a/dspace-api/src/test/java/org/dspace/content/RelationshipServiceImplTest.java +++ b/dspace-api/src/test/java/org/dspace/content/RelationshipServiceImplTest.java @@ -112,9 +112,6 @@ public void testFindByItem() throws Exception { relationshipTest.add(getRelationship(bob, cindy, hasMother,1,0)); when(relationshipService.findByItem(context, cindy, -1, -1, false)).thenReturn(relationshipTest); - // Mock the state of objects utilized in findByItem() to meet the success criteria of the invocation - when(relationshipDAO.findByItem(context, cindy, -1, -1, false, false)).thenReturn(relationshipTest); - List results = relationshipService.findByItem(context, cindy); assertEquals("TestFindByItem 0", relationshipTest, results); for (int i = 0; i < relationshipTest.size(); i++) { From b912769580c732980af456f3170e79fce0906c48 Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Fri, 4 Mar 2022 16:10:42 +0100 Subject: [PATCH 0032/1846] 88056: Bugfix: do not copy virtual metadata to new version of items --- .../org/dspace/versioning/AbstractVersionProvider.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/versioning/AbstractVersionProvider.java b/dspace-api/src/main/java/org/dspace/versioning/AbstractVersionProvider.java index 8b0ca9aeb8d4..6c04657ab41c 100644 --- a/dspace-api/src/main/java/org/dspace/versioning/AbstractVersionProvider.java +++ b/dspace-api/src/main/java/org/dspace/versioning/AbstractVersionProvider.java @@ -12,6 +12,7 @@ import java.util.List; import java.util.Set; +import org.apache.commons.lang3.StringUtils; import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.service.AuthorizeService; @@ -24,6 +25,7 @@ import org.dspace.content.service.BitstreamService; import org.dspace.content.service.BundleService; import org.dspace.content.service.ItemService; +import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.storage.bitstore.service.BitstreamStorageService; import org.springframework.beans.factory.annotation.Autowired; @@ -55,8 +57,9 @@ protected void copyMetadata(Context context, Item itemNew, Item nativeItem) thro MetadataSchema metadataSchema = metadataField.getMetadataSchema(); String unqualifiedMetadataField = metadataSchema.getName() + "." + metadataField.getElement(); if (getIgnoredMetadataFields().contains(metadataField.toString('.')) || - getIgnoredMetadataFields().contains(unqualifiedMetadataField + "." + Item.ANY)) { - //Skip this metadata field + getIgnoredMetadataFields().contains(unqualifiedMetadataField + "." + Item.ANY) || + StringUtils.startsWith(aMd.getAuthority(), Constants.VIRTUAL_AUTHORITY_PREFIX)) { + //Skip this metadata field (ignored and/or virtual) continue; } From 272fd355353e2d75ba95ea3427b89450d053f383 Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Fri, 4 Mar 2022 16:13:49 +0100 Subject: [PATCH 0033/1846] 88056: Copy relationships when creating new version of item --- .../DefaultItemVersionProvider.java | 57 ++ .../versioning/ItemVersionProvider.java | 7 + .../VersioningWithRelationshipsTest.java | 490 ++++++++++++++++++ 3 files changed, 554 insertions(+) create mode 100644 dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java diff --git a/dspace-api/src/main/java/org/dspace/versioning/DefaultItemVersionProvider.java b/dspace-api/src/main/java/org/dspace/versioning/DefaultItemVersionProvider.java index 7903a49c3148..d4590ae24ea2 100644 --- a/dspace-api/src/main/java/org/dspace/versioning/DefaultItemVersionProvider.java +++ b/dspace-api/src/main/java/org/dspace/versioning/DefaultItemVersionProvider.java @@ -15,7 +15,9 @@ import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.ResourcePolicy; import org.dspace.content.Item; +import org.dspace.content.Relationship; import org.dspace.content.WorkspaceItem; +import org.dspace.content.service.RelationshipService; import org.dspace.content.service.WorkspaceItemService; import org.dspace.core.Context; import org.dspace.identifier.IdentifierException; @@ -44,6 +46,8 @@ public class DefaultItemVersionProvider extends AbstractVersionProvider implemen protected VersioningService versioningService; @Autowired(required = true) protected IdentifierService identifierService; + @Autowired(required = true) + protected RelationshipService relationshipService; @Override public Item createNewItemAndAddItInWorkspace(Context context, Item nativeItem) { @@ -89,10 +93,18 @@ public void deleteVersionedItem(Context c, Version versionToDelete, VersionHisto } } + /** + * Copy all data (minus a few exceptions) from the old item to the new item. + * @param c the DSpace context. + * @param itemNew the new version of the item. + * @param previousItem the old version of the item. + * @return the new version of the item, with data from the old item. + */ @Override public Item updateItemState(Context c, Item itemNew, Item previousItem) { try { copyMetadata(c, itemNew, previousItem); + copyRelationships(c, itemNew, previousItem); createBundlesAndAddBitstreams(c, itemNew, previousItem); try { identifierService.reserve(c, itemNew); @@ -114,4 +126,49 @@ public Item updateItemState(Context c, Item itemNew, Item previousItem) { throw new RuntimeException(e.getMessage(), e); } } + + /** + * Copy all relationships of the old item to the new item. + * At this point in the lifecycle of the item-version (before archival), only the opposite item receives + * "latest" status. On item archival of the item-version, the "latest" status of the relevant relationships + * will be updated. + * @param context the DSpace context. + * @param newItem the new version of the item. + * @param oldItem the old version of the item. + */ + protected void copyRelationships( + Context context, Item newItem, Item oldItem + ) throws SQLException, AuthorizeException { + List oldRelationships = relationshipService.findByItem(context, oldItem, -1, -1, false, true); + for (Relationship oldRelationship : oldRelationships) { + if (oldRelationship.getLeftItem().equals(oldItem)) { + // current item is on left side of this relationship + relationshipService.create( + context, + newItem, // new item + oldRelationship.getRightItem(), + oldRelationship.getRelationshipType(), + oldRelationship.getLeftPlace(), + oldRelationship.getRightPlace(), + oldRelationship.getLeftwardValue(), + oldRelationship.getRightwardValue(), + Relationship.LatestVersionStatus.RIGHT_ONLY // only mark the opposite side as "latest" for now + ); + } else if (oldRelationship.getRightItem().equals(oldItem)) { + // current item is on right side of this relationship + relationshipService.create( + context, + oldRelationship.getLeftItem(), + newItem, // new item + oldRelationship.getRelationshipType(), + oldRelationship.getLeftPlace(), + oldRelationship.getRightPlace(), + oldRelationship.getLeftwardValue(), + oldRelationship.getRightwardValue(), + Relationship.LatestVersionStatus.LEFT_ONLY // only mark the opposite side as "latest" for now + ); + } + } + } + } diff --git a/dspace-api/src/main/java/org/dspace/versioning/ItemVersionProvider.java b/dspace-api/src/main/java/org/dspace/versioning/ItemVersionProvider.java index 83369e04650d..74014b62626d 100644 --- a/dspace-api/src/main/java/org/dspace/versioning/ItemVersionProvider.java +++ b/dspace-api/src/main/java/org/dspace/versioning/ItemVersionProvider.java @@ -22,5 +22,12 @@ public interface ItemVersionProvider { public void deleteVersionedItem(Context c, Version versionToDelete, VersionHistory history) throws SQLException; + /** + * Copy all data (minus a few exceptions) from the old item to the new item. + * @param c the DSpace context. + * @param itemNew the new version of the item. + * @param previousItem the old version of the item. + * @return the new version of the item, with data from the old item. + */ public Item updateItemState(Context c, Item itemNew, Item previousItem); } diff --git a/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java b/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java new file mode 100644 index 000000000000..449749aa993c --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java @@ -0,0 +1,490 @@ +/** + * 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.content; + +import static org.dspace.content.Relationship.LatestVersionStatus; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasProperty; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertNotSame; + +import java.util.List; + +import org.dspace.AbstractIntegrationTestWithDatabase; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.EntityTypeBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.builder.RelationshipBuilder; +import org.dspace.builder.RelationshipTypeBuilder; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.RelationshipService; +import org.dspace.versioning.Version; +import org.dspace.versioning.factory.VersionServiceFactory; +import org.dspace.versioning.service.VersioningService; +import org.hamcrest.Matcher; +import org.junit.Before; +import org.junit.Test; + +public class VersioningWithRelationshipsTest extends AbstractIntegrationTestWithDatabase { + + private RelationshipService relationshipService; + private VersioningService versioningService; + + protected Community community; + protected Collection collection; + protected EntityType publicationEntityType; + protected EntityType personEntityType; + protected EntityType projectEntityType; + protected EntityType orgUnitEntityType; + protected RelationshipType isAuthorOfPublication; + protected RelationshipType isProjectOfPublication; + protected RelationshipType isOrgUnitOfPublication; + protected RelationshipType isMemberOfProject; + protected RelationshipType isMemberOfOrgUnit; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + + relationshipService = ContentServiceFactory.getInstance().getRelationshipService(); + versioningService = VersionServiceFactory.getInstance().getVersionService(); + + context.turnOffAuthorisationSystem(); + + community = CommunityBuilder.createCommunity(context) + .withName("community") + .build(); + + collection = CollectionBuilder.createCollection(context, community) + .withName("collection") + .build(); + + publicationEntityType = EntityTypeBuilder.createEntityTypeBuilder(context, "Publication") + .build(); + + personEntityType = EntityTypeBuilder.createEntityTypeBuilder(context, "Person") + .build(); + + projectEntityType = EntityTypeBuilder.createEntityTypeBuilder(context, "Project") + .build(); + + orgUnitEntityType = EntityTypeBuilder.createEntityTypeBuilder(context, "OrgUnit") + .build(); + + isAuthorOfPublication = RelationshipTypeBuilder.createRelationshipTypeBuilder( + context, publicationEntityType, personEntityType, + "isAuthorOfPublication", "isPublicationOfAuthor", + null, null, null, null + ) + .withCopyToLeft(false) + .withCopyToRight(false) + .build(); + + isProjectOfPublication = RelationshipTypeBuilder.createRelationshipTypeBuilder( + context, publicationEntityType, projectEntityType, + "isProjectOfPublication", "isPublicationOfProject", + 0, null, 0, null + ) + .withCopyToLeft(false) + .withCopyToRight(false) + .build(); + + isOrgUnitOfPublication = RelationshipTypeBuilder.createRelationshipTypeBuilder( + context, publicationEntityType, orgUnitEntityType, + "isOrgUnitOfPublication", "isPublicationOfOrgUnit", + 0, null, 0, null + ) + .withCopyToLeft(false) + .withCopyToRight(false) + .build(); + + isMemberOfProject = RelationshipTypeBuilder.createRelationshipTypeBuilder( + context, projectEntityType, personEntityType, + "isMemberOfProject", "isProjectOfMember", + null, null, null, null + ) + .withCopyToLeft(false) + .withCopyToRight(false) + .build(); + + isMemberOfOrgUnit = RelationshipTypeBuilder.createRelationshipTypeBuilder( + context, orgUnitEntityType, personEntityType, + "isMemberOfOrgUnit", "isOrgUnitOfMember", + null, null, null, null + ) + .withCopyToLeft(false) + .withCopyToRight(false) + .build(); + } + + protected Matcher isRelationship( + Item leftItem, RelationshipType relationshipType, Item rightItem, LatestVersionStatus latestVersionStatus + ) { + return allOf( + hasProperty("leftItem", is(leftItem)), + hasProperty("relationshipType", is(relationshipType)), + hasProperty("rightItem", is(rightItem)), + // NOTE: place is not checked + hasProperty("leftwardValue", nullValue()), + hasProperty("rightwardValue", nullValue()), + hasProperty("latestVersionStatus", is(latestVersionStatus)) + ); + } + + @Test + public void test_createNewVersionOfItemOnLeftSideOfRelationships() throws Exception { + /////////////////////////////////////////////// + // create a publication with 3 relationships // + /////////////////////////////////////////////// + + Item person1 = ItemBuilder.createItem(context, collection) + .withTitle("person 1") + .withMetadata("dspace", "entity", "type", personEntityType.getLabel()) + .build(); + + Item project1 = ItemBuilder.createItem(context, collection) + .withTitle("project 1") + .withMetadata("dspace", "entity", "type", projectEntityType.getLabel()) + .build(); + + Item orgUnit1 = ItemBuilder.createItem(context, collection) + .withTitle("org unit 1") + .withMetadata("dspace", "entity", "type", orgUnitEntityType.getLabel()) + .build(); + + Item originalPublication = ItemBuilder.createItem(context, collection) + .withTitle("original publication") + .withMetadata("dspace", "entity", "type", publicationEntityType.getLabel()) + .build(); + + RelationshipBuilder.createRelationshipBuilder(context, originalPublication, person1, isAuthorOfPublication) + .build(); + + RelationshipBuilder.createRelationshipBuilder(context, originalPublication, project1, isProjectOfPublication) + .build(); + + RelationshipBuilder.createRelationshipBuilder(context, originalPublication, orgUnit1, isOrgUnitOfPublication) + .build(); + + ///////////////////////////////////////////////////////// + // verify that the relationships were properly created // + ///////////////////////////////////////////////////////// + + assertThat( + relationshipService.findByItem(context, originalPublication, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH), + isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH), + isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, person1, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, project1, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, orgUnit1, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH) + )) + ); + + ///////////////////////////////////////////// + // create a new version of the publication // + ///////////////////////////////////////////// + + Version newVersion = versioningService.createNewVersion(context, originalPublication); + Item newPublication = newVersion.getItem(); + assertNotSame(originalPublication, newPublication); + + /////////////////////////////////////////////////////////////////////// + // verify the relationships of all 5 items (excludeNonLatest = true) // + /////////////////////////////////////////////////////////////////////// + + assertThat( + relationshipService.findByItem(context, originalPublication, -1, -1, false, true), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH), + isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH), + isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, person1, -1, -1, false, true), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, project1, -1, -1, false, true), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, orgUnit1, -1, -1, false, true), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, newPublication, -1, -1, false, true), + containsInAnyOrder(List.of( + isRelationship(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY), + isRelationship(newPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY), + isRelationship(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY) + )) + ); + + //////////////////////////////////////////////////////////////////////// + // verify the relationships of all 5 items (excludeNonLatest = false) // + //////////////////////////////////////////////////////////////////////// + + assertThat( + relationshipService.findByItem(context, originalPublication, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH), + isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH), + isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, person1, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH), + isRelationship(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY) + )) + ); + + assertThat( + relationshipService.findByItem(context, project1, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH), + isRelationship(newPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY) + )) + ); + + assertThat( + relationshipService.findByItem(context, orgUnit1, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH), + isRelationship(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY) + )) + ); + + assertThat( + relationshipService.findByItem(context, newPublication, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY), + isRelationship(newPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY), + isRelationship(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY) + )) + ); + + ////////////// + // clean up // + ////////////// + + versioningService.removeVersion(context, newPublication); + } + + @Test + public void test_createNewVersionOfItemOnRightSideOfRelationships() throws Exception { + /////////////////////////////////////////////// + // create a publication with 3 relationships // + /////////////////////////////////////////////// + + Item publication1 = ItemBuilder.createItem(context, collection) + .withTitle("publication 1") + .withMetadata("dspace", "entity", "type", publicationEntityType.getLabel()) + .build(); + + Item project1 = ItemBuilder.createItem(context, collection) + .withTitle("project 1") + .withMetadata("dspace", "entity", "type", projectEntityType.getLabel()) + .build(); + + Item orgUnit1 = ItemBuilder.createItem(context, collection) + .withTitle("org unit 1") + .withMetadata("dspace", "entity", "type", orgUnitEntityType.getLabel()) + .build(); + + Item originalPerson = ItemBuilder.createItem(context, collection) + .withTitle("original person") + .withMetadata("dspace", "entity", "type", personEntityType.getLabel()) + .build(); + + RelationshipBuilder.createRelationshipBuilder(context, publication1, originalPerson, isAuthorOfPublication) + .build(); + + RelationshipBuilder.createRelationshipBuilder(context, project1, originalPerson, isMemberOfProject) + .build(); + + RelationshipBuilder.createRelationshipBuilder(context, orgUnit1, originalPerson, isMemberOfOrgUnit) + .build(); + + ///////////////////////////////////////////////////////// + // verify that the relationships were properly created // + ///////////////////////////////////////////////////////// + + assertThat( + relationshipService.findByItem(context, originalPerson, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(publication1, isAuthorOfPublication, originalPerson, LatestVersionStatus.BOTH), + isRelationship(project1, isMemberOfProject, originalPerson, LatestVersionStatus.BOTH), + isRelationship(orgUnit1, isMemberOfOrgUnit, originalPerson, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, publication1, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(publication1, isAuthorOfPublication, originalPerson, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, project1, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(project1, isMemberOfProject, originalPerson, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, orgUnit1, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(orgUnit1, isMemberOfOrgUnit, originalPerson, LatestVersionStatus.BOTH) + )) + ); + + //////////////////////////////////////// + // create a new version of the person // + //////////////////////////////////////// + + Version newVersion = versioningService.createNewVersion(context, originalPerson); + Item newPerson = newVersion.getItem(); + assertNotSame(originalPerson, newPerson); + + /////////////////////////////////////////////////////////////////////// + // verify the relationships of all 5 items (excludeNonLatest = true) // + /////////////////////////////////////////////////////////////////////// + + assertThat( + relationshipService.findByItem(context, originalPerson, -1, -1, false, true), + containsInAnyOrder(List.of( + isRelationship(publication1, isAuthorOfPublication, originalPerson, LatestVersionStatus.BOTH), + isRelationship(project1, isMemberOfProject, originalPerson, LatestVersionStatus.BOTH), + isRelationship(orgUnit1, isMemberOfOrgUnit, originalPerson, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, publication1, -1, -1, false, true), + containsInAnyOrder(List.of( + isRelationship(publication1, isAuthorOfPublication, originalPerson, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, project1, -1, -1, false, true), + containsInAnyOrder(List.of( + isRelationship(project1, isMemberOfProject, originalPerson, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, orgUnit1, -1, -1, false, true), + containsInAnyOrder(List.of( + isRelationship(orgUnit1, isMemberOfOrgUnit, originalPerson, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, newPerson, -1, -1, false, true), + containsInAnyOrder(List.of( + isRelationship(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.LEFT_ONLY), + isRelationship(project1, isMemberOfProject, newPerson, LatestVersionStatus.LEFT_ONLY), + isRelationship(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.LEFT_ONLY) + )) + ); + + //////////////////////////////////////////////////////////////////////// + // verify the relationships of all 5 items (excludeNonLatest = false) // + //////////////////////////////////////////////////////////////////////// + + assertThat( + relationshipService.findByItem(context, originalPerson, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(publication1, isAuthorOfPublication, originalPerson, LatestVersionStatus.BOTH), + isRelationship(project1, isMemberOfProject, originalPerson, LatestVersionStatus.BOTH), + isRelationship(orgUnit1, isMemberOfOrgUnit, originalPerson, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, publication1, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(publication1, isAuthorOfPublication, originalPerson, LatestVersionStatus.BOTH), + isRelationship(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.LEFT_ONLY) + )) + ); + + assertThat( + relationshipService.findByItem(context, project1, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(project1, isMemberOfProject, originalPerson, LatestVersionStatus.BOTH), + isRelationship(project1, isMemberOfProject, newPerson, LatestVersionStatus.LEFT_ONLY) + )) + ); + + assertThat( + relationshipService.findByItem(context, orgUnit1, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(orgUnit1, isMemberOfOrgUnit, originalPerson, LatestVersionStatus.BOTH), + isRelationship(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.LEFT_ONLY) + )) + ); + + assertThat( + relationshipService.findByItem(context, newPerson, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.LEFT_ONLY), + isRelationship(project1, isMemberOfProject, newPerson, LatestVersionStatus.LEFT_ONLY), + isRelationship(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.LEFT_ONLY) + )) + ); + + ////////////// + // clean up // + ////////////// + + versioningService.removeVersion(context, newPerson); + } + +} From 85b27525659e6b245a1ab3693e3a1aa5fda0d203 Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Fri, 4 Mar 2022 17:31:04 +0100 Subject: [PATCH 0034/1846] 88146: Bugfix: Add excludeTilted to RelationshipDAO#countByItem, add note to docs --- .../content/RelationshipServiceImpl.java | 12 ++-- .../dspace/content/dao/RelationshipDAO.java | 3 +- .../content/dao/impl/RelationshipDAOImpl.java | 8 ++- .../content/service/RelationshipService.java | 15 +++- ...RelationshipServiceImplVersioningTest.java | 72 ++++++++++++------- .../ItemRelationshipLinkRepository.java | 4 +- 6 files changed, 76 insertions(+), 38 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/RelationshipServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/RelationshipServiceImpl.java index 52f5f966a2de..87389a839f51 100644 --- a/dspace-api/src/main/java/org/dspace/content/RelationshipServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/RelationshipServiceImpl.java @@ -301,10 +301,10 @@ public List findByItem( @Override public List findByItem( - Context context, Item item, Integer limit, Integer offset, boolean excludeTilted, boolean excludeNonLinked + Context context, Item item, Integer limit, Integer offset, boolean excludeTilted, boolean excludeNonLatest ) throws SQLException { List list = - relationshipDAO.findByItem(context, item, limit, offset, excludeTilted, excludeNonLinked); + relationshipDAO.findByItem(context, item, limit, offset, excludeTilted, excludeNonLatest); list.sort((o1, o2) -> { int relationshipType = o1.getRelationshipType().getLeftwardType() @@ -728,12 +728,14 @@ public int countTotal(Context context) throws SQLException { @Override public int countByItem(Context context, Item item) throws SQLException { - return countByItem(context, item, true); + return countByItem(context, item, false, true); } @Override - public int countByItem(Context context, Item item, boolean excludeNonLatest) throws SQLException { - return relationshipDAO.countByItem(context, item, excludeNonLatest); + public int countByItem( + Context context, Item item, boolean excludeTilted, boolean excludeNonLatest + ) throws SQLException { + return relationshipDAO.countByItem(context, item, excludeTilted, excludeNonLatest); } @Override diff --git a/dspace-api/src/main/java/org/dspace/content/dao/RelationshipDAO.java b/dspace-api/src/main/java/org/dspace/content/dao/RelationshipDAO.java index c7a0a032ede8..07edf5935081 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/RelationshipDAO.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/RelationshipDAO.java @@ -200,13 +200,14 @@ List findByTypeName(Context context, String typeName, Integer limi * @param context The relevant DSpace context * @param item The item that should be either a leftItem or a rightItem of all * the Relationship objects in the returned list + * @param excludeTilted if true, excludes tilted relationships * @param excludeNonLatest if true, exclude relationships for which the opposite item is not the latest version * that is relevant * @return The list of Relationship objects that contain either a left or a * right item that is equal to the given item * @throws SQLException If something goes wrong */ - int countByItem(Context context, Item item, boolean excludeNonLatest) throws SQLException; + int countByItem(Context context, Item item, boolean excludeTilted, boolean excludeNonLatest) throws SQLException; /** * Count total number of relationships (rows in relationship table) by an item and a relationship type and a boolean diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/RelationshipDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/RelationshipDAOImpl.java index acd5af1509a4..182d22903342 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/impl/RelationshipDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/RelationshipDAOImpl.java @@ -159,7 +159,9 @@ protected Predicate getRightItemPredicate( } @Override - public int countByItem(Context context, Item item, boolean excludeNonLatest) throws SQLException { + public int countByItem( + Context context, Item item, boolean excludeTilted, boolean excludeNonLatest + ) throws SQLException { CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, Relationship.class); Root relationshipRoot = criteriaQuery.from(Relationship.class); @@ -167,8 +169,8 @@ public int countByItem(Context context, Item item, boolean excludeNonLatest) thr criteriaQuery.where( criteriaBuilder.or( - getLeftItemPredicate(criteriaBuilder, relationshipRoot, item, false, excludeNonLatest), - getRightItemPredicate(criteriaBuilder, relationshipRoot, item, false, excludeNonLatest) + getLeftItemPredicate(criteriaBuilder, relationshipRoot, item, excludeTilted, excludeNonLatest), + getRightItemPredicate(criteriaBuilder, relationshipRoot, item, excludeTilted, excludeNonLatest) ) ); diff --git a/dspace-api/src/main/java/org/dspace/content/service/RelationshipService.java b/dspace-api/src/main/java/org/dspace/content/service/RelationshipService.java index f54e6f07f88d..e58bcca85cdd 100644 --- a/dspace-api/src/main/java/org/dspace/content/service/RelationshipService.java +++ b/dspace-api/src/main/java/org/dspace/content/service/RelationshipService.java @@ -123,6 +123,7 @@ List findByItem( /** * This method returns a list of Relationships for which the leftItem or rightItem is equal to the given * Item object and for which the RelationshipType object is equal to the relationshipType property + * NOTE: tilted relationships are NEVER excluded when fetching one relationship type * @param context The relevant DSpace context * @param item The Item object to be matched on the leftItem or rightItem for the relationship * @param relationshipType The RelationshipType object that will be used to check the Relationship on @@ -137,6 +138,7 @@ public List findByItemAndRelationshipType(Context context, Item it /** * This method returns a list of Relationships for which the leftItem or rightItem is equal to the given * Item object and for which the RelationshipType object is equal to the relationshipType property + * NOTE: tilted relationships are NEVER excluded when fetching one relationship type * @param context The relevant DSpace context * @param item The Item object to be matched on the leftItem or rightItem for the relationship * @param relationshipType The RelationshipType object that will be used to check the Relationship on @@ -151,6 +153,7 @@ public List findByItemAndRelationshipType(Context context, Item it /** * This method returns a list of Relationships for which the leftItem or rightItem is equal to the given * Item object and for which the RelationshipType object is equal to the relationshipType property + * NOTE: tilted relationships are NEVER excluded when fetching one relationship type * @param context The relevant DSpace context * @param item The Item object to be matched on the leftItem or rightItem for the relationship * @param relationshipType The RelationshipType object that will be used to check the Relationship on @@ -167,6 +170,7 @@ public List findByItemAndRelationshipType( /** * This method returns a list of Relationships for which the leftItem or rightItem is equal to the given * Item object and for which the RelationshipType object is equal to the relationshipType property + * NOTE: tilted relationships are NEVER excluded when fetching one relationship type * @param context The relevant DSpace context * @param item The Item object to be matched on the leftItem or rightItem for the relationship * @param relationshipType The RelationshipType object that will be used to check the Relationship on @@ -183,6 +187,7 @@ public List findByItemAndRelationshipType(Context context, Item it /** * This method returns a list of Relationships for which the leftItem or rightItem is equal to the given * Item object and for which the RelationshipType object is equal to the relationshipType property + * NOTE: tilted relationships are NEVER excluded when fetching one relationship type * @param context The relevant DSpace context * @param item The Item object to be matched on the leftItem or rightItem for the relationship * @param relationshipType The RelationshipType object that will be used to check the Relationship on @@ -228,6 +233,7 @@ public void updatePlaceInRelationship(Context context, Relationship relationship /** * This method returns a list of Relationship objects for which the relationshipType property is equal to the given * RelationshipType object + * NOTE: tilted relationships are NEVER excluded when fetching one relationship type * @param context The relevant DSpace context * @param relationshipType The RelationshipType object that will be used to check the Relationship on * @return The list of Relationship objects for which the given RelationshipType object is equal @@ -239,6 +245,7 @@ public void updatePlaceInRelationship(Context context, Relationship relationship /** * This method returns a list of Relationship objets for which the relationshipType property is equal to the given * RelationshipType object + * NOTE: tilted relationships are NEVER excluded when fetching one relationship type * @param context The relevant DSpace context * @param relationshipType The RelationshipType object that will be used to check the Relationship on * @param limit paging limit @@ -343,7 +350,7 @@ List findByTypeName(Context context, String typeName, Integer limi /** * Count total number of relationships (rows in relationship table) by a relationship type - * + * NOTE: tilted relationships are NEVER excluded when fetching one relationship type * @param context context * @param relationshipType relationship type to filter by * @return total count @@ -369,18 +376,19 @@ List findByTypeName(Context context, String typeName, Integer limi * @param context The relevant DSpace context * @param item The item that should be either a leftItem or a rightItem of all * the Relationship objects in the returned list + * @param excludeTilted if true, excludes tilted relationships * @param excludeNonLatest if true, exclude relationships for which the opposite item is not the latest version * that is relevant * @return The list of Relationship objects that contain either a left or a * right item that is equal to the given item * @throws SQLException If something goes wrong */ - int countByItem(Context context, Item item, boolean excludeNonLatest) throws SQLException; + int countByItem(Context context, Item item, boolean excludeTilted, boolean excludeNonLatest) throws SQLException; /** * Count total number of relationships (rows in relationship table) by a relationship type and a boolean indicating * whether the relationship should contain the item on the left side or not - * + * NOTE: tilted relationships are NEVER excluded when fetching one relationship type * @param context context * @param relationshipType relationship type to filter by * @param isLeft Indicating whether the counted Relationships should have the given Item on the left side or not @@ -393,6 +401,7 @@ int countByItemAndRelationshipType(Context context, Item item, RelationshipType /** * Count total number of relationships (rows in relationship table) by a relationship type and a boolean indicating * whether the relationship should contain the item on the left side or not + * NOTE: tilted relationships are NEVER excluded when fetching one relationship type * @param context context * @param relationshipType relationship type to filter by * @param isLeft Indicating whether the counted Relationships should have the given Item on the left side diff --git a/dspace-api/src/test/java/org/dspace/content/RelationshipServiceImplVersioningTest.java b/dspace-api/src/test/java/org/dspace/content/RelationshipServiceImplVersioningTest.java index c4f718e8363e..d42213da2cf8 100644 --- a/dspace-api/src/test/java/org/dspace/content/RelationshipServiceImplVersioningTest.java +++ b/dspace-api/src/test/java/org/dspace/content/RelationshipServiceImplVersioningTest.java @@ -293,10 +293,14 @@ public void testExcludeNonLatestBoth() throws Exception { relationshipDAO.findByItem(context, person1, -1, -1, false, true) ); - assertEquals(1, relationshipDAO.countByItem(context, publication1, false)); - assertEquals(1, relationshipDAO.countByItem(context, publication1, true)); - assertEquals(1, relationshipDAO.countByItem(context, person1, false)); - assertEquals(1, relationshipDAO.countByItem(context, person1, true)); + assertEquals(1, relationshipDAO.countByItem(context, publication1, false, false)); + assertEquals(1, relationshipDAO.countByItem(context, publication1, false, true)); + assertEquals(1, relationshipDAO.countByItem(context, person1, false, false)); + assertEquals(1, relationshipDAO.countByItem(context, person1, false, true)); + assertEquals(1, relationshipDAO.countByItem(context, publication1, true, false)); + assertEquals(1, relationshipDAO.countByItem(context, publication1, true, true)); + assertEquals(1, relationshipDAO.countByItem(context, person1, true, false)); + assertEquals(1, relationshipDAO.countByItem(context, person1, true, true)); assertRelationship( relationship1, @@ -490,10 +494,14 @@ public void testExcludeNonLatestBoth() throws Exception { assertEquals(1, relationshipService.countByItem(context, publication1)); assertEquals(1, relationshipService.countByItem(context, person1)); - assertEquals(1, relationshipService.countByItem(context, publication1, false)); - assertEquals(1, relationshipService.countByItem(context, publication1, true)); - assertEquals(1, relationshipService.countByItem(context, person1, false)); - assertEquals(1, relationshipService.countByItem(context, person1, true)); + assertEquals(1, relationshipService.countByItem(context, publication1, false, false)); + assertEquals(1, relationshipService.countByItem(context, publication1, false, true)); + assertEquals(1, relationshipService.countByItem(context, person1, false, false)); + assertEquals(1, relationshipService.countByItem(context, person1, false, true)); + assertEquals(1, relationshipService.countByItem(context, publication1, true, false)); + assertEquals(1, relationshipService.countByItem(context, publication1, true, true)); + assertEquals(1, relationshipService.countByItem(context, person1, true, false)); + assertEquals(1, relationshipService.countByItem(context, person1, true, true)); assertEquals( 0, relationshipService.countByItemAndRelationshipType(context, publication1, relationshipType, false) @@ -575,10 +583,14 @@ public void testExcludeNonLatestLeftOnly() throws Exception { relationshipDAO.findByItem(context, person1, -1, -1, false, true) ); - assertEquals(1, relationshipDAO.countByItem(context, publication1, false)); - assertEquals(0, relationshipDAO.countByItem(context, publication1, true)); - assertEquals(1, relationshipDAO.countByItem(context, person1, false)); - assertEquals(1, relationshipDAO.countByItem(context, person1, true)); + assertEquals(1, relationshipDAO.countByItem(context, publication1, false, false)); + assertEquals(0, relationshipDAO.countByItem(context, publication1, false, true)); + assertEquals(1, relationshipDAO.countByItem(context, person1, false, false)); + assertEquals(1, relationshipDAO.countByItem(context, person1, false, true)); + assertEquals(1, relationshipDAO.countByItem(context, publication1, true, false)); + assertEquals(0, relationshipDAO.countByItem(context, publication1, true, true)); + assertEquals(1, relationshipDAO.countByItem(context, person1, true, false)); + assertEquals(1, relationshipDAO.countByItem(context, person1, true, true)); assertRelationship( relationship1, @@ -762,10 +774,14 @@ public void testExcludeNonLatestLeftOnly() throws Exception { assertEquals(0, relationshipService.countByItem(context, publication1)); assertEquals(1, relationshipService.countByItem(context, person1)); - assertEquals(1, relationshipService.countByItem(context, publication1, false)); - assertEquals(0, relationshipService.countByItem(context, publication1, true)); - assertEquals(1, relationshipService.countByItem(context, person1, false)); - assertEquals(1, relationshipService.countByItem(context, person1, true)); + assertEquals(1, relationshipService.countByItem(context, publication1, false, false)); + assertEquals(0, relationshipService.countByItem(context, publication1, false, true)); + assertEquals(1, relationshipService.countByItem(context, person1, false, false)); + assertEquals(1, relationshipService.countByItem(context, person1, false, true)); + assertEquals(1, relationshipService.countByItem(context, publication1, true, false)); + assertEquals(0, relationshipService.countByItem(context, publication1, true, true)); + assertEquals(1, relationshipService.countByItem(context, person1, true, false)); + assertEquals(1, relationshipService.countByItem(context, person1, true, true)); assertEquals( 0, relationshipService.countByItemAndRelationshipType(context, publication1, relationshipType, false) @@ -847,10 +863,14 @@ public void testExcludeNonLatestRightOnly() throws Exception { relationshipDAO.findByItem(context, person1, -1, -1, false, true) ); - assertEquals(1, relationshipDAO.countByItem(context, publication1, false)); - assertEquals(1, relationshipDAO.countByItem(context, publication1, true)); - assertEquals(1, relationshipDAO.countByItem(context, person1, false)); - assertEquals(0, relationshipDAO.countByItem(context, person1, true)); + assertEquals(1, relationshipDAO.countByItem(context, publication1, false, false)); + assertEquals(1, relationshipDAO.countByItem(context, publication1, false, true)); + assertEquals(1, relationshipDAO.countByItem(context, person1, false, false)); + assertEquals(0, relationshipDAO.countByItem(context, person1, false, true)); + assertEquals(1, relationshipDAO.countByItem(context, publication1, true, false)); + assertEquals(1, relationshipDAO.countByItem(context, publication1, true, true)); + assertEquals(1, relationshipDAO.countByItem(context, person1, true, false)); + assertEquals(0, relationshipDAO.countByItem(context, person1, true, true)); assertRelationship( relationship1, @@ -1034,10 +1054,14 @@ public void testExcludeNonLatestRightOnly() throws Exception { assertEquals(1, relationshipService.countByItem(context, publication1)); assertEquals(0, relationshipService.countByItem(context, person1)); - assertEquals(1, relationshipService.countByItem(context, publication1, false)); - assertEquals(1, relationshipService.countByItem(context, publication1, true)); - assertEquals(1, relationshipService.countByItem(context, person1, false)); - assertEquals(0, relationshipService.countByItem(context, person1, true)); + assertEquals(1, relationshipService.countByItem(context, publication1, false, false)); + assertEquals(1, relationshipService.countByItem(context, publication1, false, true)); + assertEquals(1, relationshipService.countByItem(context, person1, false, false)); + assertEquals(0, relationshipService.countByItem(context, person1, false, true)); + assertEquals(1, relationshipService.countByItem(context, publication1, true, false)); + assertEquals(1, relationshipService.countByItem(context, publication1, true, true)); + assertEquals(1, relationshipService.countByItem(context, person1, true, false)); + assertEquals(0, relationshipService.countByItem(context, person1, true, true)); assertEquals( 0, relationshipService.countByItemAndRelationshipType(context, publication1, relationshipType, false) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ItemRelationshipLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ItemRelationshipLinkRepository.java index 3eae86361a4a..4a282ee46693 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ItemRelationshipLinkRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ItemRelationshipLinkRepository.java @@ -52,10 +52,10 @@ public Page getRelationships(@Nullable HttpServletRequest requ if (item == null) { throw new ResourceNotFoundException("No such item: " + itemId); } - int total = relationshipService.countByItem(context, item); + int total = relationshipService.countByItem(context, item, true, true); Pageable pageable = utils.getPageable(optionalPageable); List relationships = relationshipService.findByItem(context, item, - pageable.getPageSize(), Math.toIntExact(pageable.getOffset()), true); + pageable.getPageSize(), Math.toIntExact(pageable.getOffset()), true, true); return converter.toRestPage(relationships, pageable, total, projection); } catch (SQLException e) { throw new RuntimeException(e); From 696fcae777f43d22ffec0b7e53ad3644811a2954 Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Mon, 7 Mar 2022 12:30:30 +0100 Subject: [PATCH 0035/1846] 87994: Refactor reset file after retrieved & only put in byteArray if with coverpage --- .../app/rest/BitstreamRestController.java | 5 +- .../app/rest/utils/BitstreamResource.java | 62 +++++++++++-------- 2 files changed, 37 insertions(+), 30 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamRestController.java index 42f06639284e..183aee83d0e5 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamRestController.java @@ -132,7 +132,7 @@ public ResponseEntity retrieve(@PathVariable UUID uuid, HttpServletResponse resp try { long filesize = bit.getSizeBytes(); - var citationEnabledForBitstream = citationDocumentService.isCitationEnabledForBitstream(bit, context); + Boolean citationEnabledForBitstream = citationDocumentService.isCitationEnabledForBitstream(bit, context); HttpHeadersInitializer httpHeadersInitializer = new HttpHeadersInitializer() .withBufferSize(BUFFER_SIZE) @@ -152,10 +152,9 @@ public ResponseEntity retrieve(@PathVariable UUID uuid, HttpServletResponse resp httpHeadersInitializer.withDisposition(HttpHeadersInitializer.CONTENT_DISPOSITION_ATTACHMENT); } - org.dspace.app.rest.utils.BitstreamResource bitstreamResource = new org.dspace.app.rest.utils.BitstreamResource( - bit, name, uuid, currentUser != null ? currentUser.getID() : null, citationEnabledForBitstream); + name, uuid, currentUser != null ? currentUser.getID() : null, citationEnabledForBitstream); //We have all the data we need, close the connection to the database so that it doesn't stay open during //download/streaming diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamResource.java index 6de71fed59a2..694e6a254a4a 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamResource.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamResource.java @@ -35,44 +35,46 @@ */ public class BitstreamResource extends AbstractResource { - private Bitstream bitstream; private String name; private UUID uuid; private UUID currentUserUUID; private boolean shouldGenerateCoverPage; private byte[] file; - private Long fileSize; private BitstreamService bitstreamService = ContentServiceFactory.getInstance().getBitstreamService(); private EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService(); private CitationDocumentService citationDocumentService = new DSpace().getServiceManager() - .getServicesByType(CitationDocumentService.class).get(0); + .getServicesByType(CitationDocumentService.class).get(0); - public BitstreamResource(Bitstream bitstream, String name, UUID uuid, UUID currentUserUUID, + public BitstreamResource(String name, UUID uuid, UUID currentUserUUID, boolean shouldGenerateCoverPage) { - this.bitstream = bitstream; this.name = name; this.uuid = uuid; this.currentUserUUID = currentUserUUID; this.shouldGenerateCoverPage = shouldGenerateCoverPage; } - private Pair getFileData(Context context, Bitstream bitstream) throws SQLException, - AuthorizeException, IOException { - if (file == null || fileSize == null) { - if (shouldGenerateCoverPage) { - var citedDocument = citationDocumentService.makeCitedDocument(context, bitstream); + /** + * Get Potential cover page by array, this method should only be called when a coverpage should be generated + * In case of failure the original file will be returned + * + * @param context the DSpace context + * @param bitstream the pdf for which we want to generate a coverpage + * @return a byte array containing the cover page + */ + private byte[] getCoverpageByteArray(Context context, Bitstream bitstream) + throws IOException, SQLException, AuthorizeException { + if (file == null) { + try { + Pair citedDocument = citationDocumentService.makeCitedDocument(context, bitstream); this.file = citedDocument.getLeft(); - this.fileSize = citedDocument.getRight(); - } else { - var inputStream = bitstreamService.retrieve(context, bitstream); - this.file = IOUtils.toByteArray(inputStream); - inputStream.close(); - this.fileSize = bitstream.getSizeBytes(); + } catch (Exception e) { + // Return the original bitstream without the cover page + this.file = IOUtils.toByteArray(bitstreamService.retrieve(context, bitstream)); } } - return Pair.of(file, fileSize); + return file; } @Override @@ -82,20 +84,22 @@ public String getDescription() { @Override public InputStream getInputStream() throws IOException { - Context context = new Context(); - try { + try (Context context = new Context()) { EPerson currentUser = ePersonService.find(context, currentUserUUID); context.setCurrentUser(currentUser); Bitstream bitstream = bitstreamService.find(context, uuid); - return new ByteArrayInputStream(getFileData(context, bitstream).getLeft()); + InputStream out; + + if (shouldGenerateCoverPage) { + out = new ByteArrayInputStream(getCoverpageByteArray(context, bitstream)); + } else { + out = bitstreamService.retrieve(context, bitstream); + } + + this.file = null; + return out; } catch (SQLException | AuthorizeException e) { throw new IOException(e); - } finally { - try { - context.complete(); - } catch (SQLException e) { - throw new IOException(e); - } } } @@ -110,7 +114,11 @@ public long contentLength() throws IOException { EPerson currentUser = ePersonService.find(context, currentUserUUID); context.setCurrentUser(currentUser); Bitstream bitstream = bitstreamService.find(context, uuid); - return getFileData(context, bitstream).getRight(); + if (shouldGenerateCoverPage) { + return getCoverpageByteArray(context, bitstream).length; + } else { + return bitstream.getSizeBytes(); + } } catch (SQLException | AuthorizeException e) { throw new IOException(e); } From b142601c62869a8b8af8ccf24fcb7a11b5fed880 Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Mon, 7 Mar 2022 14:17:26 +0100 Subject: [PATCH 0036/1846] 88056: Filter relationship metadata by class instance instead of authority key --- .../java/org/dspace/versioning/AbstractVersionProvider.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/versioning/AbstractVersionProvider.java b/dspace-api/src/main/java/org/dspace/versioning/AbstractVersionProvider.java index 6c04657ab41c..3633f1949b46 100644 --- a/dspace-api/src/main/java/org/dspace/versioning/AbstractVersionProvider.java +++ b/dspace-api/src/main/java/org/dspace/versioning/AbstractVersionProvider.java @@ -12,7 +12,6 @@ import java.util.List; import java.util.Set; -import org.apache.commons.lang3.StringUtils; import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.service.AuthorizeService; @@ -22,10 +21,10 @@ import org.dspace.content.MetadataField; import org.dspace.content.MetadataSchema; import org.dspace.content.MetadataValue; +import org.dspace.content.RelationshipMetadataValue; import org.dspace.content.service.BitstreamService; import org.dspace.content.service.BundleService; import org.dspace.content.service.ItemService; -import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.storage.bitstore.service.BitstreamStorageService; import org.springframework.beans.factory.annotation.Autowired; @@ -58,7 +57,7 @@ protected void copyMetadata(Context context, Item itemNew, Item nativeItem) thro String unqualifiedMetadataField = metadataSchema.getName() + "." + metadataField.getElement(); if (getIgnoredMetadataFields().contains(metadataField.toString('.')) || getIgnoredMetadataFields().contains(unqualifiedMetadataField + "." + Item.ANY) || - StringUtils.startsWith(aMd.getAuthority(), Constants.VIRTUAL_AUTHORITY_PREFIX)) { + aMd instanceof RelationshipMetadataValue) { //Skip this metadata field (ignored and/or virtual) continue; } From 2e4f920d24fafced53fd5679ebc31296f2303279 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 7 Mar 2022 15:03:18 -0600 Subject: [PATCH 0037/1846] Move "solr.authority.server" to solrauthority.cfg --- dspace/config/dspace.cfg | 4 ---- dspace/config/modules/solrauthority.cfg | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index 31b4f8a6ce3f..168c86d64a75 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -1413,10 +1413,6 @@ plugin.selfnamed.org.dspace.content.authority.ChoiceAuthority = \ ## See manual or org.dspace.content.authority.Choices source for descriptions. authority.minconfidence = ambiguous -# Solr Authority index location -# Default is ${solr.server}/authority, unless solr.multicorePrefix is specified -solr.authority.server=${solr.server}/${solr.multicorePrefix}authority - # Configuration settings for ORCID based authority control. # Uncomment the lines below to enable configuration #choices.plugin.dc.contributor.author = SolrAuthorAuthority diff --git a/dspace/config/modules/solrauthority.cfg b/dspace/config/modules/solrauthority.cfg index 9ed1855d44ad..afa1bdd22a12 100644 --- a/dspace/config/modules/solrauthority.cfg +++ b/dspace/config/modules/solrauthority.cfg @@ -4,6 +4,10 @@ # These configs are only used by the SOLR authority index # #---------------------------------------------------------------# +# Solr Authority index location +# Default is ${solr.server}/authority, unless solr.multicorePrefix is specified +solr.authority.server=${solr.server}/${solr.multicorePrefix}authority + # Update item metadata displayed values (not the authority keys) # with the lasted cached versions when running the UpdateAuthorities index script #solrauthority.auto-update-items=false From 204d9f0047f514c94bd312a66363b7b336b0c1ba Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Tue, 8 Mar 2022 11:39:04 +0100 Subject: [PATCH 0038/1846] 88146: Fix tests --- ...eftTiltedRelationshipRestRepositoryIT.java | 164 ++++++++++++++++++ .../rest/RelationshipRestRepositoryIT.java | 8 +- 2 files changed, 170 insertions(+), 2 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/LeftTiltedRelationshipRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/LeftTiltedRelationshipRestRepositoryIT.java index e50d0f4654fb..480ca7b18d97 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/LeftTiltedRelationshipRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/LeftTiltedRelationshipRestRepositoryIT.java @@ -7,16 +7,25 @@ */ package org.dspace.app.rest; +import static com.jayway.jsonpath.JsonPath.read; import static org.dspace.app.rest.matcher.MetadataMatcher.matchMetadata; +import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.util.concurrent.atomic.AtomicReference; + +import org.dspace.builder.ItemBuilder; import org.dspace.builder.RelationshipBuilder; +import org.dspace.content.Item; import org.dspace.content.MetadataSchemaEnum; import org.dspace.content.RelationshipType; import org.junit.Before; import org.junit.Test; +import org.springframework.http.MediaType; /** * This class carries out the same test cases as {@link RelationshipRestRepositoryIT}. @@ -75,4 +84,159 @@ public void testIsAuthorOfPublicationRelationshipMetadataViaREST() throws Except ))); } + /** + * This method will test the deletion of a Relationship and will then + * verify that the relation is removed + * @throws Exception + */ + @Override + @Test + public void deleteRelationship() throws Exception { + context.turnOffAuthorisationSystem(); + + Item author2 = ItemBuilder.createItem(context, col1) + .withTitle("Author2") + .withIssueDate("2016-02-13") + .withPersonIdentifierFirstName("Maria") + .withPersonIdentifierLastName("Smith") + .build(); + + String adminToken = getAuthToken(admin.getEmail(), password); + + // First create 1 relationship. + context.restoreAuthSystemState(); + + AtomicReference idRef1 = new AtomicReference<>(); + AtomicReference idRef2 = new AtomicReference<>(); + try { + // This post request will add a first relationship to the publication and thus create + // a first set of metadata for the author values, namely "Donald Smith" + getClient(adminToken).perform(post("/api/core/relationships") + .param("relationshipType", + isAuthorOfPublicationRelationshipType.getID() + .toString()) + .contentType(MediaType.parseMediaType + (org.springframework.data.rest.webmvc.RestMediaTypes + .TEXT_URI_LIST_VALUE)) + .content( + "https://localhost:8080/spring-rest/api/core" + + "/items/" + publication1 + .getID() + "\n" + + "https://localhost:8080/spring-rest/api/core" + + "/items/" + author1 + .getID())) + .andExpect(status().isCreated()) + .andDo(result -> idRef1.set(read(result.getResponse().getContentAsString(), "$.id"))); + + // This test checks that there's one relationship on the publication + getClient(adminToken).perform(get("/api/core/items/" + + publication1.getID() + "/relationships")) + .andExpect(status().isOk()) + .andExpect(jsonPath("page.totalElements", is(1))); + + // This test checks that there's NO relationship on the first author + // NOTE: relationship is excluded because of tilted left + getClient(adminToken).perform(get("/api/core/items/" + + author1.getID() + "/relationships")) + .andExpect(status().isOk()) + .andExpect(jsonPath("page.totalElements", is(0))); + + // This test checks that there's no relationship on the second author + getClient(adminToken).perform(get("/api/core/items/" + + author2.getID() + "/relationships")) + .andExpect(status().isOk()) + .andExpect(jsonPath("page.totalElements", is(0))); + + // Creates another Relationship for the Publication + getClient(adminToken).perform(post("/api/core/relationships") + .param("relationshipType", + isAuthorOfPublicationRelationshipType.getID() + .toString()) + .contentType(MediaType.parseMediaType + (org.springframework.data.rest.webmvc.RestMediaTypes + .TEXT_URI_LIST_VALUE)) + .content( + "https://localhost:8080/spring-rest/api/core/items/" + publication1 + .getID() + "\n" + + "https://localhost:8080/spring-rest/api/core/items/" + author2 + .getID())) + .andExpect(status().isCreated()) + .andDo(result -> idRef2.set(read(result.getResponse().getContentAsString(), "$.id"))); + + // This test checks that there are 2 relationships on the publication + getClient(adminToken).perform(get("/api/core/items/" + + publication1.getID() + "/relationships")) + .andExpect(status().isOk()) + .andExpect(jsonPath("page.totalElements", is(2))); + + // This test checks that there's no relationship on the first author + // NOTE: relationship is excluded because of tilted left + getClient(adminToken).perform(get("/api/core/items/" + + author1.getID() + "/relationships")) + .andExpect(status().isOk()) + .andExpect(jsonPath("page.totalElements", is(0))); + + // This test checks that there's no relationship on the second author + // NOTE: relationship is excluded because of tilted left + getClient(adminToken).perform(get("/api/core/items/" + + author2.getID() + "/relationships")) + .andExpect(status().isOk()) + .andExpect(jsonPath("page.totalElements", is(0))); + + + // Now we delete the first relationship + getClient(adminToken).perform(delete("/api/core/relationships/" + idRef1)); + + + // This test checks that there's one relationship on the publication + getClient(adminToken).perform(get("/api/core/items/" + + publication1.getID() + "/relationships")) + .andExpect(status().isOk()) + .andExpect(jsonPath("page.totalElements", is(1))); + + // This test checks that there's no relationship on the first author + getClient(adminToken).perform(get("/api/core/items/" + + author1.getID() + "/relationships")) + .andExpect(status().isOk()) + .andExpect(jsonPath("page.totalElements", is(0))); + + // This test checks that there's no relationship on the second author + // NOTE: relationship is excluded because of tilted left + getClient(adminToken).perform(get("/api/core/items/" + + author2.getID() + "/relationships")) + .andExpect(status().isOk()) + .andExpect(jsonPath("page.totalElements", is(0))); + + + // Now we delete the second relationship + getClient(adminToken).perform(delete("/api/core/relationships/" + idRef2)); + + + // This test checks that there's no relationship on the publication + getClient(adminToken).perform(get("/api/core/items/" + + publication1.getID() + "/relationships")) + .andExpect(status().isOk()) + .andExpect(jsonPath("page.totalElements", is(0))); + + // This test checks that there's no relationship on the first author + getClient(adminToken).perform(get("/api/core/items/" + + author1.getID() + "/relationships")) + .andExpect(status().isOk()) + .andExpect(jsonPath("page.totalElements", is(0))); + + // This test checks that there are no relationship on the second author + getClient(adminToken).perform(get("/api/core/items/" + + author2.getID() + "/relationships")) + .andExpect(status().isOk()) + .andExpect(jsonPath("page.totalElements", is(0))); + } finally { + if (idRef1.get() != null) { + RelationshipBuilder.deleteRelationship(idRef1.get()); + } + if (idRef2.get() != null) { + RelationshipBuilder.deleteRelationship(idRef2.get()); + } + } + } + } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RelationshipRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RelationshipRestRepositoryIT.java index cdd9c7f2c309..7ec5a065e9d2 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RelationshipRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RelationshipRestRepositoryIT.java @@ -1524,8 +1524,12 @@ public void deleteRelationship() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("page.totalElements", is(0))); } finally { - RelationshipBuilder.deleteRelationship(idRef1.get()); - RelationshipBuilder.deleteRelationship(idRef2.get()); + if (idRef1.get() != null) { + RelationshipBuilder.deleteRelationship(idRef1.get()); + } + if (idRef2.get() != null) { + RelationshipBuilder.deleteRelationship(idRef2.get()); + } } } From 2d7a52dc005323df0d41cc723c5063a72a44469d Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Tue, 8 Mar 2022 13:24:53 +0100 Subject: [PATCH 0039/1846] 88141: Scripts and processes bug #7992 --- .../app/rest/converter/ScriptConverter.java | 23 ++++++++- .../config/spring/rest/scripts.xml | 8 +++- .../app/rest/ScriptRestRepositoryIT.java | 43 +++++++++++++++++ .../app/scripts/TypeConversionTestScript.java | 33 +++++++++++++ ...TypeConversionTestScriptConfiguration.java | 48 +++++++++++++++++++ 5 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/scripts/TypeConversionTestScript.java create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/scripts/TypeConversionTestScriptConfiguration.java diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ScriptConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ScriptConverter.java index 105975355b2a..9c9957a55bd3 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ScriptConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ScriptConverter.java @@ -12,6 +12,7 @@ import org.apache.commons.cli.Option; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang.StringUtils; import org.dspace.app.rest.model.ParameterRest; import org.dspace.app.rest.model.ScriptRest; import org.dspace.app.rest.projection.Projection; @@ -39,7 +40,7 @@ public ScriptRest convert(ScriptConfiguration scriptConfiguration, Projection pr parameterRest.setDescription(option.getDescription()); parameterRest.setName((option.getOpt() != null ? "-" + option.getOpt() : "--" + option.getLongOpt())); parameterRest.setNameLong(option.getLongOpt() != null ? "--" + option.getLongOpt() : null); - parameterRest.setType(((Class) option.getType()).getSimpleName()); + parameterRest.setType(getType(option)); parameterRest.setMandatory(option.isRequired()); parameterRestList.add(parameterRest); } @@ -48,6 +49,26 @@ public ScriptRest convert(ScriptConfiguration scriptConfiguration, Projection pr return scriptRest; } + /** + * Retrieve the type string for this option + * + * String is the default option class when no alternative is set. However, DSpace angular will force an argument + * when the type is set to string. Therefor, this method will return the boolean type when the option is of type + * string and does require an argument. + * + * @param option Option to retrieve the type for + * @return the type of the option based on the aforementioned logic + */ + private String getType(Option option) { + String simpleName = ((Class) option.getType()).getSimpleName(); + if (StringUtils.equalsIgnoreCase(simpleName, "string")) { + if (!option.hasArg()) { + return boolean.class.getSimpleName(); + } + } + return simpleName; + } + @Override public Class getModelClass() { return ScriptConfiguration.class; diff --git a/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/rest/scripts.xml b/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/rest/scripts.xml index ab9826b84d74..4e88ef2763d8 100644 --- a/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/rest/scripts.xml +++ b/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/rest/scripts.xml @@ -27,4 +27,10 @@ - \ No newline at end of file + + + + + + + diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScriptRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScriptRestRepositoryIT.java index 9976fb4be19b..48cd26227c7f 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScriptRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScriptRestRepositoryIT.java @@ -8,6 +8,8 @@ package org.dspace.app.rest; import static com.jayway.jsonpath.JsonPath.read; +import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; +import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; @@ -484,6 +486,47 @@ public void postProcessAdminWithFileSuccess() throws Exception { } } + @Test + public void scriptTypeConversionTest() throws Exception { + String token = getAuthToken(admin.getEmail(), password); + + getClient(token).perform(get("/api/system/scripts/type-conversion-test")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", ScriptMatcher + .matchScript("type-conversion-test", + "Test the type conversion different option types"))) + .andExpect(jsonPath("$.parameters", containsInAnyOrder( + allOf( + hasJsonPath("$.name", is("-b")), + hasJsonPath("$.description", is("option set to the boolean class")), + hasJsonPath("$.type", is("boolean")), + hasJsonPath("$.mandatory", is(false)), + hasJsonPath("$.nameLong", is("--boolean")) + ), + allOf( + hasJsonPath("$.name", is("-s")), + hasJsonPath("$.description", is("string option with an argument")), + hasJsonPath("$.type", is("String")), + hasJsonPath("$.mandatory", is(false)), + hasJsonPath("$.nameLong", is("--string")) + ), + allOf( + hasJsonPath("$.name", is("-n")), + hasJsonPath("$.description", is("string option without an argument")), + hasJsonPath("$.type", is("boolean")), + hasJsonPath("$.mandatory", is(false)), + hasJsonPath("$.nameLong", is("--noargument")) + ), + allOf( + hasJsonPath("$.name", is("-f")), + hasJsonPath("$.description", is("file option with an argument")), + hasJsonPath("$.type", is("InputStream")), + hasJsonPath("$.mandatory", is(false)), + hasJsonPath("$.nameLong", is("--file")) + ) + ) )); + } + @After public void destroy() throws Exception { diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/scripts/TypeConversionTestScript.java b/dspace-server-webapp/src/test/java/org/dspace/app/scripts/TypeConversionTestScript.java new file mode 100644 index 000000000000..824e5dda9f37 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/scripts/TypeConversionTestScript.java @@ -0,0 +1,33 @@ +/** + * 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.scripts; + +import org.apache.commons.cli.ParseException; +import org.dspace.app.rest.converter.ScriptConverter; +import org.dspace.scripts.DSpaceRunnable; +import org.dspace.utils.DSpace; + +/** + * Script used to test the type conversion in the {@link ScriptConverter} + */ +public class TypeConversionTestScript extends DSpaceRunnable { + + + public TypeConversionTestScriptConfiguration getScriptConfiguration() { + return new DSpace().getServiceManager() + .getServiceByName("type-conversion-test", TypeConversionTestScriptConfiguration.class); + } + + public void setup() throws ParseException { + // This script is only used to test rest exposure, no setup is required. + } + + public void internalRun() throws Exception { + // This script is only used to test rest exposure, no internal run is required. + } +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/scripts/TypeConversionTestScriptConfiguration.java b/dspace-server-webapp/src/test/java/org/dspace/app/scripts/TypeConversionTestScriptConfiguration.java new file mode 100644 index 000000000000..5738748e0b51 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/scripts/TypeConversionTestScriptConfiguration.java @@ -0,0 +1,48 @@ +/** + * 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.scripts; + +import java.io.InputStream; + +import org.apache.commons.cli.Options; +import org.dspace.app.rest.converter.ScriptConverter; +import org.dspace.core.Context; +import org.dspace.scripts.configuration.ScriptConfiguration; + +/** + * Script configuration used to test the type conversion in the {@link ScriptConverter} + */ +public class TypeConversionTestScriptConfiguration extends ScriptConfiguration { + + + public Class getDspaceRunnableClass() { + return null; + } + + public void setDspaceRunnableClass(final Class dspaceRunnableClass) { + + } + + public boolean isAllowedToExecute(final Context context) { + return true; + } + + public Options getOptions() { + + Options options = new Options(); + + options.addOption("b", "boolean", false, "option set to the boolean class"); + options.getOption("b").setType(boolean.class); + options.addOption("s", "string", true, "string option with an argument"); + options.addOption("n", "noargument", false, "string option without an argument"); + options.addOption("f", "file", true, "file option with an argument"); + options.getOption("f").setType(InputStream.class); + + return options; + } +} From 020ebbd3d161e68cb35167912238a1081cf1e5f6 Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Wed, 9 Mar 2022 10:55:09 +0100 Subject: [PATCH 0040/1846] 88061: Refactor VersioningConsumer --- .../dspace/versioning/VersioningConsumer.java | 105 +++++++++++++----- 1 file changed, 75 insertions(+), 30 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/versioning/VersioningConsumer.java b/dspace-api/src/main/java/org/dspace/versioning/VersioningConsumer.java index 6683419844e1..11eb977f9639 100644 --- a/dspace-api/src/main/java/org/dspace/versioning/VersioningConsumer.java +++ b/dspace-api/src/main/java/org/dspace/versioning/VersioningConsumer.java @@ -10,7 +10,10 @@ import java.util.HashSet; import java.util.Set; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.content.Item; +import org.dspace.content.Relationship; import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.service.ItemService; import org.dspace.core.Constants; @@ -19,26 +22,28 @@ import org.dspace.event.Event; import org.dspace.versioning.factory.VersionServiceFactory; import org.dspace.versioning.service.VersionHistoryService; -import org.dspace.versioning.service.VersioningService; /** + * When a new version of an item is published, unarchive the previous version and + * update {@link Relationship#latestVersionStatus} of the relevant relationships. + * * @author Fabio Bolognesi (fabio at atmire dot com) * @author Mark Diggory (markd at atmire dot com) * @author Ben Bosman (ben at atmire dot com) */ public class VersioningConsumer implements Consumer { - private static Set itemsToProcess; + private static final Logger log = LogManager.getLogger(VersioningConsumer.class); + + private Set itemsToProcess; private VersionHistoryService versionHistoryService; - private VersioningService versioningService; private ItemService itemService; @Override public void initialize() throws Exception { versionHistoryService = VersionServiceFactory.getInstance().getVersionHistoryService(); - versioningService = VersionServiceFactory.getInstance().getVersionService(); itemService = ContentServiceFactory.getInstance().getItemService(); } @@ -49,35 +54,75 @@ public void finish(Context ctx) throws Exception { @Override public void consume(Context ctx, Event event) throws Exception { if (itemsToProcess == null) { - itemsToProcess = new HashSet(); + itemsToProcess = new HashSet<>(); } - int st = event.getSubjectType(); - int et = event.getEventType(); - - if (st == Constants.ITEM && et == Event.INSTALL) { - Item item = (Item) event.getSubject(ctx); - if (item != null && item.isArchived()) { - VersionHistory history = versionHistoryService.findByItem(ctx, item); - if (history != null) { - Version latest = versionHistoryService.getLatestVersion(ctx, history); - Version previous = versionHistoryService.getPrevious(ctx, history, latest); - if (previous != null) { - Item previousItem = previous.getItem(); - if (previousItem != null) { - previousItem.setArchived(false); - itemsToProcess.add(previousItem); - //Fire a new modify event for our previous item - //Due to the need to reindex the item in the search - //and browse index we need to fire a new event - ctx.addEvent(new Event(Event.MODIFY, - previousItem.getType(), previousItem.getID(), - null, itemService.getIdentifiers(ctx, previousItem))); - } - } - } - } + // only items + if (event.getSubjectType() != Constants.ITEM) { + return; + } + + // only install events + if (event.getEventType() != Event.INSTALL) { + return; + } + + // get the item (should be archived) + Item item = (Item) event.getSubject(ctx); + if (item == null || !item.isArchived()) { + return; + } + + // get version history + VersionHistory history = versionHistoryService.findByItem(ctx, item); + if (history == null) { + return; + } + + // get latest version + Version latestVersion = versionHistoryService.getLatestVersion(ctx, history); + if (latestVersion == null) { + return; + } + + // get previous version + Version previousVersion = versionHistoryService.getPrevious(ctx, history, latestVersion); + if (previousVersion == null) { + return; } + + // get latest item + Item latestItem = latestVersion.getItem(); + if (latestItem == null) { + String msg = String.format( + "Illegal state: Obtained version history of item with uuid %s, handle %s, but the latest item is null", + item.getID(), item.getHandle() + ); + log.error(msg); + throw new IllegalStateException(msg); + } + + // get previous item + Item previousItem = previousVersion.getItem(); + if (previousItem == null) { + return; + } + + // unarchive previous item + unarchiveItem(ctx, previousItem); + + // TODO implement w2p 88061 + } + + protected void unarchiveItem(Context ctx, Item item) { + item.setArchived(false); + itemsToProcess.add(item); + //Fire a new modify event for our previous item + //Due to the need to reindex the item in the search + //and browse index we need to fire a new event + ctx.addEvent(new Event( + Event.MODIFY, item.getType(), item.getID(), null, itemService.getIdentifiers(ctx, item) + )); } @Override From 4d8a81d3bd6fde6c02f074e7c1604b143f4b3a5e Mon Sep 17 00:00:00 2001 From: Antoine Snyers Date: Wed, 9 Mar 2022 14:35:00 +0100 Subject: [PATCH 0041/1846] Filter browse-by-item lists instead of jumping to prefix --- .../java/org/dspace/browse/BrowseEngine.java | 16 +++++++++------- .../java/org/dspace/browse/SolrBrowseDAO.java | 3 +++ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/browse/BrowseEngine.java b/dspace-api/src/main/java/org/dspace/browse/BrowseEngine.java index 302d46eb0df4..cd6058d99b43 100644 --- a/dspace-api/src/main/java/org/dspace/browse/BrowseEngine.java +++ b/dspace-api/src/main/java/org/dspace/browse/BrowseEngine.java @@ -203,6 +203,8 @@ private BrowseInfo browseByItem(BrowserScope bs) // get the table name that we are going to be getting our data from dao.setTable(browseIndex.getTableName()); + dao.setStartsWith(StringUtils.lowerCase(scope.getStartsWith())); + // tell the browse query whether we are ascending or descending on the value dao.setAscending(scope.isAscending()); @@ -249,9 +251,6 @@ private BrowseInfo browseByItem(BrowserScope bs) } } - // this is the total number of results in answer to the query - int total = getTotalResults(); - // assemble the ORDER BY clause String orderBy = browseIndex.getSortField(scope.isSecondLevel()); if (scope.getSortBy() > 0) { @@ -259,6 +258,9 @@ private BrowseInfo browseByItem(BrowserScope bs) } dao.setOrderField(orderBy); + // this is the total number of results in answer to the query + int total = getTotalResults(); + int offset = scope.getOffset(); String rawFocusValue = null; if (offset < 1 && (scope.hasJumpToItem() || scope.hasJumpToValue() || scope.hasStartsWith())) { @@ -272,7 +274,7 @@ private BrowseInfo browseByItem(BrowserScope bs) log.debug("browsing using focus: " + focusValue); // Convert the focus value into an offset - offset = getOffsetForValue(focusValue); + // offset = getOffsetForValue(focusValue); } dao.setOffset(offset); @@ -685,13 +687,13 @@ private int getTotalResults(boolean distinct) // our count, storing them locally to reinstate later String focusField = dao.getJumpToField(); String focusValue = dao.getJumpToValue(); - String orderField = dao.getOrderField(); + // String orderField = dao.getOrderField(); int limit = dao.getLimit(); int offset = dao.getOffset(); dao.setJumpToField(null); dao.setJumpToValue(null); - dao.setOrderField(null); + // dao.setOrderField(null); dao.setLimit(-1); dao.setOffset(-1); @@ -701,7 +703,7 @@ private int getTotalResults(boolean distinct) // now put back the values we removed for this method dao.setJumpToField(focusField); dao.setJumpToValue(focusValue); - dao.setOrderField(orderField); + // dao.setOrderField(orderField); dao.setLimit(limit); dao.setOffset(offset); dao.setCountValues(null); diff --git a/dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java b/dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java index 6a960e8d75ea..391ed9077162 100644 --- a/dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java +++ b/dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java @@ -205,6 +205,9 @@ private DiscoverResult getSolrResponse() throws BrowseException { } else if (valuePartial) { query.addFilterQueries("{!field f=" + facetField + "_partial}" + value); } + if (StringUtils.isNotBlank(startsWith) && orderField != null) { + query.addFilterQueries("bi_" + orderField + "_sort:" + startsWith + "*"); + } // filter on item to be sure to don't include any other object // indexed in the Discovery Search core query.addFilterQueries("search.resourcetype:" + IndexableItem.TYPE); From 0b9b16a14a8eb3c46c472c77a9098b9dfa3bc7db Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Wed, 9 Mar 2022 19:37:10 +0100 Subject: [PATCH 0042/1846] 88061: Implement relationship latest version status change (untested) --- .../dspace/versioning/VersioningConsumer.java | 356 +++++++++++++++++- 1 file changed, 354 insertions(+), 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/versioning/VersioningConsumer.java b/dspace-api/src/main/java/org/dspace/versioning/VersioningConsumer.java index 11eb977f9639..9314bad850aa 100644 --- a/dspace-api/src/main/java/org/dspace/versioning/VersioningConsumer.java +++ b/dspace-api/src/main/java/org/dspace/versioning/VersioningConsumer.java @@ -7,15 +7,27 @@ */ package org.dspace.versioning; +import java.sql.SQLException; import java.util.HashSet; +import java.util.List; import java.util.Set; +import java.util.stream.Collectors; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.content.EntityType; import org.dspace.content.Item; +import org.dspace.content.MetadataValue; import org.dspace.content.Relationship; +import org.dspace.content.Relationship.LatestVersionStatus; +import org.dspace.content.RelationshipType; import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.EntityTypeService; import org.dspace.content.service.ItemService; +import org.dspace.content.service.RelationshipService; +import org.dspace.content.service.RelationshipTypeService; import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.event.Consumer; @@ -39,12 +51,17 @@ public class VersioningConsumer implements Consumer { private VersionHistoryService versionHistoryService; private ItemService itemService; - + private EntityTypeService entityTypeService; + private RelationshipTypeService relationshipTypeService; + private RelationshipService relationshipService; @Override public void initialize() throws Exception { versionHistoryService = VersionServiceFactory.getInstance().getVersionHistoryService(); itemService = ContentServiceFactory.getInstance().getItemService(); + entityTypeService = ContentServiceFactory.getInstance().getEntityTypeService(); + relationshipTypeService = ContentServiceFactory.getInstance().getRelationshipTypeService(); + relationshipService = ContentServiceFactory.getInstance().getRelationshipService(); } @Override @@ -111,7 +128,8 @@ public void consume(Context ctx, Event event) throws Exception { // unarchive previous item unarchiveItem(ctx, previousItem); - // TODO implement w2p 88061 + // update relationships + updateRelationships(ctx, latestItem, previousItem); } protected void unarchiveItem(Context ctx, Item item) { @@ -125,6 +143,340 @@ protected void unarchiveItem(Context ctx, Item item) { )); } + protected void updateRelationships(Context ctx, Item latestItem, Item previousItem) { + // check that the entity types of both items match + if (!doEntityTypesMatch(latestItem, previousItem)) { + return; + } + + // get the entity type (same for both items) + EntityType entityType = getEntityType(ctx, latestItem); + if (entityType == null) { + return; + } + + // get all relationship types that are linked to the given entity type + List relationshipTypes = getRelationshipTypes(ctx, entityType); + if (CollectionUtils.isEmpty(relationshipTypes)) { + return; + } + + for (RelationshipType relationshipType : relationshipTypes) { + List latestItemRelationships = getAllRelationships(ctx, latestItem, relationshipType); + if (latestItemRelationships == null) { + continue; + } + + List previousItemRelationships = getAllRelationships(ctx, previousItem, relationshipType); + if (previousItemRelationships == null) { + continue; + } + + for (Relationship previousItemRelationship : previousItemRelationships) { + // determine on which side of the relationship the latest and previous item should be + boolean isLeft = previousItem.equals(previousItemRelationship.getLeftItem()); + boolean isRight = previousItem.equals(previousItemRelationship.getRightItem()); + if (isLeft == isRight) { + Item leftItem = previousItemRelationship.getLeftItem(); + Item rightItem = previousItemRelationship.getRightItem(); + String msg = String.format( + "Illegal state: could not determine side of item with uuid %s, handle %s in " + + "relationship with id %s, rightward name %s between " + + "left item with uuid %s, handle %s and right item with uuid %s, handle %s", + previousItem.getID(), previousItem.getHandle(), previousItemRelationship.getID(), + previousItemRelationship.getRelationshipType().getRightwardType(), + leftItem.getID(), leftItem.getHandle(), rightItem.getID(), rightItem.getHandle() + ); + log.error(msg); + throw new IllegalStateException(msg); + } + + // get the matching relationship on the latest item + Relationship latestItemRelationship = + getMatchingRelationship(latestItem, isLeft, previousItemRelationship, latestItemRelationships); + + // for sure set the previous item to non-latest + // NOTE: if no matching relationship exists, this relationship will be considered deleted + // when viewed from the other item + updateLatestVersionStatus(previousItemRelationship, isLeft, false); + + // set the new item to latest if the relevant relationship exists + if (latestItemRelationship != null) { + updateLatestVersionStatus(latestItemRelationship, isLeft, true); + } + } + } + } + + /** + * Given two items, check if their entity types match. + * If one or both items don't have an entity type, comparing is pointless and this method will return false. + * @param latestItem the item that represents the most recent version. + * @param previousItem the item that represents the second-most recent version. + * @return true if the entity types of both items are non-null and equal, false otherwise. + */ + protected boolean doEntityTypesMatch(Item latestItem, Item previousItem) { + String latestItemEntityType = getEntityType(latestItem); + String previousItemEntityType = getEntityType(previousItem); + + // check if both items have an entity type + if (latestItemEntityType == null || previousItemEntityType == null) { + if (previousItemEntityType != null) { + log.warn(String.format( + "Inconsistency: Item with uuid %s, handle %s has NO entity type, " + + "but the previous version of that item with uuid %s, handle %s has entity type %s", + latestItem.getID(), latestItem.getHandle(), + previousItem.getID(), previousItem.getHandle(), previousItemEntityType + )); + } + + // one or both items do not have an entity type, so comparing is pointless + return false; + } + + // check if the entity types are equal + if (!StringUtils.equals(latestItemEntityType, previousItemEntityType)) { + log.warn(String.format( + "Inconsistency: Item with uuid %s, handle %s has entity type %s, " + + "but the previous version of that item with uuid %s, handle %s has entity type %s", + latestItem.getID(), latestItem.getHandle(), latestItemEntityType, + previousItem.getID(), previousItem.getHandle(), previousItemEntityType + )); + return false; + } + + // success - the entity types of both items are non-null and equal + log.info(String.format( + "Item with uuid %s, handle %s and the previous version of that item with uuid %s, handle %s " + + "have the same entity type: %s", + latestItem.getID(), latestItem.getHandle(), previousItem.getID(), previousItem.getHandle(), + latestItemEntityType + )); + return true; + } + + /** + * Get the entity type (stored in metadata field dspace.entity.type) of any item. + * @param item the item. + * @return the label of the entity type. + */ + protected String getEntityType(Item item) { + List mdvs = itemService.getMetadata(item, "dspace", "entity", "type", Item.ANY, false); + if (mdvs.isEmpty()) { + return null; + } + if (mdvs.size() > 1) { + log.warn(String.format( + "Item with uuid %s, handle %s has %s entity types (%s), expected 1 entity type", + item.getID(), item.getHandle(), mdvs.size(), + mdvs.stream().map(MetadataValue::getValue).collect(Collectors.toUnmodifiableList()) + )); + } + + String entityType = mdvs.get(0).getValue(); + if (StringUtils.isBlank(entityType)) { + return null; + } + + return entityType; + } + + /** + * Get the entity type (stored in metadata field dspace.entity.type) of any item. + * @param item the item. + * @return the entity type. + */ + protected EntityType getEntityType(Context ctx, Item item) { + String entityTypeStr = getEntityType(item); + if (entityTypeStr == null) { + return null; + } + + try { + return entityTypeService.findByEntityType(ctx, entityTypeStr); + } catch (SQLException e) { + log.error(String.format( + "Exception occurred when trying to obtain entity type with label %s of item with uuid %s, handle %s", + entityTypeStr, item.getID(), item.getHandle() + ), e); + return null; + } + } + + /** + * Get all relationship types that have the given entity type on their left and/or right side. + * @param ctx the DSpace context. + * @param entityType the entity type for which all relationship types should be found. + * @return a list of relationship types (possibly empty), or null in case of error. + */ + protected List getRelationshipTypes(Context ctx, EntityType entityType) { + try { + return relationshipTypeService.findByEntityType(ctx, entityType); + } catch (SQLException e) { + log.error(String.format( + "Exception occurred when trying to obtain relationship types via entity type with id %s, label %s", + entityType.getID(), entityType.getLabel() + ), e); + return null; + } + } + + /** + * Get all relationships of the given type linked to the given item. + * @param ctx the DSpace context. + * @param item the item. + * @param relationshipType the relationship type. + * @return a list of relationships (possibly empty), or null in case of error. + */ + protected List getAllRelationships(Context ctx, Item item, RelationshipType relationshipType) { + try { + return relationshipService.findByItemAndRelationshipType(ctx, item, relationshipType, -1, -1, false); + } catch (SQLException e) { + log.error(String.format( + "Exception occurred when trying to obtain relationships of type with id %s, rightward name %s " + + "for item with uuid %s, handle %s", + relationshipType.getID(), relationshipType.getRightwardType(), item.getID(), item.getHandle() + ), e); + return null; + } + } + + /** + * From a list of relationships, find the relationship with the correct relationship type and items. + * If isLeft is true, the provided item should be on the left side of the relationship. + * If isLeft is false, the provided item should be on the right side of the relationship. + * In both cases, the other item is taken from the given relationship. + * @param latestItem the item that should either be on the left or right side of the returned relationship (if any). + * @param isLeft decide on which side of the relationship the provided item should be. + * @param previousItemRelationship the relationship from which the type and the other item are read. + * @param relationships the list of relationships that we'll search through. + * @return the relationship that satisfies the requirements (can only be one or zero). + */ + protected Relationship getMatchingRelationship( + Item latestItem, boolean isLeft, Relationship previousItemRelationship, List relationships + ) { + Item leftItem = previousItemRelationship.getLeftItem(); + RelationshipType relationshipType = previousItemRelationship.getRelationshipType(); + Item rightItem = previousItemRelationship.getRightItem(); + + if (isLeft) { + return getMatchingRelationship(latestItem, relationshipType, rightItem, relationships); + } else { + return getMatchingRelationship(leftItem, relationshipType, latestItem, relationships); + } + } + + + /** + * Find the relationship with the given left item, relation type and right item, from a list of relationships. + * @param expectedLeftItem the relationship that we're looking for has this item on the left side. + * @param expectedRelationshipType the relationship that we're looking for has this relationship type. + * @param expectedRightItem the relationship that we're looking for has this item on the right side. + * @param relationships the list of relationships that we'll search through. + * @return the relationship that satisfies the requirements (can only be one or zero). + */ + protected Relationship getMatchingRelationship( + Item expectedLeftItem, RelationshipType expectedRelationshipType, Item expectedRightItem, + List relationships + ) { + Integer expectedRelationshipTypeId = expectedRelationshipType.getID(); + + List matchingRelationships = relationships.stream() + .filter(relationship -> { + int relationshipTypeId = relationship.getID(); + + boolean leftItemMatches = expectedLeftItem.equals(relationship.getLeftItem()); + boolean relationshipTypeMatches = expectedRelationshipTypeId == relationshipTypeId; + boolean rightItemMatches = expectedRightItem.equals(relationship.getRightItem()); + + return leftItemMatches && relationshipTypeMatches && rightItemMatches; + }) + .distinct() + .collect(Collectors.toUnmodifiableList()); + + if (matchingRelationships.isEmpty()) { + return null; + } + + // NOTE: this situation should never occur because the relationship table has a unique constraint + // over the "left_id", "type_id" and "right_id" columns + if (matchingRelationships.size() > 1) { + String msg = String.format( + "Illegal state: expected 0 or 1 relationship, but found %s relationships (ids: %s) " + + "of type with id %s, rightward name %s " + + "between left item with uuid %s, handle %s and right item with uuid %s, handle %s", + matchingRelationships.size(), + matchingRelationships.stream().map(Relationship::getID).collect(Collectors.toUnmodifiableList()), + expectedRelationshipTypeId, expectedRelationshipType.getRightwardType(), + expectedLeftItem.getID(), expectedLeftItem.getHandle(), + expectedRightItem.getID(), expectedRightItem.getHandle() + ); + log.error(msg); + throw new IllegalStateException(msg); + } + + return matchingRelationships.get(0); + } + + /** + * Update {@link Relationship#latestVersionStatus} of the given relationship. + * @param relationship the relationship. + * @param updateLeftSide whether the status of the left item or the right item should be updated. + * @param isLatest to what the status should be set. + * @throws IllegalStateException if the operation would result in both the left side and the right side + * being set to non-latest. + */ + protected void updateLatestVersionStatus( + Relationship relationship, boolean updateLeftSide, boolean isLatest + ) throws IllegalStateException { + LatestVersionStatus lvs = relationship.getLatestVersionStatus(); + + boolean leftSideIsLatest = lvs == LatestVersionStatus.BOTH || lvs == LatestVersionStatus.LEFT_ONLY; + boolean rightSideIsLatest = lvs == LatestVersionStatus.BOTH || lvs == LatestVersionStatus.RIGHT_ONLY; + + if (updateLeftSide) { + if (leftSideIsLatest == isLatest) { + return; // no change needed + } + leftSideIsLatest = isLatest; + } else { + if (rightSideIsLatest == isLatest) { + return; // no change needed + } + rightSideIsLatest = isLatest; + } + + LatestVersionStatus newVersionStatus; + if (leftSideIsLatest && rightSideIsLatest) { + newVersionStatus = LatestVersionStatus.BOTH; + } else if (leftSideIsLatest) { + newVersionStatus = LatestVersionStatus.LEFT_ONLY; + } else if (rightSideIsLatest) { + newVersionStatus = LatestVersionStatus.RIGHT_ONLY; + } else { + String msg = String.format( + "Illegal state: cannot set %s item to latest = false, because relationship with id %s, " + + "rightward name %s between left item with uuid %s, handle %s and right item with uuid %s, handle %s " + + "has latest version status set to %s", + updateLeftSide ? "left" : "right", relationship.getID(), + relationship.getRelationshipType().getRightwardType(), + relationship.getLeftItem().getID(), relationship.getLeftItem().getHandle(), + relationship.getRightItem().getID(), relationship.getRightItem().getHandle(), lvs + ); + log.error(msg); + throw new IllegalStateException(msg); + } + + log.info(String.format( + "set latest version status from %s to %s for relationship with id %s, rightward name %s " + + "between left item with uuid %s, handle %s and right item with uuid %s, handle %s", + lvs, newVersionStatus, relationship.getID(), relationship.getRelationshipType().getRightwardType(), + relationship.getLeftItem().getID(), relationship.getLeftItem().getHandle(), + relationship.getRightItem().getID(), relationship.getRightItem().getHandle() + )); + relationship.setLatestVersionStatus(newVersionStatus); + } + @Override public void end(Context ctx) throws Exception { if (itemsToProcess != null) { From 87e3fdef983457463ceff9cd7bad1d809129c8c4 Mon Sep 17 00:00:00 2001 From: Antoine Snyers Date: Thu, 10 Mar 2022 10:05:40 +0100 Subject: [PATCH 0043/1846] Adjust tests to expect filtering instead of jumping --- .../app/rest/BrowsesResourceControllerIT.java | 56 ++++++++----------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BrowsesResourceControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BrowsesResourceControllerIT.java index bd9e7d93566a..5bea017a9ed2 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BrowsesResourceControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BrowsesResourceControllerIT.java @@ -897,8 +897,8 @@ public void testBrowseByItemsStartsWith() throws Exception { // ---- BROWSES BY ITEM ---- //** WHEN ** //An anonymous user browses the items in the Browse by date issued endpoint - //with startsWith set to 1990 - getClient().perform(get("/api/discover/browses/dateissued/items?startsWith=1990") + //with startsWith set to 199 + getClient().perform(get("/api/discover/browses/dateissued/items?startsWith=199") .param("size", "2")) //** THEN ** @@ -907,12 +907,11 @@ public void testBrowseByItemsStartsWith() throws Exception { //We expect the content type to be "application/hal+json;charset=UTF-8" .andExpect(content().contentType(contentType)) - //We expect the totalElements to be the 7 items present in the repository - .andExpect(jsonPath("$.page.totalElements", is(7))) + //We expect the totalElements to be the 2 items present in the repository + .andExpect(jsonPath("$.page.totalElements", is(2))) //We expect to jump to page 1 of the index - .andExpect(jsonPath("$.page.number", is(1))) + .andExpect(jsonPath("$.page.number", is(0))) .andExpect(jsonPath("$.page.size", is(2))) - .andExpect(jsonPath("$._links.first.href", containsString("startsWith=1990"))) //Verify that the index jumps to the "Python" item. .andExpect(jsonPath("$._embedded.items", @@ -933,19 +932,16 @@ public void testBrowseByItemsStartsWith() throws Exception { //We expect the content type to be "application/hal+json;charset=UTF-8" .andExpect(content().contentType(contentType)) - //We expect the totalElements to be the 7 items present in the repository - .andExpect(jsonPath("$.page.totalElements", is(7))) + //We expect the totalElements to be the 1 item present in the repository + .andExpect(jsonPath("$.page.totalElements", is(1))) //We expect to jump to page 2 in the index - .andExpect(jsonPath("$.page.number", is(2))) + .andExpect(jsonPath("$.page.number", is(0))) .andExpect(jsonPath("$._links.self.href", containsString("startsWith=T"))) //Verify that the index jumps to the "T-800" item. .andExpect(jsonPath("$._embedded.items", contains(ItemMatcher.matchItemWithTitleAndDateIssued(item7, - "T-800", "2029"), - ItemMatcher.matchItemWithTitleAndDateIssued(item5, - "Zeta Reticuli", - "2018-01-01") + "T-800", "2029") ))); //** WHEN ** @@ -961,8 +957,8 @@ public void testBrowseByItemsStartsWith() throws Exception { //We expect the content type to be "application/hal+json;charset=UTF-8" .andExpect(content().contentType(contentType)) - //We expect the totalElements to be the 3 items present in the collection - .andExpect(jsonPath("$.page.totalElements", is(3))) + //We expect the totalElements to be the 1 item present in the collection + .andExpect(jsonPath("$.page.totalElements", is(1))) //As this is is a small collection, we expect to go-to page 0 .andExpect(jsonPath("$.page.number", is(0))) .andExpect(jsonPath("$._links.self.href", containsString("startsWith=Blade"))) @@ -971,9 +967,7 @@ public void testBrowseByItemsStartsWith() throws Exception { .andExpect(jsonPath("$._embedded.items", contains(ItemMatcher.matchItemWithTitleAndDateIssued(item2, "Blade Runner", - "1982-06-25"), - ItemMatcher.matchItemWithTitleAndDateIssued(item3, - "Python", "1990") + "1982-06-25") ))); } @@ -1048,9 +1042,9 @@ public void testBrowseByStartsWithAndPage() throws Exception { //** WHEN ** //An anonymous user browses the items in the Browse by date issued endpoint - //with startsWith set to 1990 and Page to 3 - getClient().perform(get("/api/discover/browses/dateissued/items?startsWith=1990") - .param("size", "2").param("page", "2")) + //with startsWith set to 199 and Page to 1 + getClient().perform(get("/api/discover/browses/dateissued/items?startsWith=199") + .param("size", "1").param("page", "1")) //** THEN ** //The status has to be 200 OK @@ -1058,20 +1052,18 @@ public void testBrowseByStartsWithAndPage() throws Exception { //We expect the content type to be "application/hal+json;charset=UTF-8" .andExpect(content().contentType(contentType)) - //We expect the totalElements to be the 7 items present in the repository - .andExpect(jsonPath("$.page.totalElements", is(7))) + //We expect the totalElements to be the 2 items present in the repository + .andExpect(jsonPath("$.page.totalElements", is(2))) //We expect to jump to page 1 of the index - .andExpect(jsonPath("$.page.number", is(2))) - .andExpect(jsonPath("$.page.size", is(2))) - .andExpect(jsonPath("$._links.self.href", containsString("startsWith=1990"))) + .andExpect(jsonPath("$.page.number", is(1))) + .andExpect(jsonPath("$.page.size", is(1))) + .andExpect(jsonPath("$._links.self.href", containsString("startsWith=199"))) - //Verify that the index jumps to the "Zeta Reticuli" item. + //Verify that the index jumps to the "Java" item. .andExpect(jsonPath("$._embedded.items", - contains(ItemMatcher.matchItemWithTitleAndDateIssued(item7, - "Zeta Reticuli", "2018-01-01"), - ItemMatcher.matchItemWithTitleAndDateIssued(item4, - "Moon", "2018-01-02") - ))); + contains( + ItemMatcher.matchItemWithTitleAndDateIssued(item3, "Java", "1995-05-23") + ))); } @Test From f75d680ce3b55af34782f9ab7b60fa5570ff61db Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Thu, 10 Mar 2022 19:15:04 +0100 Subject: [PATCH 0044/1846] 88061: Test relationship support of VersioningConsumer --- .../dspace/versioning/VersioningConsumer.java | 2 +- .../VersioningWithRelationshipsTest.java | 559 +++++++++++++++++- 2 files changed, 550 insertions(+), 11 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/versioning/VersioningConsumer.java b/dspace-api/src/main/java/org/dspace/versioning/VersioningConsumer.java index 9314bad850aa..b9489d1b22ec 100644 --- a/dspace-api/src/main/java/org/dspace/versioning/VersioningConsumer.java +++ b/dspace-api/src/main/java/org/dspace/versioning/VersioningConsumer.java @@ -383,7 +383,7 @@ protected Relationship getMatchingRelationship( List matchingRelationships = relationships.stream() .filter(relationship -> { - int relationshipTypeId = relationship.getID(); + int relationshipTypeId = relationship.getRelationshipType().getID(); boolean leftItemMatches = expectedLeftItem.equals(relationship.getLeftItem()); boolean relationshipTypeMatches = expectedRelationshipTypeId == relationshipTypeId; diff --git a/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java b/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java index 449749aa993c..a3144a85bc0c 100644 --- a/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java +++ b/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java @@ -11,9 +11,11 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.hasProperty; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotSame; import java.util.List; @@ -26,7 +28,9 @@ import org.dspace.builder.RelationshipBuilder; import org.dspace.builder.RelationshipTypeBuilder; import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.InstallItemService; import org.dspace.content.service.RelationshipService; +import org.dspace.content.service.WorkspaceItemService; import org.dspace.versioning.Version; import org.dspace.versioning.factory.VersionServiceFactory; import org.dspace.versioning.service.VersioningService; @@ -36,8 +40,14 @@ public class VersioningWithRelationshipsTest extends AbstractIntegrationTestWithDatabase { - private RelationshipService relationshipService; - private VersioningService versioningService; + private final RelationshipService relationshipService = + ContentServiceFactory.getInstance().getRelationshipService(); + private final VersioningService versioningService = + VersionServiceFactory.getInstance().getVersionService(); + private final WorkspaceItemService workspaceItemService = + ContentServiceFactory.getInstance().getWorkspaceItemService(); + private final InstallItemService installItemService = + ContentServiceFactory.getInstance().getInstallItemService(); protected Community community; protected Collection collection; @@ -56,9 +66,6 @@ public class VersioningWithRelationshipsTest extends AbstractIntegrationTestWith public void setUp() throws Exception { super.setUp(); - relationshipService = ContentServiceFactory.getInstance().getRelationshipService(); - versioningService = VersionServiceFactory.getInstance().getVersionService(); - context.turnOffAuthorisationSystem(); community = CommunityBuilder.createCommunity(context) @@ -93,7 +100,7 @@ public void setUp() throws Exception { isProjectOfPublication = RelationshipTypeBuilder.createRelationshipTypeBuilder( context, publicationEntityType, projectEntityType, "isProjectOfPublication", "isPublicationOfProject", - 0, null, 0, null + null, null, null, null ) .withCopyToLeft(false) .withCopyToRight(false) @@ -102,7 +109,7 @@ public void setUp() throws Exception { isOrgUnitOfPublication = RelationshipTypeBuilder.createRelationshipTypeBuilder( context, publicationEntityType, orgUnitEntityType, "isOrgUnitOfPublication", "isPublicationOfOrgUnit", - 0, null, 0, null + null, null, null, null ) .withCopyToLeft(false) .withCopyToRight(false) @@ -307,19 +314,450 @@ public void test_createNewVersionOfItemOnLeftSideOfRelationships() throws Except )) ); + //////////////////////////////////////// + // do item install on new publication // + //////////////////////////////////////// + + WorkspaceItem newPublicationWSI = workspaceItemService.findByItem(context, newPublication); + installItemService.installItem(context, newPublicationWSI); + context.dispatchEvents(); + + /////////////////////////////////////////////////////////////////////// + // verify the relationships of all 5 items (excludeNonLatest = true) // + /////////////////////////////////////////////////////////////////////// + + assertThat( + relationshipService.findByItem(context, originalPublication, -1, -1, false, true), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY), + isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY), + isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY) + )) + ); + + assertThat( + relationshipService.findByItem(context, person1, -1, -1, false, true), + containsInAnyOrder(List.of( + isRelationship(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, project1, -1, -1, false, true), + containsInAnyOrder(List.of( + isRelationship(newPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, orgUnit1, -1, -1, false, true), + containsInAnyOrder(List.of( + isRelationship(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, newPublication, -1, -1, false, true), + containsInAnyOrder(List.of( + isRelationship(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH), + isRelationship(newPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH), + isRelationship(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH) + )) + ); + + //////////////////////////////////////////////////////////////////////// + // verify the relationships of all 5 items (excludeNonLatest = false) // + //////////////////////////////////////////////////////////////////////// + + assertThat( + relationshipService.findByItem(context, originalPublication, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY), + isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY), + isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY) + )) + ); + + assertThat( + relationshipService.findByItem(context, person1, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY), + isRelationship(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, project1, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY), + isRelationship(newPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, orgUnit1, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY), + isRelationship(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, newPublication, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH), + isRelationship(newPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH), + isRelationship(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH) + )) + ); + ////////////// // clean up // ////////////// - versioningService.removeVersion(context, newPublication); + // need to manually delete all relationships to avoid SQL constraint violation exception + List relationships = relationshipService.findAll(context); + for (Relationship relationship : relationships) { + relationshipService.delete(context, relationship); + } } @Test - public void test_createNewVersionOfItemOnRightSideOfRelationships() throws Exception { + public void test_createNewVersionOfItemAndModifyRelationships() throws Exception { /////////////////////////////////////////////// // create a publication with 3 relationships // /////////////////////////////////////////////// + Item person1 = ItemBuilder.createItem(context, collection) + .withTitle("person 1") + .withMetadata("dspace", "entity", "type", personEntityType.getLabel()) + .build(); + + Item project1 = ItemBuilder.createItem(context, collection) + .withTitle("project 1") + .withMetadata("dspace", "entity", "type", projectEntityType.getLabel()) + .build(); + + Item orgUnit1 = ItemBuilder.createItem(context, collection) + .withTitle("org unit 1") + .withMetadata("dspace", "entity", "type", orgUnitEntityType.getLabel()) + .build(); + + Item originalPublication = ItemBuilder.createItem(context, collection) + .withTitle("original publication") + .withMetadata("dspace", "entity", "type", publicationEntityType.getLabel()) + .build(); + + RelationshipBuilder.createRelationshipBuilder(context, originalPublication, person1, isAuthorOfPublication) + .build(); + + RelationshipBuilder + .createRelationshipBuilder(context, originalPublication, project1, isProjectOfPublication) + .build(); + + RelationshipBuilder + .createRelationshipBuilder(context, originalPublication, orgUnit1, isOrgUnitOfPublication) + .build(); + + ///////////////////////////////////////////////////////// + // verify that the relationships were properly created // + ///////////////////////////////////////////////////////// + + assertThat( + relationshipService.findByItem(context, originalPublication, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH), + isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH), + isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, person1, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, project1, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, orgUnit1, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH) + )) + ); + + ///////////////////////////////////////////// + // create a new version of the publication // + ///////////////////////////////////////////// + + Version newVersion = versioningService.createNewVersion(context, originalPublication); + Item newPublication = newVersion.getItem(); + assertNotSame(originalPublication, newPublication); + + /////////////////////////////////////////////////////////////////////// + // verify the relationships of all 5 items (excludeNonLatest = true) // + /////////////////////////////////////////////////////////////////////// + + assertThat( + relationshipService.findByItem(context, originalPublication, -1, -1, false, true), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH), + isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH), + isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, person1, -1, -1, false, true), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, project1, -1, -1, false, true), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, orgUnit1, -1, -1, false, true), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, newPublication, -1, -1, false, true), + containsInAnyOrder(List.of( + isRelationship(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY), + isRelationship(newPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY), + isRelationship(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY) + )) + ); + + //////////////////////////////////////////////////////////////////////// + // verify the relationships of all 5 items (excludeNonLatest = false) // + //////////////////////////////////////////////////////////////////////// + + assertThat( + relationshipService.findByItem(context, originalPublication, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH), + isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH), + isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, person1, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH), + isRelationship(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY) + )) + ); + + assertThat( + relationshipService.findByItem(context, project1, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH), + isRelationship(newPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY) + )) + ); + + assertThat( + relationshipService.findByItem(context, orgUnit1, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH), + isRelationship(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY) + )) + ); + + assertThat( + relationshipService.findByItem(context, newPublication, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY), + isRelationship(newPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY), + isRelationship(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY) + )) + ); + + ///////////////////////////////////////////// + // modify relationships on new publication // + ///////////////////////////////////////////// + + Item person2 = ItemBuilder.createItem(context, collection) + .withTitle("person 2") + .withMetadata("dspace", "entity", "type", personEntityType.getLabel()) + .build(); + + Item orgUnit2 = ItemBuilder.createItem(context, collection) + .withTitle("org unit 2") + .withMetadata("dspace", "entity", "type", orgUnitEntityType.getLabel()) + .build(); + + // on new item, remove relationship with project 1 + List newProjectRels = relationshipService + .findByItemAndRelationshipType(context, newPublication, isProjectOfPublication); + assertEquals(1, newProjectRels.size()); + relationshipService.delete(context, newProjectRels.get(0)); + + // on new item remove relationship with org unit 1 + List newOrgUnitRels = relationshipService + .findByItemAndRelationshipType(context, newPublication, isOrgUnitOfPublication); + assertEquals(1, newOrgUnitRels.size()); + relationshipService.delete(context, newOrgUnitRels.get(0)); + + RelationshipBuilder.createRelationshipBuilder(context, newPublication, person2, isAuthorOfPublication) + .build(); + + RelationshipBuilder.createRelationshipBuilder(context, newPublication, orgUnit2, isOrgUnitOfPublication) + .build(); + + //////////////////////////////////////// + // do item install on new publication // + //////////////////////////////////////// + + WorkspaceItem newPublicationWSI = workspaceItemService.findByItem(context, newPublication); + installItemService.installItem(context, newPublicationWSI); + context.dispatchEvents(); + + /////////////////////////////////////////////////////////////////////// + // verify the relationships of all 7 items (excludeNonLatest = true) // + /////////////////////////////////////////////////////////////////////// + + assertThat( + relationshipService.findByItem(context, originalPublication, -1, -1, false, true), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY), + isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY), + isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY) + )) + ); + + assertThat( + relationshipService.findByItem(context, person1, -1, -1, false, true), + containsInAnyOrder(List.of( + isRelationship(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, person2, -1, -1, false, true), + containsInAnyOrder(List.of( + isRelationship(newPublication, isAuthorOfPublication, person2, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, project1, -1, -1, false, true), + empty() + ); + + assertThat( + relationshipService.findByItem(context, orgUnit1, -1, -1, false, true), + empty() + ); + + assertThat( + relationshipService.findByItem(context, orgUnit2, -1, -1, false, true), + containsInAnyOrder(List.of( + isRelationship(newPublication, isOrgUnitOfPublication, orgUnit2, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, newPublication, -1, -1, false, true), + containsInAnyOrder(List.of( + isRelationship(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH), + isRelationship(newPublication, isAuthorOfPublication, person2, LatestVersionStatus.BOTH), + isRelationship(newPublication, isOrgUnitOfPublication, orgUnit2, LatestVersionStatus.BOTH) + )) + ); + + //////////////////////////////////////////////////////////////////////// + // verify the relationships of all 7 items (excludeNonLatest = false) // + //////////////////////////////////////////////////////////////////////// + + assertThat( + relationshipService.findByItem(context, originalPublication, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY), + isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY), + isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY) + )) + ); + + assertThat( + relationshipService.findByItem(context, person1, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY), + isRelationship(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, person2, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(newPublication, isAuthorOfPublication, person2, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, project1, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY) + )) + ); + + assertThat( + relationshipService.findByItem(context, orgUnit1, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY) + )) + ); + + assertThat( + relationshipService.findByItem(context, orgUnit2, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(newPublication, isOrgUnitOfPublication, orgUnit2, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, newPublication, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH), + isRelationship(newPublication, isAuthorOfPublication, person2, LatestVersionStatus.BOTH), + isRelationship(newPublication, isOrgUnitOfPublication, orgUnit2, LatestVersionStatus.BOTH) + )) + ); + + ////////////// + // clean up // + ////////////// + + // need to manually delete all relationships to avoid SQL constraint violation exception + List relationships = relationshipService.findAll(context); + for (Relationship relationship : relationships) { + relationshipService.delete(context, relationship); + } + } + + @Test + public void test_createNewVersionOfItemOnRightSideOfRelationships() throws Exception { + ////////////////////////////////////////// + // create a person with 3 relationships // + ////////////////////////////////////////// + Item publication1 = ItemBuilder.createItem(context, collection) .withTitle("publication 1") .withMetadata("dspace", "entity", "type", publicationEntityType.getLabel()) @@ -480,11 +918,112 @@ public void test_createNewVersionOfItemOnRightSideOfRelationships() throws Excep )) ); + /////////////////////////////////// + // do item install on new person // + /////////////////////////////////// + + WorkspaceItem newPersonWSI = workspaceItemService.findByItem(context, newPerson); + installItemService.installItem(context, newPersonWSI); + context.dispatchEvents(); + + /////////////////////////////////////////////////////////////////////// + // verify the relationships of all 5 items (excludeNonLatest = true) // + /////////////////////////////////////////////////////////////////////// + + assertThat( + relationshipService.findByItem(context, originalPerson, -1, -1, false, true), + containsInAnyOrder(List.of( + isRelationship(publication1, isAuthorOfPublication, originalPerson, LatestVersionStatus.LEFT_ONLY), + isRelationship(project1, isMemberOfProject, originalPerson, LatestVersionStatus.LEFT_ONLY), + isRelationship(orgUnit1, isMemberOfOrgUnit, originalPerson, LatestVersionStatus.LEFT_ONLY) + )) + ); + + assertThat( + relationshipService.findByItem(context, publication1, -1, -1, false, true), + containsInAnyOrder(List.of( + isRelationship(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, project1, -1, -1, false, true), + containsInAnyOrder(List.of( + isRelationship(project1, isMemberOfProject, newPerson, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, orgUnit1, -1, -1, false, true), + containsInAnyOrder(List.of( + isRelationship(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, newPerson, -1, -1, false, true), + containsInAnyOrder(List.of( + isRelationship(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.BOTH), + isRelationship(project1, isMemberOfProject, newPerson, LatestVersionStatus.BOTH), + isRelationship(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.BOTH) + )) + ); + + //////////////////////////////////////////////////////////////////////// + // verify the relationships of all 5 items (excludeNonLatest = false) // + //////////////////////////////////////////////////////////////////////// + + assertThat( + relationshipService.findByItem(context, originalPerson, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(publication1, isAuthorOfPublication, originalPerson, LatestVersionStatus.LEFT_ONLY), + isRelationship(project1, isMemberOfProject, originalPerson, LatestVersionStatus.LEFT_ONLY), + isRelationship(orgUnit1, isMemberOfOrgUnit, originalPerson, LatestVersionStatus.LEFT_ONLY) + )) + ); + + assertThat( + relationshipService.findByItem(context, publication1, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(publication1, isAuthorOfPublication, originalPerson, LatestVersionStatus.LEFT_ONLY), + isRelationship(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, project1, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(project1, isMemberOfProject, originalPerson, LatestVersionStatus.LEFT_ONLY), + isRelationship(project1, isMemberOfProject, newPerson, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, orgUnit1, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(orgUnit1, isMemberOfOrgUnit, originalPerson, LatestVersionStatus.LEFT_ONLY), + isRelationship(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, newPerson, -1, -1, false, false), + containsInAnyOrder(List.of( + isRelationship(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.BOTH), + isRelationship(project1, isMemberOfProject, newPerson, LatestVersionStatus.BOTH), + isRelationship(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.BOTH) + )) + ); + ////////////// // clean up // ////////////// - versioningService.removeVersion(context, newPerson); + // need to manually delete all relationships to avoid SQL constraint violation exception + List relationships = relationshipService.findAll(context); + for (Relationship relationship : relationships) { + relationshipService.delete(context, relationship); + } } } From f6b72787dee441c0440c46126190d0eac143a6d9 Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Thu, 10 Mar 2022 20:00:22 +0100 Subject: [PATCH 0045/1846] 88196: WIP: Test order of plain and virtual metadata (after relationship versioning) --- .../VersioningWithRelationshipsTest.java | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java b/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java index a3144a85bc0c..979d9b8b2863 100644 --- a/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java +++ b/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java @@ -16,7 +16,9 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; import java.util.List; @@ -29,6 +31,7 @@ import org.dspace.builder.RelationshipTypeBuilder; import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.service.InstallItemService; +import org.dspace.content.service.ItemService; import org.dspace.content.service.RelationshipService; import org.dspace.content.service.WorkspaceItemService; import org.dspace.versioning.Version; @@ -48,6 +51,8 @@ public class VersioningWithRelationshipsTest extends AbstractIntegrationTestWith ContentServiceFactory.getInstance().getWorkspaceItemService(); private final InstallItemService installItemService = ContentServiceFactory.getInstance().getInstallItemService(); + private final ItemService itemService = + ContentServiceFactory.getInstance().getItemService(); protected Community community; protected Collection collection; @@ -1026,4 +1031,87 @@ public void test_createNewVersionOfItemOnRightSideOfRelationships() throws Excep } } + @Test + public void test_createNewVersionOfItemAndVerifyMetadataOrder() throws Exception { + ///////////////////////////////////////// + // create a publication with 6 authors // + ///////////////////////////////////////// + + Item originalPublication = ItemBuilder.createItem(context, collection) + .withTitle("original publication") + .withMetadata("dspace", "entity", "type", publicationEntityType.getLabel()) + .build(); + + // author 1 (plain metadata) + itemService.addMetadata(context, originalPublication, "dc", "contributor", "author", null, "author 1 (plain)"); + + // author 2 (virtual) + Item author2 = ItemBuilder.createItem(context, collection) + .withTitle("author 2 (item)") + .withMetadata("dspace", "entity", "type", personEntityType.getLabel()) + .withPersonIdentifierFirstName("2 (item)") + .withPersonIdentifierLastName("author") + .build(); + RelationshipBuilder.createRelationshipBuilder(context, originalPublication, author2, isAuthorOfPublication) + .build(); + + // author 3 (plain metadata) + itemService.addMetadata(context, originalPublication, "dc", "contributor", "author", null, "author 3 (plain)"); + + // author 4 (virtual) + Item author4 = ItemBuilder.createItem(context, collection) + .withTitle("author 4 (item)") + .withMetadata("dspace", "entity", "type", personEntityType.getLabel()) + .withPersonIdentifierFirstName("4 (item)") + .withPersonIdentifierLastName("author") + .build(); + RelationshipBuilder.createRelationshipBuilder(context, originalPublication, author4, isAuthorOfPublication) + .build(); + + // author 5 (plain metadata) + itemService.addMetadata(context, originalPublication, "dc", "contributor", "author", null, "author 5 (plain)"); + + // author 6 (virtual) + Item author6 = ItemBuilder.createItem(context, collection) + .withTitle("author 6 (item)") + .withMetadata("dspace", "entity", "type", personEntityType.getLabel()) + .withPersonIdentifierFirstName("6 (item)") + .withPersonIdentifierLastName("author") + .build(); + RelationshipBuilder.createRelationshipBuilder(context, originalPublication, author6, isAuthorOfPublication) + .build(); + + itemService.update(context, originalPublication); + + //////////////////////////////// + // test dc.contributor.author // + //////////////////////////////// + + List mdvs = itemService.getMetadata( + originalPublication, "dc", "contributor", "author", Item.ANY + ); + + // TODO make sure test setup respects following order + + assertFalse(mdvs.get(0) instanceof RelationshipMetadataValue); + assertEquals("author 1 (plain)", mdvs.get(0).getValue()); + + assertTrue(mdvs.get(1) instanceof RelationshipMetadataValue); + assertEquals("author, 2 (item)", mdvs.get(1).getValue()); + + assertFalse(mdvs.get(2) instanceof RelationshipMetadataValue); + assertEquals("author 3 (plain)", mdvs.get(2).getValue()); + + assertTrue(mdvs.get(3) instanceof RelationshipMetadataValue); + assertEquals("author, 4 (item)", mdvs.get(3).getValue()); + + assertFalse(mdvs.get(4) instanceof RelationshipMetadataValue); + assertEquals("author 5 (plain)", mdvs.get(4).getValue()); + + assertTrue(mdvs.get(5) instanceof RelationshipMetadataValue); + assertEquals("author, 6 (item)", mdvs.get(5).getValue()); + + // TODO finish rest of test + } + } From 938f1bab526ceeb0465fe34daee6fff0f7c6f68b Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Fri, 11 Mar 2022 11:24:04 +0100 Subject: [PATCH 0046/1846] 88196: BUGFIX: Auto-assign place in RelationshipBuilder --- .../src/test/java/org/dspace/builder/RelationshipBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/test/java/org/dspace/builder/RelationshipBuilder.java b/dspace-api/src/test/java/org/dspace/builder/RelationshipBuilder.java index 3c9c6800df4b..5e54b9c8eee0 100644 --- a/dspace-api/src/test/java/org/dspace/builder/RelationshipBuilder.java +++ b/dspace-api/src/test/java/org/dspace/builder/RelationshipBuilder.java @@ -117,7 +117,7 @@ private RelationshipBuilder create(Context context, Item leftItem, Item rightIte this.context = context; try { - relationship = relationshipService.create(context, leftItem, rightItem, relationshipType, 0, 0); + relationship = relationshipService.create(context, leftItem, rightItem, relationshipType, -1, -1); } catch (SQLException | AuthorizeException e) { log.warn("Failed to create relationship", e); } From d31f6834c84be2b65ba642ada51cec8cd27dba30 Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Fri, 11 Mar 2022 15:10:26 +0100 Subject: [PATCH 0047/1846] 88196: WIP: Test place of metadata in new version (implementation needs fix) --- .../versioning/AbstractVersionProvider.java | 15 ++++- .../VersioningWithRelationshipsTest.java | 64 ++++++++++++++----- 2 files changed, 59 insertions(+), 20 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/versioning/AbstractVersionProvider.java b/dspace-api/src/main/java/org/dspace/versioning/AbstractVersionProvider.java index 3633f1949b46..329332d31526 100644 --- a/dspace-api/src/main/java/org/dspace/versioning/AbstractVersionProvider.java +++ b/dspace-api/src/main/java/org/dspace/versioning/AbstractVersionProvider.java @@ -62,9 +62,18 @@ protected void copyMetadata(Context context, Item itemNew, Item nativeItem) thro continue; } - itemService - .addMetadata(context, itemNew, metadataField, aMd.getLanguage(), aMd.getValue(), aMd.getAuthority(), - aMd.getConfidence()); + itemService.addMetadata( + context, + itemNew, + metadataField.getMetadataSchema().getName(), + metadataField.getElement(), + metadataField.getQualifier(), + aMd.getLanguage(), + aMd.getValue(), + aMd.getAuthority(), + aMd.getConfidence(), + aMd.getPlace() + ); } } diff --git a/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java b/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java index 979d9b8b2863..34e817609093 100644 --- a/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java +++ b/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java @@ -1081,37 +1081,67 @@ public void test_createNewVersionOfItemAndVerifyMetadataOrder() throws Exception RelationshipBuilder.createRelationshipBuilder(context, originalPublication, author6, isAuthorOfPublication) .build(); - itemService.update(context, originalPublication); - //////////////////////////////// // test dc.contributor.author // //////////////////////////////// - List mdvs = itemService.getMetadata( + List oldMdvs = itemService.getMetadata( originalPublication, "dc", "contributor", "author", Item.ANY ); - // TODO make sure test setup respects following order + assertFalse(oldMdvs.get(0) instanceof RelationshipMetadataValue); + assertEquals("author 1 (plain)", oldMdvs.get(0).getValue()); + + assertTrue(oldMdvs.get(1) instanceof RelationshipMetadataValue); + assertEquals("author, 2 (item)", oldMdvs.get(1).getValue()); + + assertFalse(oldMdvs.get(2) instanceof RelationshipMetadataValue); + assertEquals("author 3 (plain)", oldMdvs.get(2).getValue()); + + assertTrue(oldMdvs.get(3) instanceof RelationshipMetadataValue); + assertEquals("author, 4 (item)", oldMdvs.get(3).getValue()); + + assertFalse(oldMdvs.get(4) instanceof RelationshipMetadataValue); + assertEquals("author 5 (plain)", oldMdvs.get(4).getValue()); + + assertTrue(oldMdvs.get(5) instanceof RelationshipMetadataValue); + assertEquals("author, 6 (item)", oldMdvs.get(5).getValue()); + + /////////////////////////////////////// + // create new version of publication // + /////////////////////////////////////// + + Version newVersion = versioningService.createNewVersion(context, originalPublication); + Item newPublication = newVersion.getItem(); + assertNotSame(originalPublication, newPublication); + + //////////////////////////////// + // test dc.contributor.author // + //////////////////////////////// + + List newMdvs = itemService.getMetadata( + newPublication, "dc", "contributor", "author", Item.ANY + ); - assertFalse(mdvs.get(0) instanceof RelationshipMetadataValue); - assertEquals("author 1 (plain)", mdvs.get(0).getValue()); + assertFalse(newMdvs.get(0) instanceof RelationshipMetadataValue); + assertEquals("author 1 (plain)", newMdvs.get(0).getValue()); - assertTrue(mdvs.get(1) instanceof RelationshipMetadataValue); - assertEquals("author, 2 (item)", mdvs.get(1).getValue()); + assertTrue(newMdvs.get(1) instanceof RelationshipMetadataValue); + assertEquals("author, 2 (item)", newMdvs.get(1).getValue()); - assertFalse(mdvs.get(2) instanceof RelationshipMetadataValue); - assertEquals("author 3 (plain)", mdvs.get(2).getValue()); + assertFalse(newMdvs.get(2) instanceof RelationshipMetadataValue); + assertEquals("author 3 (plain)", newMdvs.get(2).getValue()); - assertTrue(mdvs.get(3) instanceof RelationshipMetadataValue); - assertEquals("author, 4 (item)", mdvs.get(3).getValue()); + assertTrue(newMdvs.get(3) instanceof RelationshipMetadataValue); + assertEquals("author, 4 (item)", newMdvs.get(3).getValue()); - assertFalse(mdvs.get(4) instanceof RelationshipMetadataValue); - assertEquals("author 5 (plain)", mdvs.get(4).getValue()); + assertFalse(newMdvs.get(4) instanceof RelationshipMetadataValue); + assertEquals("author 5 (plain)", newMdvs.get(4).getValue()); - assertTrue(mdvs.get(5) instanceof RelationshipMetadataValue); - assertEquals("author, 6 (item)", mdvs.get(5).getValue()); + assertTrue(newMdvs.get(5) instanceof RelationshipMetadataValue); + assertEquals("author, 6 (item)", newMdvs.get(5).getValue()); - // TODO finish rest of test + // TODO also test place after removing/adding relationships and metadata } } From df07d1abf11dfe464d491f10548fcd8f39a258fc Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Fri, 11 Mar 2022 16:12:13 +0100 Subject: [PATCH 0048/1846] 88196: Avoid SQL constraint violation in test --- .../content/VersioningWithRelationshipsTest.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java b/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java index 34e817609093..accd878c7caa 100644 --- a/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java +++ b/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java @@ -1141,7 +1141,15 @@ public void test_createNewVersionOfItemAndVerifyMetadataOrder() throws Exception assertTrue(newMdvs.get(5) instanceof RelationshipMetadataValue); assertEquals("author, 6 (item)", newMdvs.get(5).getValue()); - // TODO also test place after removing/adding relationships and metadata + ////////////// + // clean up // + ////////////// + + // need to manually delete all relationships to avoid SQL constraint violation exception + List relationships = relationshipService.findAll(context); + for (Relationship relationship : relationships) { + relationshipService.delete(context, relationship); + } } } From 23d537f5d9995f39c2b21b48165fd828bf524f77 Mon Sep 17 00:00:00 2001 From: Antoine Snyers Date: Mon, 14 Mar 2022 12:30:00 +0100 Subject: [PATCH 0049/1846] Update code --- dspace-api/src/main/java/org/dspace/browse/BrowseEngine.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/browse/BrowseEngine.java b/dspace-api/src/main/java/org/dspace/browse/BrowseEngine.java index 2c295c9e9db5..2f9b96d85899 100644 --- a/dspace-api/src/main/java/org/dspace/browse/BrowseEngine.java +++ b/dspace-api/src/main/java/org/dspace/browse/BrowseEngine.java @@ -202,7 +202,7 @@ private BrowseInfo browseByItem(BrowserScope bs) // get the table name that we are going to be getting our data from dao.setTable(browseIndex.getTableName()); - dao.setStartsWith(StringUtils.lowerCase(scope.getStartsWith())); + dao.setStartsWith(normalizeJumpToValue(scope.getStartsWith())); // tell the browse query whether we are ascending or descending on the value dao.setAscending(scope.isAscending()); From ca17ebf338f7cf257f39f270fcbd1b55463d8979 Mon Sep 17 00:00:00 2001 From: Antoine Snyers Date: Mon, 14 Mar 2022 16:04:02 +0100 Subject: [PATCH 0050/1846] Update code --- dspace-api/src/main/java/org/dspace/browse/BrowseEngine.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/browse/BrowseEngine.java b/dspace-api/src/main/java/org/dspace/browse/BrowseEngine.java index 2f9b96d85899..0969c205cbb7 100644 --- a/dspace-api/src/main/java/org/dspace/browse/BrowseEngine.java +++ b/dspace-api/src/main/java/org/dspace/browse/BrowseEngine.java @@ -11,6 +11,7 @@ import java.util.ArrayList; import java.util.List; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.dspace.content.Collection; import org.dspace.content.Community; @@ -202,7 +203,7 @@ private BrowseInfo browseByItem(BrowserScope bs) // get the table name that we are going to be getting our data from dao.setTable(browseIndex.getTableName()); - dao.setStartsWith(normalizeJumpToValue(scope.getStartsWith())); + dao.setStartsWith(StringUtils.lowerCase(scope.getStartsWith())); // tell the browse query whether we are ascending or descending on the value dao.setAscending(scope.isAscending()); From 135c6e6ec079194bbab47d97a6bde023ca00d832 Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Mon, 14 Mar 2022 18:16:31 +0100 Subject: [PATCH 0051/1846] 88196: Modify tests to verify that place recalculation does NOT matter --- .../VersioningWithRelationshipsTest.java | 90 ++++++++++++++----- 1 file changed, 69 insertions(+), 21 deletions(-) diff --git a/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java b/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java index accd878c7caa..b638c407a22b 100644 --- a/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java +++ b/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java @@ -1055,8 +1055,15 @@ public void test_createNewVersionOfItemAndVerifyMetadataOrder() throws Exception RelationshipBuilder.createRelationshipBuilder(context, originalPublication, author2, isAuthorOfPublication) .build(); - // author 3 (plain metadata) - itemService.addMetadata(context, originalPublication, "dc", "contributor", "author", null, "author 3 (plain)"); + // author 3 (virtual) + Item author3 = ItemBuilder.createItem(context, collection) + .withTitle("author 3 (item)") + .withMetadata("dspace", "entity", "type", personEntityType.getLabel()) + .withPersonIdentifierFirstName("3 (item)") + .withPersonIdentifierLastName("author") + .build(); + RelationshipBuilder.createRelationshipBuilder(context, originalPublication, author3, isAuthorOfPublication) + .build(); // author 4 (virtual) Item author4 = ItemBuilder.createItem(context, collection) @@ -1068,17 +1075,40 @@ public void test_createNewVersionOfItemAndVerifyMetadataOrder() throws Exception RelationshipBuilder.createRelationshipBuilder(context, originalPublication, author4, isAuthorOfPublication) .build(); - // author 5 (plain metadata) - itemService.addMetadata(context, originalPublication, "dc", "contributor", "author", null, "author 5 (plain)"); + // author 5 (virtual) + Item author5 = ItemBuilder.createItem(context, collection) + .withTitle("author 5 (item)") + .withMetadata("dspace", "entity", "type", personEntityType.getLabel()) + .withPersonIdentifierFirstName("5 (item)") + .withPersonIdentifierLastName("author") + .build(); + RelationshipBuilder.createRelationshipBuilder(context, originalPublication, author5, isAuthorOfPublication) + .build(); + + // author 6 (plain metadata) + itemService.addMetadata(context, originalPublication, "dc", "contributor", "author", null, "author 6 (plain)"); - // author 6 (virtual) - Item author6 = ItemBuilder.createItem(context, collection) - .withTitle("author 6 (item)") + // author 7 (virtual) + Item author7 = ItemBuilder.createItem(context, collection) + .withTitle("author 7 (item)") .withMetadata("dspace", "entity", "type", personEntityType.getLabel()) - .withPersonIdentifierFirstName("6 (item)") + .withPersonIdentifierFirstName("7 (item)") .withPersonIdentifierLastName("author") .build(); - RelationshipBuilder.createRelationshipBuilder(context, originalPublication, author6, isAuthorOfPublication) + RelationshipBuilder.createRelationshipBuilder(context, originalPublication, author7, isAuthorOfPublication) + .build(); + + // author 8 (plain metadata) + itemService.addMetadata(context, originalPublication, "dc", "contributor", "author", null, "author 8 (plain)"); + + // author 9 (virtual) + Item author9 = ItemBuilder.createItem(context, collection) + .withTitle("author 9 (item)") + .withMetadata("dspace", "entity", "type", personEntityType.getLabel()) + .withPersonIdentifierFirstName("9 (item)") + .withPersonIdentifierLastName("author") + .build(); + RelationshipBuilder.createRelationshipBuilder(context, originalPublication, author9, isAuthorOfPublication) .build(); //////////////////////////////// @@ -1095,17 +1125,26 @@ public void test_createNewVersionOfItemAndVerifyMetadataOrder() throws Exception assertTrue(oldMdvs.get(1) instanceof RelationshipMetadataValue); assertEquals("author, 2 (item)", oldMdvs.get(1).getValue()); - assertFalse(oldMdvs.get(2) instanceof RelationshipMetadataValue); - assertEquals("author 3 (plain)", oldMdvs.get(2).getValue()); + assertTrue(oldMdvs.get(2) instanceof RelationshipMetadataValue); + assertEquals("author, 3 (item)", oldMdvs.get(2).getValue()); assertTrue(oldMdvs.get(3) instanceof RelationshipMetadataValue); assertEquals("author, 4 (item)", oldMdvs.get(3).getValue()); - assertFalse(oldMdvs.get(4) instanceof RelationshipMetadataValue); - assertEquals("author 5 (plain)", oldMdvs.get(4).getValue()); + assertTrue(oldMdvs.get(4) instanceof RelationshipMetadataValue); + assertEquals("author, 5 (item)", oldMdvs.get(4).getValue()); + + assertFalse(oldMdvs.get(5) instanceof RelationshipMetadataValue); + assertEquals("author 6 (plain)", oldMdvs.get(5).getValue()); - assertTrue(oldMdvs.get(5) instanceof RelationshipMetadataValue); - assertEquals("author, 6 (item)", oldMdvs.get(5).getValue()); + assertTrue(oldMdvs.get(6) instanceof RelationshipMetadataValue); + assertEquals("author, 7 (item)", oldMdvs.get(6).getValue()); + + assertFalse(oldMdvs.get(7) instanceof RelationshipMetadataValue); + assertEquals("author 8 (plain)", oldMdvs.get(7).getValue()); + + assertTrue(oldMdvs.get(8) instanceof RelationshipMetadataValue); + assertEquals("author, 9 (item)", oldMdvs.get(8).getValue()); /////////////////////////////////////// // create new version of publication // @@ -1129,17 +1168,26 @@ public void test_createNewVersionOfItemAndVerifyMetadataOrder() throws Exception assertTrue(newMdvs.get(1) instanceof RelationshipMetadataValue); assertEquals("author, 2 (item)", newMdvs.get(1).getValue()); - assertFalse(newMdvs.get(2) instanceof RelationshipMetadataValue); - assertEquals("author 3 (plain)", newMdvs.get(2).getValue()); + assertTrue(newMdvs.get(2) instanceof RelationshipMetadataValue); + assertEquals("author, 3 (item)", newMdvs.get(2).getValue()); assertTrue(newMdvs.get(3) instanceof RelationshipMetadataValue); assertEquals("author, 4 (item)", newMdvs.get(3).getValue()); - assertFalse(newMdvs.get(4) instanceof RelationshipMetadataValue); - assertEquals("author 5 (plain)", newMdvs.get(4).getValue()); + assertTrue(newMdvs.get(4) instanceof RelationshipMetadataValue); + assertEquals("author, 5 (item)", newMdvs.get(4).getValue()); + + assertFalse(newMdvs.get(5) instanceof RelationshipMetadataValue); + assertEquals("author 6 (plain)", newMdvs.get(5).getValue()); + + assertTrue(newMdvs.get(6) instanceof RelationshipMetadataValue); + assertEquals("author, 7 (item)", newMdvs.get(6).getValue()); + + assertFalse(newMdvs.get(7) instanceof RelationshipMetadataValue); + assertEquals("author 8 (plain)", newMdvs.get(7).getValue()); - assertTrue(newMdvs.get(5) instanceof RelationshipMetadataValue); - assertEquals("author, 6 (item)", newMdvs.get(5).getValue()); + assertTrue(newMdvs.get(8) instanceof RelationshipMetadataValue); + assertEquals("author, 9 (item)", newMdvs.get(8).getValue()); ////////////// // clean up // From 9288a18c4ff4897a3f9f55a19bd8b29b16377b9f Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Wed, 16 Mar 2022 15:21:14 +0100 Subject: [PATCH 0052/1846] 88061: Verify modified relationships on new version of item before item install --- .../VersioningWithRelationshipsTest.java | 116 +++++++++++------- 1 file changed, 74 insertions(+), 42 deletions(-) diff --git a/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java b/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java index b638c407a22b..317def4d07b1 100644 --- a/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java +++ b/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java @@ -506,8 +506,40 @@ public void test_createNewVersionOfItemAndModifyRelationships() throws Exception Item newPublication = newVersion.getItem(); assertNotSame(originalPublication, newPublication); + ///////////////////////////////////////////// + // modify relationships on new publication // + ///////////////////////////////////////////// + + Item person2 = ItemBuilder.createItem(context, collection) + .withTitle("person 2") + .withMetadata("dspace", "entity", "type", personEntityType.getLabel()) + .build(); + + Item orgUnit2 = ItemBuilder.createItem(context, collection) + .withTitle("org unit 2") + .withMetadata("dspace", "entity", "type", orgUnitEntityType.getLabel()) + .build(); + + // on new item, remove relationship with project 1 + List newProjectRels = relationshipService + .findByItemAndRelationshipType(context, newPublication, isProjectOfPublication); + assertEquals(1, newProjectRels.size()); + relationshipService.delete(context, newProjectRels.get(0)); + + // on new item remove relationship with org unit 1 + List newOrgUnitRels = relationshipService + .findByItemAndRelationshipType(context, newPublication, isOrgUnitOfPublication); + assertEquals(1, newOrgUnitRels.size()); + relationshipService.delete(context, newOrgUnitRels.get(0)); + + RelationshipBuilder.createRelationshipBuilder(context, newPublication, person2, isAuthorOfPublication) + .build(); + + RelationshipBuilder.createRelationshipBuilder(context, newPublication, orgUnit2, isOrgUnitOfPublication) + .build(); + /////////////////////////////////////////////////////////////////////// - // verify the relationships of all 5 items (excludeNonLatest = true) // + // verify the relationships of all 7 items (excludeNonLatest = true) // /////////////////////////////////////////////////////////////////////// assertThat( @@ -526,6 +558,14 @@ public void test_createNewVersionOfItemAndModifyRelationships() throws Exception )) ); + assertThat( + relationshipService.findByItem(context, person2, -1, -1, false, true), + containsInAnyOrder(List.of( + // NOTE: BOTH because new relationship + isRelationship(newPublication, isAuthorOfPublication, person2, LatestVersionStatus.BOTH) + )) + ); + assertThat( relationshipService.findByItem(context, project1, -1, -1, false, true), containsInAnyOrder(List.of( @@ -540,17 +580,26 @@ public void test_createNewVersionOfItemAndModifyRelationships() throws Exception )) ); + assertThat( + relationshipService.findByItem(context, orgUnit2, -1, -1, false, true), + containsInAnyOrder(List.of( + // NOTE: BOTH because new relationship + isRelationship(newPublication, isOrgUnitOfPublication, orgUnit2, LatestVersionStatus.BOTH) + )) + ); + assertThat( relationshipService.findByItem(context, newPublication, -1, -1, false, true), containsInAnyOrder(List.of( isRelationship(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY), - isRelationship(newPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY), - isRelationship(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY) + // NOTE: BOTH because new relationship + isRelationship(newPublication, isAuthorOfPublication, person2, LatestVersionStatus.BOTH), + isRelationship(newPublication, isOrgUnitOfPublication, orgUnit2, LatestVersionStatus.BOTH) )) ); //////////////////////////////////////////////////////////////////////// - // verify the relationships of all 5 items (excludeNonLatest = false) // + // verify the relationships of all 7 items (excludeNonLatest = false) // //////////////////////////////////////////////////////////////////////// assertThat( @@ -570,19 +619,33 @@ public void test_createNewVersionOfItemAndModifyRelationships() throws Exception )) ); + assertThat( + relationshipService.findByItem(context, person2, -1, -1, false, false), + containsInAnyOrder(List.of( + // NOTE: BOTH because new relationship + isRelationship(newPublication, isAuthorOfPublication, person2, LatestVersionStatus.BOTH) + )) + ); + assertThat( relationshipService.findByItem(context, project1, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH), - isRelationship(newPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY) + isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH) )) ); assertThat( relationshipService.findByItem(context, orgUnit1, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH), - isRelationship(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY) + isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH) + )) + ); + + assertThat( + relationshipService.findByItem(context, orgUnit2, -1, -1, false, false), + containsInAnyOrder(List.of( + // NOTE: BOTH because new relationship + isRelationship(newPublication, isOrgUnitOfPublication, orgUnit2, LatestVersionStatus.BOTH) )) ); @@ -590,43 +653,12 @@ public void test_createNewVersionOfItemAndModifyRelationships() throws Exception relationshipService.findByItem(context, newPublication, -1, -1, false, false), containsInAnyOrder(List.of( isRelationship(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY), - isRelationship(newPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY), - isRelationship(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY) + // NOTE: BOTH because new relationship + isRelationship(newPublication, isAuthorOfPublication, person2, LatestVersionStatus.BOTH), + isRelationship(newPublication, isOrgUnitOfPublication, orgUnit2, LatestVersionStatus.BOTH) )) ); - ///////////////////////////////////////////// - // modify relationships on new publication // - ///////////////////////////////////////////// - - Item person2 = ItemBuilder.createItem(context, collection) - .withTitle("person 2") - .withMetadata("dspace", "entity", "type", personEntityType.getLabel()) - .build(); - - Item orgUnit2 = ItemBuilder.createItem(context, collection) - .withTitle("org unit 2") - .withMetadata("dspace", "entity", "type", orgUnitEntityType.getLabel()) - .build(); - - // on new item, remove relationship with project 1 - List newProjectRels = relationshipService - .findByItemAndRelationshipType(context, newPublication, isProjectOfPublication); - assertEquals(1, newProjectRels.size()); - relationshipService.delete(context, newProjectRels.get(0)); - - // on new item remove relationship with org unit 1 - List newOrgUnitRels = relationshipService - .findByItemAndRelationshipType(context, newPublication, isOrgUnitOfPublication); - assertEquals(1, newOrgUnitRels.size()); - relationshipService.delete(context, newOrgUnitRels.get(0)); - - RelationshipBuilder.createRelationshipBuilder(context, newPublication, person2, isAuthorOfPublication) - .build(); - - RelationshipBuilder.createRelationshipBuilder(context, newPublication, orgUnit2, isOrgUnitOfPublication) - .build(); - //////////////////////////////////////// // do item install on new publication // //////////////////////////////////////// From f3ff9af3e2830575e02b5734569e360b3d7abbe3 Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Wed, 16 Mar 2022 17:58:59 +0100 Subject: [PATCH 0053/1846] 88061: Clarify VersioningConsumer with extra comments --- .../dspace/versioning/VersioningConsumer.java | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/versioning/VersioningConsumer.java b/dspace-api/src/main/java/org/dspace/versioning/VersioningConsumer.java index b9489d1b22ec..b186daad8aa0 100644 --- a/dspace-api/src/main/java/org/dspace/versioning/VersioningConsumer.java +++ b/dspace-api/src/main/java/org/dspace/versioning/VersioningConsumer.java @@ -143,6 +143,31 @@ protected void unarchiveItem(Context ctx, Item item) { )); } + /** + * Update {@link Relationship#latestVersionStatus} of the relationships of both the old version and the new version + * of the item. + * + * This method will first locate all relationships that are eligible for an update, + * then it will try to match each of those relationships on the old version of given item + * with a relationship on the new version. + * + * One of the following scenarios will happen: + * - if a match is found, then the "latest" status on the side of given item is transferred from + * the old relationship to the new relationship. This implies that on the page of the third-party item, + * the old version of given item will NOT be shown anymore and the new version of given item will appear. + * Both versions of the given item still show the third-party item on their pages. + * - if a relationship only exists on the new version of given item, then this method does nothing. + * The status of those relationships should already have been set to "latest" on both sides during relationship + * creation. + * - if a relationship only exists on the old version of given item, then we assume that the relationship is no + * longer relevant to / has been removed from the new version of the item. The "latest" status is removed from + * the side of the given item. This implies that on the page of the third-party item, + * the relationship with given item will no longer be listed. The old version of given item still lists + * the third-party item and the new version doesn't. + * @param ctx the DSpace context. + * @param latestItem the new version of the item. + * @param previousItem the old version of the item. + */ protected void updateRelationships(Context ctx, Item latestItem, Item previousItem) { // check that the entity types of both items match if (!doEntityTypesMatch(latestItem, previousItem)) { @@ -172,6 +197,9 @@ protected void updateRelationships(Context ctx, Item latestItem, Item previousIt continue; } + // NOTE: no need to loop through latestItemRelationships, because if no match can be found + // (meaning a relationship is only present on the new version of the item), then it's + // a newly added relationship and its status should have been set to BOTH during creation. for (Relationship previousItemRelationship : previousItemRelationships) { // determine on which side of the relationship the latest and previous item should be boolean isLeft = previousItem.equals(previousItemRelationship.getLeftItem()); @@ -195,12 +223,16 @@ protected void updateRelationships(Context ctx, Item latestItem, Item previousIt Relationship latestItemRelationship = getMatchingRelationship(latestItem, isLeft, previousItemRelationship, latestItemRelationships); - // for sure set the previous item to non-latest - // NOTE: if no matching relationship exists, this relationship will be considered deleted - // when viewed from the other item + // Set the previous version of the item to non-latest. This implies that the previous version + // of the item will not be shown anymore on the page of the third-party item. That makes sense, + // because either the relationship has been deleted from the new version of the item (no match), + // or the matching relationship (linked to new version) will receive "latest" status in the next step. updateLatestVersionStatus(previousItemRelationship, isLeft, false); - // set the new item to latest if the relevant relationship exists + // Set the new version of the item to latest if the relevant relationship exists (match found). + // This implies that the new version of the item will appear on the page of the third-party item. + // The old version of the item will not appear anymore on the page of the third-party item, + // see previous step. if (latestItemRelationship != null) { updateLatestVersionStatus(latestItemRelationship, isLeft, true); } From 1fcff8900704c628f41d92633358c9804e397a9b Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Fri, 18 Mar 2022 01:32:56 +0100 Subject: [PATCH 0054/1846] 88595: Set versioning.block.entity = false --- dspace/config/modules/versioning.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace/config/modules/versioning.cfg b/dspace/config/modules/versioning.cfg index a345294242cf..2d2ba9450bde 100644 --- a/dspace/config/modules/versioning.cfg +++ b/dspace/config/modules/versioning.cfg @@ -23,4 +23,4 @@ versioning.item.history.include.submitter=false # The property versioning.block.entity is used to disable versioning # for items with EntityType, the default value is true if it unset. -# versioning.block.entity = true \ No newline at end of file +versioning.block.entity=false From 46d9ba91bc93b3451b6f9a874b4cdb956a39a9dc Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Mon, 21 Mar 2022 10:26:43 +0100 Subject: [PATCH 0055/1846] 88599: Test metadata and relationship place when creating new version of item --- .../VersioningWithRelationshipsTest.java | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java b/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java index 317def4d07b1..272d2ccb2bb5 100644 --- a/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java +++ b/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java @@ -1150,33 +1150,86 @@ public void test_createNewVersionOfItemAndVerifyMetadataOrder() throws Exception List oldMdvs = itemService.getMetadata( originalPublication, "dc", "contributor", "author", Item.ANY ); + assertEquals(9, oldMdvs.size()); assertFalse(oldMdvs.get(0) instanceof RelationshipMetadataValue); assertEquals("author 1 (plain)", oldMdvs.get(0).getValue()); + assertEquals(0, oldMdvs.get(0).getPlace()); assertTrue(oldMdvs.get(1) instanceof RelationshipMetadataValue); assertEquals("author, 2 (item)", oldMdvs.get(1).getValue()); + assertEquals(1, oldMdvs.get(1).getPlace()); assertTrue(oldMdvs.get(2) instanceof RelationshipMetadataValue); assertEquals("author, 3 (item)", oldMdvs.get(2).getValue()); + assertEquals(2, oldMdvs.get(2).getPlace()); assertTrue(oldMdvs.get(3) instanceof RelationshipMetadataValue); assertEquals("author, 4 (item)", oldMdvs.get(3).getValue()); + assertEquals(3, oldMdvs.get(3).getPlace()); assertTrue(oldMdvs.get(4) instanceof RelationshipMetadataValue); assertEquals("author, 5 (item)", oldMdvs.get(4).getValue()); + assertEquals(4, oldMdvs.get(4).getPlace()); assertFalse(oldMdvs.get(5) instanceof RelationshipMetadataValue); assertEquals("author 6 (plain)", oldMdvs.get(5).getValue()); + assertEquals(5, oldMdvs.get(5).getPlace()); assertTrue(oldMdvs.get(6) instanceof RelationshipMetadataValue); assertEquals("author, 7 (item)", oldMdvs.get(6).getValue()); + assertEquals(6, oldMdvs.get(6).getPlace()); assertFalse(oldMdvs.get(7) instanceof RelationshipMetadataValue); assertEquals("author 8 (plain)", oldMdvs.get(7).getValue()); + assertEquals(7, oldMdvs.get(7).getPlace()); assertTrue(oldMdvs.get(8) instanceof RelationshipMetadataValue); assertEquals("author, 9 (item)", oldMdvs.get(8).getValue()); + assertEquals(8, oldMdvs.get(8).getPlace()); + + ///////////////////////////////////////////// + // test relationship isAuthorOfPublication // + ///////////////////////////////////////////// + + List oldRelationships = relationshipService.findByItem(context, originalPublication); + assertEquals(6, oldRelationships.size()); + + assertEquals(originalPublication, oldRelationships.get(0).getLeftItem()); + assertEquals(isAuthorOfPublication, oldRelationships.get(0).getRelationshipType()); + assertEquals(author2, oldRelationships.get(0).getRightItem()); + assertEquals(1, oldRelationships.get(0).getLeftPlace()); + assertEquals(0, oldRelationships.get(0).getRightPlace()); + + assertEquals(originalPublication, oldRelationships.get(1).getLeftItem()); + assertEquals(isAuthorOfPublication, oldRelationships.get(1).getRelationshipType()); + assertEquals(author3, oldRelationships.get(1).getRightItem()); + assertEquals(2, oldRelationships.get(1).getLeftPlace()); + assertEquals(0, oldRelationships.get(1).getRightPlace()); + + assertEquals(originalPublication, oldRelationships.get(2).getLeftItem()); + assertEquals(isAuthorOfPublication, oldRelationships.get(2).getRelationshipType()); + assertEquals(author4, oldRelationships.get(2).getRightItem()); + assertEquals(3, oldRelationships.get(2).getLeftPlace()); + assertEquals(0, oldRelationships.get(2).getRightPlace()); + + assertEquals(originalPublication, oldRelationships.get(3).getLeftItem()); + assertEquals(isAuthorOfPublication, oldRelationships.get(3).getRelationshipType()); + assertEquals(author5, oldRelationships.get(3).getRightItem()); + assertEquals(4, oldRelationships.get(3).getLeftPlace()); + assertEquals(0, oldRelationships.get(3).getRightPlace()); + + assertEquals(originalPublication, oldRelationships.get(4).getLeftItem()); + assertEquals(isAuthorOfPublication, oldRelationships.get(4).getRelationshipType()); + assertEquals(author7, oldRelationships.get(4).getRightItem()); + assertEquals(6, oldRelationships.get(4).getLeftPlace()); + assertEquals(0, oldRelationships.get(4).getRightPlace()); + + assertEquals(originalPublication, oldRelationships.get(5).getLeftItem()); + assertEquals(isAuthorOfPublication, oldRelationships.get(5).getRelationshipType()); + assertEquals(author9, oldRelationships.get(5).getRightItem()); + assertEquals(8, oldRelationships.get(5).getLeftPlace()); + assertEquals(0, oldRelationships.get(5).getRightPlace()); /////////////////////////////////////// // create new version of publication // @@ -1193,33 +1246,86 @@ public void test_createNewVersionOfItemAndVerifyMetadataOrder() throws Exception List newMdvs = itemService.getMetadata( newPublication, "dc", "contributor", "author", Item.ANY ); + assertEquals(9, newMdvs.size()); assertFalse(newMdvs.get(0) instanceof RelationshipMetadataValue); assertEquals("author 1 (plain)", newMdvs.get(0).getValue()); + assertEquals(0, newMdvs.get(0).getPlace()); assertTrue(newMdvs.get(1) instanceof RelationshipMetadataValue); assertEquals("author, 2 (item)", newMdvs.get(1).getValue()); + assertEquals(1, newMdvs.get(1).getPlace()); assertTrue(newMdvs.get(2) instanceof RelationshipMetadataValue); assertEquals("author, 3 (item)", newMdvs.get(2).getValue()); + assertEquals(2, newMdvs.get(2).getPlace()); assertTrue(newMdvs.get(3) instanceof RelationshipMetadataValue); assertEquals("author, 4 (item)", newMdvs.get(3).getValue()); + assertEquals(3, newMdvs.get(3).getPlace()); assertTrue(newMdvs.get(4) instanceof RelationshipMetadataValue); assertEquals("author, 5 (item)", newMdvs.get(4).getValue()); + assertEquals(4, newMdvs.get(4).getPlace()); assertFalse(newMdvs.get(5) instanceof RelationshipMetadataValue); assertEquals("author 6 (plain)", newMdvs.get(5).getValue()); + assertEquals(5, newMdvs.get(5).getPlace()); assertTrue(newMdvs.get(6) instanceof RelationshipMetadataValue); assertEquals("author, 7 (item)", newMdvs.get(6).getValue()); + assertEquals(6, newMdvs.get(6).getPlace()); assertFalse(newMdvs.get(7) instanceof RelationshipMetadataValue); assertEquals("author 8 (plain)", newMdvs.get(7).getValue()); + assertEquals(7, newMdvs.get(7).getPlace()); assertTrue(newMdvs.get(8) instanceof RelationshipMetadataValue); assertEquals("author, 9 (item)", newMdvs.get(8).getValue()); + assertEquals(8, newMdvs.get(8).getPlace()); + + ///////////////////////////////////////////// + // test relationship isAuthorOfPublication // + ///////////////////////////////////////////// + + List newRelationships = relationshipService.findByItem(context, newPublication); + assertEquals(6, newRelationships.size()); + + assertEquals(newPublication, newRelationships.get(0).getLeftItem()); + assertEquals(isAuthorOfPublication, newRelationships.get(0).getRelationshipType()); + assertEquals(author2, newRelationships.get(0).getRightItem()); + assertEquals(1, newRelationships.get(0).getLeftPlace()); + assertEquals(0, newRelationships.get(0).getRightPlace()); + + assertEquals(newPublication, newRelationships.get(1).getLeftItem()); + assertEquals(isAuthorOfPublication, newRelationships.get(1).getRelationshipType()); + assertEquals(author3, newRelationships.get(1).getRightItem()); + assertEquals(2, newRelationships.get(1).getLeftPlace()); + assertEquals(0, newRelationships.get(1).getRightPlace()); + + assertEquals(newPublication, newRelationships.get(2).getLeftItem()); + assertEquals(isAuthorOfPublication, newRelationships.get(2).getRelationshipType()); + assertEquals(author4, newRelationships.get(2).getRightItem()); + assertEquals(3, newRelationships.get(2).getLeftPlace()); + assertEquals(0, newRelationships.get(2).getRightPlace()); + + assertEquals(newPublication, newRelationships.get(3).getLeftItem()); + assertEquals(isAuthorOfPublication, newRelationships.get(3).getRelationshipType()); + assertEquals(author5, newRelationships.get(3).getRightItem()); + assertEquals(4, newRelationships.get(3).getLeftPlace()); + assertEquals(0, newRelationships.get(3).getRightPlace()); + + assertEquals(newPublication, newRelationships.get(4).getLeftItem()); + assertEquals(isAuthorOfPublication, newRelationships.get(4).getRelationshipType()); + assertEquals(author7, newRelationships.get(4).getRightItem()); + assertEquals(6, newRelationships.get(4).getLeftPlace()); + assertEquals(0, newRelationships.get(4).getRightPlace()); + + assertEquals(newPublication, newRelationships.get(5).getLeftItem()); + assertEquals(isAuthorOfPublication, newRelationships.get(5).getRelationshipType()); + assertEquals(author9, newRelationships.get(5).getRightItem()); + assertEquals(8, newRelationships.get(5).getLeftPlace()); + assertEquals(0, newRelationships.get(5).getRightPlace()); ////////////// // clean up // From 0d148abf1169590831dcba2c15d805541be080f7 Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Tue, 22 Mar 2022 11:22:32 +0100 Subject: [PATCH 0056/1846] 88629: Prove that ItemServiceImpl#rawDelete is broken --- .../content/service/ItemServiceTest.java | 68 +++++++++++++++++-- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/dspace-api/src/test/java/org/dspace/content/service/ItemServiceTest.java b/dspace-api/src/test/java/org/dspace/content/service/ItemServiceTest.java index c66a6eed7669..dbed948737a2 100644 --- a/dspace-api/src/test/java/org/dspace/content/service/ItemServiceTest.java +++ b/dspace-api/src/test/java/org/dspace/content/service/ItemServiceTest.java @@ -9,6 +9,7 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import java.sql.SQLException; @@ -17,18 +18,28 @@ import java.util.stream.Collectors; import org.apache.logging.log4j.Logger; -import org.dspace.AbstractUnitTest; +import org.dspace.AbstractIntegrationTestWithDatabase; import org.dspace.authorize.AuthorizeException; +import org.dspace.builder.EntityTypeBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.builder.RelationshipBuilder; +import org.dspace.builder.RelationshipTypeBuilder; import org.dspace.content.Collection; import org.dspace.content.Community; +import org.dspace.content.EntityType; import org.dspace.content.Item; import org.dspace.content.MetadataValue; +import org.dspace.content.Relationship; +import org.dspace.content.RelationshipType; import org.dspace.content.WorkspaceItem; import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.versioning.Version; +import org.dspace.versioning.factory.VersionServiceFactory; +import org.dspace.versioning.service.VersioningService; import org.junit.Before; import org.junit.Test; -public class ItemServiceTest extends AbstractUnitTest { +public class ItemServiceTest extends AbstractIntegrationTestWithDatabase { private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(ItemServiceTest.class); protected RelationshipService relationshipService = ContentServiceFactory.getInstance().getRelationshipService(); @@ -41,6 +52,7 @@ public class ItemServiceTest extends AbstractUnitTest { protected InstallItemService installItemService = ContentServiceFactory.getInstance().getInstallItemService(); protected WorkspaceItemService workspaceItemService = ContentServiceFactory.getInstance().getWorkspaceItemService(); protected MetadataValueService metadataValueService = ContentServiceFactory.getInstance().getMetadataValueService(); + protected VersioningService versioningService = VersionServiceFactory.getInstance().getVersionService(); Community community; Collection col; @@ -57,8 +69,8 @@ public class ItemServiceTest extends AbstractUnitTest { */ @Before @Override - public void init() { - super.init(); + public void setUp() throws Exception { + super.setUp(); try { context.turnOffAuthorisationSystem(); community = communityService.create(null, context); @@ -190,6 +202,54 @@ public void InsertAndMoveMetadataOnePlaceForwardTest() throws Exception { assertMetadataValue(authorQualifier, contributorElement, dcSchema, "test, three", null, 3, list.get(3)); } + @Test + public void testDeleteItemWithMultipleVersions() throws Exception { + context.turnOffAuthorisationSystem(); + + EntityType publicationEntityType = EntityTypeBuilder.createEntityTypeBuilder(context, "Publication") + .build(); + + EntityType personEntityType = EntityTypeBuilder.createEntityTypeBuilder(context, "Person") + .build(); + + RelationshipType isAuthorOfPublication = RelationshipTypeBuilder.createRelationshipTypeBuilder( + context, publicationEntityType, personEntityType, "isAuthorOfPublication", "isPublicationOfAuthor", + null, null, null, null + ) + .withCopyToLeft(false) + .withCopyToRight(false) + .build(); + + Item publication1 = ItemBuilder.createItem(context, col) + .withTitle("publication 1") + .withEntityType("Publication") + .build(); + + Item person1 = ItemBuilder.createItem(context, col) + .withTitle("person 2") + .withEntityType("Person") + .build(); + + RelationshipBuilder.createRelationshipBuilder(context, publication1, person1, isAuthorOfPublication); + + // create a new version, which results in a non-latest relationship attached person 1. + Version newVersion = versioningService.createNewVersion(context, publication1); + Item newPublication1 = newVersion.getItem(); + WorkspaceItem newPublication1WSI = workspaceItemService.findByItem(context, newPublication1); + installItemService.installItem(context, newPublication1WSI); + context.dispatchEvents(); + + // verify person1 has a non-latest relationship, which should also be removed + List relationships1 = relationshipService.findByItem(context, person1, -1, -1, false, true); + assertEquals(1, relationships1.size()); + List relationships2 = relationshipService.findByItem(context, person1, -1, -1, false, false); + assertEquals(2, relationships2.size()); + + itemService.delete(context, person1); + + context.restoreAuthSystemState(); + } + private void assertMetadataValue(String authorQualifier, String contributorElement, String dcSchema, String value, String authority, int place, MetadataValue metadataValue) { assertThat(metadataValue.getValue(), equalTo(value)); From d55935c19748253f68e2c6b08a4da9679eade1f7 Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Tue, 22 Mar 2022 11:25:15 +0100 Subject: [PATCH 0057/1846] 88629: Fix ItemServiceImpl#rawDelete --- .../src/main/java/org/dspace/content/ItemServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java index 96dac1a4df3d..c1848a1ebf4d 100644 --- a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java @@ -724,7 +724,7 @@ protected void rawDelete(Context context, Item item) throws AuthorizeException, + item.getID())); // Remove relationships - for (Relationship relationship : relationshipService.findByItem(context, item)) { + for (Relationship relationship : relationshipService.findByItem(context, item, -1, -1, false, false)) { relationshipService.forceDelete(context, relationship, false, false); } From ae25c67064f9c4a841c355e81b228c71050cc6f3 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Tue, 15 Mar 2022 09:47:03 -0500 Subject: [PATCH 0058/1846] Update to JDOM2. Also replaced deprecated XPath with new XPathFactory --- dspace-api/pom.xml | 2 +- .../org/dspace/administer/StructBuilder.java | 10 +-- .../dspace/app/launcher/CommandRunner.java | 2 +- .../dspace/app/launcher/ScriptLauncher.java | 6 +- .../org/dspace/app/util/GoogleMetadata.java | 2 +- .../app/util/OpenSearchServiceImpl.java | 16 ++-- .../content/crosswalk/AIPDIMCrosswalk.java | 4 +- .../content/crosswalk/AIPTechMDCrosswalk.java | 4 +- .../crosswalk/DIMDisseminationCrosswalk.java | 4 +- .../crosswalk/DIMIngestionCrosswalk.java | 4 +- .../crosswalk/DisseminationCrosswalk.java | 4 +- .../content/crosswalk/IngestionCrosswalk.java | 2 +- .../crosswalk/METSDisseminationCrosswalk.java | 10 +-- .../crosswalk/METSRightsCrosswalk.java | 4 +- .../crosswalk/MODSDisseminationCrosswalk.java | 82 ++++++++--------- .../crosswalk/NullIngestionCrosswalk.java | 6 +- .../crosswalk/OAIDCIngestionCrosswalk.java | 4 +- .../crosswalk/OREDisseminationCrosswalk.java | 4 +- .../crosswalk/OREIngestionCrosswalk.java | 53 +++++------ .../content/crosswalk/PREMISCrosswalk.java | 4 +- .../ParameterizedDisseminationCrosswalk.java | 2 +- .../content/crosswalk/QDCCrosswalk.java | 11 +-- .../content/crosswalk/RoleCrosswalk.java | 12 +-- .../SimpleDCDisseminationCrosswalk.java | 5 +- .../XHTMLHeadDisseminationCrosswalk.java | 7 +- .../content/crosswalk/XSLTCrosswalk.java | 2 +- .../crosswalk/XSLTDisseminationCrosswalk.java | 26 ++++-- .../crosswalk/XSLTIngestionCrosswalk.java | 23 +++-- .../packager/AbstractMETSDisseminator.java | 8 +- .../packager/AbstractMETSIngester.java | 2 +- .../content/packager/DSpaceAIPIngester.java | 2 +- .../content/packager/DSpaceMETSIngester.java | 2 +- .../dspace/content/packager/METSManifest.java | 90 +++++++++---------- .../content/packager/RoleDisseminator.java | 2 +- .../HarvestedCollectionServiceImpl.java | 8 +- .../java/org/dspace/harvest/OAIHarvester.java | 10 +-- .../dspace/identifier/DataCiteXMLCreator.java | 4 +- .../identifier/doi/DataCiteConnector.java | 14 +-- .../license/CCLicenseConnectorService.java | 2 +- .../CCLicenseConnectorServiceImpl.java | 16 ++-- .../license/CreativeCommonsServiceImpl.java | 4 +- .../service/CreativeCommonsService.java | 2 +- .../org/dspace/testing/PubMedToImport.java | 8 +- .../AbstractIntegrationTestWithDatabase.java | 2 +- .../org/dspace/app/packager/PackagerIT.java | 2 +- .../content/crosswalk/QDCCrosswalkTest.java | 2 +- .../MockCCLicenseConnectorServiceImpl.java | 4 +- .../MockCCLicenseConnectorServiceImpl.java | 4 +- pom.xml | 4 +- 49 files changed, 253 insertions(+), 254 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index c3c531d59b0a..1ac1c3742ee9 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -520,7 +520,7 @@ org.jdom - jdom + jdom2 org.apache.pdfbox diff --git a/dspace-api/src/main/java/org/dspace/administer/StructBuilder.java b/dspace-api/src/main/java/org/dspace/administer/StructBuilder.java index 89d9ffe5a841..7a60313f8e15 100644 --- a/dspace-api/src/main/java/org/dspace/administer/StructBuilder.java +++ b/dspace-api/src/main/java/org/dspace/administer/StructBuilder.java @@ -52,9 +52,9 @@ import org.dspace.core.Context; import org.dspace.eperson.factory.EPersonServiceFactory; import org.dspace.eperson.service.EPersonService; -import org.jdom.Element; -import org.jdom.output.Format; -import org.jdom.output.XMLOutputter; +import org.jdom2.Element; +import org.jdom2.output.Format; +import org.jdom2.output.XMLOutputter; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; @@ -307,7 +307,7 @@ static void importStructure(Context context, InputStream input, OutputStream out } // finally write the string into the output file. - final org.jdom.Document xmlOutput = new org.jdom.Document(root); + final org.jdom2.Document xmlOutput = new org.jdom2.Document(root); try { new XMLOutputter().output(xmlOutput, output); } catch (IOException e) { @@ -411,7 +411,7 @@ static void exportStructure(Context context, OutputStream output) { } // Now write the structure out. - org.jdom.Document xmlOutput = new org.jdom.Document(rootElement); + org.jdom2.Document xmlOutput = new org.jdom2.Document(rootElement); try { XMLOutputter outputter = new XMLOutputter(Format.getPrettyFormat()); outputter.output(xmlOutput, output); diff --git a/dspace-api/src/main/java/org/dspace/app/launcher/CommandRunner.java b/dspace-api/src/main/java/org/dspace/app/launcher/CommandRunner.java index ce33b6655bc6..06c2ddb48340 100644 --- a/dspace-api/src/main/java/org/dspace/app/launcher/CommandRunner.java +++ b/dspace-api/src/main/java/org/dspace/app/launcher/CommandRunner.java @@ -16,7 +16,7 @@ import java.util.ArrayList; import java.util.List; -import org.jdom.Document; +import org.jdom2.Document; /** * @author mwood diff --git a/dspace-api/src/main/java/org/dspace/app/launcher/ScriptLauncher.java b/dspace-api/src/main/java/org/dspace/app/launcher/ScriptLauncher.java index d445f9bbf3f5..fcb2098bd066 100644 --- a/dspace-api/src/main/java/org/dspace/app/launcher/ScriptLauncher.java +++ b/dspace-api/src/main/java/org/dspace/app/launcher/ScriptLauncher.java @@ -29,9 +29,9 @@ import org.dspace.servicemanager.DSpaceKernelImpl; import org.dspace.servicemanager.DSpaceKernelInit; import org.dspace.services.RequestService; -import org.jdom.Document; -import org.jdom.Element; -import org.jdom.input.SAXBuilder; +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.input.SAXBuilder; /** * A DSpace script launcher. diff --git a/dspace-api/src/main/java/org/dspace/app/util/GoogleMetadata.java b/dspace-api/src/main/java/org/dspace/app/util/GoogleMetadata.java index 0021f267005f..72e2af409f90 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/GoogleMetadata.java +++ b/dspace-api/src/main/java/org/dspace/app/util/GoogleMetadata.java @@ -42,7 +42,7 @@ import org.dspace.handle.factory.HandleServiceFactory; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; -import org.jdom.Element; +import org.jdom2.Element; /** * Configuration and mapping for Google Scholar output metadata diff --git a/dspace-api/src/main/java/org/dspace/app/util/OpenSearchServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/util/OpenSearchServiceImpl.java index 97f25cb2b213..474ee4c99cea 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/OpenSearchServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/util/OpenSearchServiceImpl.java @@ -29,11 +29,11 @@ import org.dspace.handle.service.HandleService; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; -import org.jdom.Element; -import org.jdom.JDOMException; -import org.jdom.Namespace; -import org.jdom.output.DOMOutputter; -import org.jdom.output.XMLOutputter; +import org.jdom2.Element; +import org.jdom2.JDOMException; +import org.jdom2.Namespace; +import org.jdom2.output.DOMOutputter; +import org.jdom2.output.XMLOutputter; import org.springframework.beans.factory.annotation.Autowired; import org.w3c.dom.Document; @@ -192,7 +192,7 @@ protected OpenSearchModule openSearchMarkup(String query, int totalResults, int * @param scope - null for the entire repository, or a collection/community handle * @return Service Document */ - protected org.jdom.Document getServiceDocument(String scope) { + protected org.jdom2.Document getServiceDocument(String scope) { ConfigurationService config = DSpaceServicesFactory.getInstance().getConfigurationService(); Namespace ns = Namespace.getNamespace(osNs); @@ -245,7 +245,7 @@ protected org.jdom.Document getServiceDocument(String scope) { url.setAttribute("template", template.toString()); root.addContent(url); } - return new org.jdom.Document(root); + return new org.jdom2.Document(root); } /** @@ -255,7 +255,7 @@ protected org.jdom.Document getServiceDocument(String scope) { * @return W3C Document object * @throws IOException if IO error */ - protected Document jDomToW3(org.jdom.Document jdomDoc) throws IOException { + protected Document jDomToW3(org.jdom2.Document jdomDoc) throws IOException { DOMOutputter domOut = new DOMOutputter(); try { return domOut.output(jdomDoc); diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/AIPDIMCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/AIPDIMCrosswalk.java index 2d919baa9d29..4b77e4807a34 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/AIPDIMCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/AIPDIMCrosswalk.java @@ -14,8 +14,8 @@ import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; import org.dspace.core.Context; -import org.jdom.Element; -import org.jdom.Namespace; +import org.jdom2.Element; +import org.jdom2.Namespace; /** * Crosswalk descriptive metadata to and from DIM (DSpace Intermediate diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/AIPTechMDCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/AIPTechMDCrosswalk.java index 8ffddf715f50..978cabfb4bd6 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/AIPTechMDCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/AIPTechMDCrosswalk.java @@ -40,8 +40,8 @@ import org.dspace.handle.service.HandleService; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; -import org.jdom.Element; -import org.jdom.Namespace; +import org.jdom2.Element; +import org.jdom2.Namespace; /** * Crosswalk of technical metadata for DSpace AIP. This is diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/DIMDisseminationCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/DIMDisseminationCrosswalk.java index 3f4d6bd44ee7..4365d9a48533 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/DIMDisseminationCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/DIMDisseminationCrosswalk.java @@ -23,8 +23,8 @@ import org.dspace.content.service.ItemService; import org.dspace.core.Constants; import org.dspace.core.Context; -import org.jdom.Element; -import org.jdom.Namespace; +import org.jdom2.Element; +import org.jdom2.Namespace; /** * DIM dissemination crosswalk diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/DIMIngestionCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/DIMIngestionCrosswalk.java index ad922a65f275..4217308e65da 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/DIMIngestionCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/DIMIngestionCrosswalk.java @@ -19,8 +19,8 @@ import org.dspace.content.service.ItemService; import org.dspace.core.Constants; import org.dspace.core.Context; -import org.jdom.Element; -import org.jdom.Namespace; +import org.jdom2.Element; +import org.jdom2.Namespace; /** * DIM ingestion crosswalk diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/DisseminationCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/DisseminationCrosswalk.java index 23e1965d7b38..3e4fe21f8fa7 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/DisseminationCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/DisseminationCrosswalk.java @@ -14,8 +14,8 @@ import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; import org.dspace.core.Context; -import org.jdom.Element; -import org.jdom.Namespace; +import org.jdom2.Element; +import org.jdom2.Namespace; /** * Dissemination Crosswalk plugin -- translate DSpace native diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/IngestionCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/IngestionCrosswalk.java index 7edfb6f79fd9..bb73c83c459e 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/IngestionCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/IngestionCrosswalk.java @@ -14,7 +14,7 @@ import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; import org.dspace.core.Context; -import org.jdom.Element; +import org.jdom2.Element; /** * Ingestion Crosswalk plugin -- translate an external metadata format diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/METSDisseminationCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/METSDisseminationCrosswalk.java index e44774a672b3..b8a4a8aef390 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/METSDisseminationCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/METSDisseminationCrosswalk.java @@ -24,11 +24,11 @@ import org.dspace.core.factory.CoreServiceFactory; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; -import org.jdom.Document; -import org.jdom.Element; -import org.jdom.JDOMException; -import org.jdom.Namespace; -import org.jdom.input.SAXBuilder; +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.JDOMException; +import org.jdom2.Namespace; +import org.jdom2.input.SAXBuilder; /** * METS dissemination crosswalk diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/METSRightsCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/METSRightsCrosswalk.java index 559d463be2ed..7f6622841ba7 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/METSRightsCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/METSRightsCrosswalk.java @@ -35,8 +35,8 @@ import org.dspace.eperson.factory.EPersonServiceFactory; import org.dspace.eperson.service.EPersonService; import org.dspace.eperson.service.GroupService; -import org.jdom.Element; -import org.jdom.Namespace; +import org.jdom2.Element; +import org.jdom2.Namespace; /** * METSRights Ingestion and Dissemination Crosswalk diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/MODSDisseminationCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/MODSDisseminationCrosswalk.java index 182fcebe2ff3..57202f656e16 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/MODSDisseminationCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/MODSDisseminationCrosswalk.java @@ -15,7 +15,6 @@ import java.util.ArrayList; import java.util.Enumeration; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; @@ -42,16 +41,18 @@ import org.dspace.handle.service.HandleService; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; -import org.jdom.Attribute; -import org.jdom.Document; -import org.jdom.Element; -import org.jdom.JDOMException; -import org.jdom.Namespace; -import org.jdom.Text; -import org.jdom.Verifier; -import org.jdom.input.SAXBuilder; -import org.jdom.output.XMLOutputter; -import org.jdom.xpath.XPath; +import org.jdom2.Attribute; +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.JDOMException; +import org.jdom2.Namespace; +import org.jdom2.Text; +import org.jdom2.Verifier; +import org.jdom2.filter.Filters; +import org.jdom2.input.SAXBuilder; +import org.jdom2.output.XMLOutputter; +import org.jdom2.xpath.XPathExpression; +import org.jdom2.xpath.XPathFactory; /** * Configurable MODS Crosswalk @@ -156,7 +157,7 @@ public static String[] getPluginNames() { static class modsTriple { public String qdc = null; public Element xml = null; - public XPath xpath = null; + public XPathExpression xpath = null; /** * Initialize from text versions of QDC, XML and XPath. @@ -171,9 +172,9 @@ public static modsTriple create(String qdc, String xml, String xpath) { final String postlog = ""; try { result.qdc = qdc; - result.xpath = XPath.newInstance(xpath); - result.xpath.addNamespace(MODS_NS.getPrefix(), MODS_NS.getURI()); - result.xpath.addNamespace(XLINK_NS); + result.xpath = + XPathFactory.instance() + .compile(xpath, Filters.fpassthrough(), null, MODS_NS, XLINK_NS); Document d = builder.build(new StringReader(prolog + xml + postlog)); result.xml = (Element) d.getRootElement().getContent(0); } catch (JDOMException | IOException je) { @@ -295,6 +296,7 @@ public String getSchemaLocation() { * @throws IOException if IO error * @throws SQLException if database error * @throws AuthorizeException if authorization error + * @return List of Elements */ @Override public List disseminateList(Context context, DSpaceObject dso) @@ -352,37 +354,29 @@ private List disseminateListInternal(DSpaceObject dso, boolean addSchem if (trip == null) { log.warn("WARNING: " + getPluginInstanceName() + ": No MODS mapping for \"" + qdc + "\""); } else { - try { - Element me = (Element) trip.xml.clone(); - if (addSchema) { - me.setAttribute("schemaLocation", schemaLocation, XSI_NS); - } - Iterator ni = trip.xpath.selectNodes(me).iterator(); - if (!ni.hasNext()) { - log.warn("XPath \"" + trip.xpath.getXPath() + - "\" found no elements in \"" + - outputUgly.outputString(me) + - "\", qdc=" + qdc); - } - while (ni.hasNext()) { - Object what = ni.next(); - if (what instanceof Element) { - ((Element) what).setText(checkedString(value)); - } else if (what instanceof Attribute) { - ((Attribute) what).setValue(checkedString(value)); - } else if (what instanceof Text) { - ((Text) what).setText(checkedString(value)); - } else { - log.warn("Got unknown object from XPath, class=" + what.getClass().getName()); - } + Element me = (Element) trip.xml.clone(); + if (addSchema) { + me.setAttribute("schemaLocation", schemaLocation, XSI_NS); + } + List matches = trip.xpath.evaluate(me); + if (matches.isEmpty()) { + log.warn("XPath \"" + trip.xpath.getExpression() + + "\" found no elements in \"" + + outputUgly.outputString(me) + + "\", qdc=" + qdc); + } + for (Object match: matches) { + if (match instanceof Element) { + ((Element) match).setText(checkedString(value)); + } else if (match instanceof Attribute) { + ((Attribute) match).setValue(checkedString(value)); + } else if (match instanceof Text) { + ((Text) match).setText(checkedString(value)); + } else { + log.warn("Got unknown object from XPath, class=" + match.getClass().getName()); } - result.add(me); - } catch (JDOMException je) { - log.error("Error following XPath in modsTriple: context=" + - outputUgly.outputString(trip.xml) + - ", xpath=" + trip.xpath.getXPath() + ", exception=" + - je.toString()); } + result.add(me); } } return result; diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/NullIngestionCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/NullIngestionCrosswalk.java index 994e15601dff..562dadaca0bb 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/NullIngestionCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/NullIngestionCrosswalk.java @@ -15,9 +15,9 @@ import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; import org.dspace.core.Context; -import org.jdom.Element; -import org.jdom.output.Format; -import org.jdom.output.XMLOutputter; +import org.jdom2.Element; +import org.jdom2.output.Format; +import org.jdom2.output.XMLOutputter; /** * "Null" ingestion crosswalk diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/OAIDCIngestionCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/OAIDCIngestionCrosswalk.java index 10bd5ce6fa31..6b0ecae780ce 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/OAIDCIngestionCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/OAIDCIngestionCrosswalk.java @@ -20,8 +20,8 @@ import org.dspace.content.service.ItemService; import org.dspace.core.Constants; import org.dspace.core.Context; -import org.jdom.Element; -import org.jdom.Namespace; +import org.jdom2.Element; +import org.jdom2.Namespace; /** * DIM ingestion crosswalk diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/OREDisseminationCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/OREDisseminationCrosswalk.java index 3dde093784de..ac1c434322a6 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/OREDisseminationCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/OREDisseminationCrosswalk.java @@ -31,8 +31,8 @@ import org.dspace.core.Utils; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; -import org.jdom.Element; -import org.jdom.Namespace; +import org.jdom2.Element; +import org.jdom2.Namespace; /** * ORE dissemination crosswalk diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/OREIngestionCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/OREIngestionCrosswalk.java index 80c424e78263..f756aae22577 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/OREIngestionCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/OREIngestionCrosswalk.java @@ -34,12 +34,13 @@ import org.dspace.content.service.ItemService; import org.dspace.core.Constants; import org.dspace.core.Context; -import org.jdom.Attribute; -import org.jdom.Document; -import org.jdom.Element; -import org.jdom.JDOMException; -import org.jdom.Namespace; -import org.jdom.xpath.XPath; +import org.jdom2.Attribute; +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.Namespace; +import org.jdom2.filter.Filters; +import org.jdom2.xpath.XPathExpression; +import org.jdom2.xpath.XPathFactory; /** * ORE ingestion crosswalk @@ -113,23 +114,21 @@ public void ingest(Context context, DSpaceObject dso, Element root, boolean crea Document doc = new Document(); doc.addContent(root.detach()); - XPath xpathLinks; List aggregatedResources; String entryId; - try { - xpathLinks = XPath.newInstance("/atom:entry/atom:link[@rel=\"" + ORE_NS.getURI() + "aggregates" + "\"]"); - xpathLinks.addNamespace(ATOM_NS); - aggregatedResources = xpathLinks.selectNodes(doc); - - xpathLinks = XPath.newInstance("/atom:entry/atom:link[@rel='alternate']/@href"); - xpathLinks.addNamespace(ATOM_NS); - entryId = ((Attribute) xpathLinks.selectSingleNode(doc)).getValue(); - } catch (JDOMException e) { - throw new CrosswalkException("JDOM exception occurred while ingesting the ORE", e); - } + XPathExpression xpathLinks = + XPathFactory.instance() + .compile("/atom:entry/atom:link[@rel=\"" + ORE_NS.getURI() + "aggregates" + "\"]", + Filters.element(), null, ATOM_NS); + aggregatedResources = xpathLinks.evaluate(doc); + + XPathExpression xpathAltHref = + XPathFactory.instance() + .compile("/atom:entry/atom:link[@rel='alternate']/@href", + Filters.attribute(), null, ATOM_NS); + entryId = xpathAltHref.evaluateFirst(doc).getValue(); // Next for each resource, create a bitstream - XPath xpathDesc; NumberFormat nf = NumberFormat.getInstance(); nf.setGroupingUsed(false); nf.setMinimumIntegerDigits(4); @@ -140,16 +139,12 @@ public void ingest(Context context, DSpaceObject dso, Element root, boolean crea String bundleName; Element desc = null; - try { - xpathDesc = XPath.newInstance( - "/atom:entry/oreatom:triples/rdf:Description[@rdf:about=\"" + this.encodeForURL(href) + "\"][1]"); - xpathDesc.addNamespace(ATOM_NS); - xpathDesc.addNamespace(ORE_ATOM); - xpathDesc.addNamespace(RDF_NS); - desc = (Element) xpathDesc.selectSingleNode(doc); - } catch (JDOMException e) { - log.warn("Could not find description for {}", href, e); - } + XPathExpression xpathDesc = + XPathFactory.instance() + .compile("/atom:entry/oreatom:triples/rdf:Description[@rdf:about=\"" + + this.encodeForURL(href) + "\"][1]", + Filters.element(), null, ATOM_NS, ORE_ATOM, RDF_NS); + desc = xpathDesc.evaluateFirst(doc); if (desc != null && desc.getChild("type", RDF_NS).getAttributeValue("resource", RDF_NS) .equals(DS_NS.getURI() + "DSpaceBitstream")) { diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/PREMISCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/PREMISCrosswalk.java index e4e387a3ec31..39b6c8f29c80 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/PREMISCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/PREMISCrosswalk.java @@ -30,8 +30,8 @@ import org.dspace.core.Context; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; -import org.jdom.Element; -import org.jdom.Namespace; +import org.jdom2.Element; +import org.jdom2.Namespace; /** * PREMIS Crosswalk diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/ParameterizedDisseminationCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/ParameterizedDisseminationCrosswalk.java index 312aed35434b..5d9322339d0e 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/ParameterizedDisseminationCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/ParameterizedDisseminationCrosswalk.java @@ -14,7 +14,7 @@ import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; import org.dspace.core.Context; -import org.jdom.Element; +import org.jdom2.Element; /** * Translate DSpace native metadata into an external XML format, with parameters. diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/QDCCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/QDCCrosswalk.java index f3c51a5d4625..2fdbaaad003e 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/QDCCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/QDCCrosswalk.java @@ -36,10 +36,10 @@ import org.dspace.core.SelfNamedPlugin; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; -import org.jdom.Document; -import org.jdom.Element; -import org.jdom.Namespace; -import org.jdom.input.SAXBuilder; +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.Namespace; +import org.jdom2.input.SAXBuilder; /** * Configurable QDC Crosswalk @@ -290,7 +290,7 @@ private void init() qdc2element.put(qdc, element); element2qdc.put(makeQualifiedTagName(element), qdc); log.debug("Building Maps: qdc=\"" + qdc + "\", element=\"" + element.toString() + "\""); - } catch (org.jdom.JDOMException je) { + } catch (org.jdom2.JDOMException je) { throw new CrosswalkInternalException( "Failed parsing XML fragment in properties file: \"" + prolog + val + postlog + "\": " + je .toString(), je); @@ -326,6 +326,7 @@ public String getSchemaLocation() { * @throws IOException if IO error * @throws SQLException if database error * @throws AuthorizeException if authorization error + * @return List of Elements */ @Override public List disseminateList(Context context, DSpaceObject dso) diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/RoleCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/RoleCrosswalk.java index d36ff3edf5af..2c763036ce33 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/RoleCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/RoleCrosswalk.java @@ -26,12 +26,12 @@ import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; import org.dspace.workflow.WorkflowException; -import org.jdom.Document; -import org.jdom.Element; -import org.jdom.JDOMException; -import org.jdom.Namespace; -import org.jdom.input.SAXBuilder; -import org.jdom.output.XMLOutputter; +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.JDOMException; +import org.jdom2.Namespace; +import org.jdom2.input.SAXBuilder; +import org.jdom2.output.XMLOutputter; /** * Role Crosswalk diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/SimpleDCDisseminationCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/SimpleDCDisseminationCrosswalk.java index 22ec68070aed..2f91c3aa0712 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/SimpleDCDisseminationCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/SimpleDCDisseminationCrosswalk.java @@ -24,8 +24,8 @@ import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.core.SelfNamedPlugin; -import org.jdom.Element; -import org.jdom.Namespace; +import org.jdom2.Element; +import org.jdom2.Namespace; /** * Disseminator for Simple Dublin Core metadata in XML format. @@ -84,6 +84,7 @@ public Element disseminateElement(Context context, DSpaceObject dso) * @throws IOException if IO error * @throws SQLException if database error * @throws AuthorizeException if authorization error + * @return List of Elements */ @Override public List disseminateList(Context context, DSpaceObject dso) diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/XHTMLHeadDisseminationCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/XHTMLHeadDisseminationCrosswalk.java index d03d2dd8876d..2fbbdd9756e7 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/XHTMLHeadDisseminationCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/XHTMLHeadDisseminationCrosswalk.java @@ -34,9 +34,9 @@ import org.dspace.core.SelfNamedPlugin; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; -import org.jdom.Element; -import org.jdom.Namespace; -import org.jdom.Verifier; +import org.jdom2.Element; +import org.jdom2.Namespace; +import org.jdom2.Verifier; /** * Crosswalk for creating appropriate <meta> elements to appear in the @@ -178,6 +178,7 @@ public Element disseminateElement(Context context, DSpaceObject dso) * @throws IOException if IO error * @throws SQLException if database error * @throws AuthorizeException if authorization error + * @return List of Elements */ @Override public List disseminateList(Context context, DSpaceObject dso) throws CrosswalkException, diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/XSLTCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/XSLTCrosswalk.java index 1c85fd82c51e..ba3d5717d6ef 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/XSLTCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/XSLTCrosswalk.java @@ -21,7 +21,7 @@ import org.dspace.core.SelfNamedPlugin; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; -import org.jdom.Namespace; +import org.jdom2.Namespace; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/XSLTDisseminationCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/XSLTDisseminationCrosswalk.java index 6c30c1b1a4db..26371b46aab0 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/XSLTDisseminationCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/XSLTDisseminationCrosswalk.java @@ -18,6 +18,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; @@ -41,14 +42,15 @@ import org.dspace.handle.factory.HandleServiceFactory; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; -import org.jdom.Document; -import org.jdom.Element; -import org.jdom.Namespace; -import org.jdom.Verifier; -import org.jdom.output.Format; -import org.jdom.output.XMLOutputter; -import org.jdom.transform.JDOMResult; -import org.jdom.transform.JDOMSource; +import org.jdom2.Content; +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.Namespace; +import org.jdom2.Verifier; +import org.jdom2.output.Format; +import org.jdom2.output.XMLOutputter; +import org.jdom2.transform.JDOMResult; +import org.jdom2.transform.JDOMSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -244,6 +246,7 @@ public Element disseminateElement(Context context, DSpaceObject dso, * @throws SQLException if database error * @throws AuthorizeException if authorization error * @see DisseminationCrosswalk + * @return List of Elements */ @Override public List disseminateList(Context context, DSpaceObject dso) @@ -268,7 +271,12 @@ public List disseminateList(Context context, DSpaceObject dso) try { JDOMResult result = new JDOMResult(); xform.transform(new JDOMSource(createDIM(dso).getChildren()), result); - return result.getResult(); + List contentList = result.getResult(); + // Transform List into List + List elementList = contentList.stream() + .filter(obj -> obj instanceof Element) + .map(Element.class::cast).collect(Collectors.toList()); + return elementList; } catch (TransformerException e) { LOG.error("Got error: " + e.toString()); throw new CrosswalkInternalException("XSL translation failed: " + e.toString(), e); diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/XSLTIngestionCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/XSLTIngestionCrosswalk.java index 37a822374d92..63ef5f7336c7 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/XSLTIngestionCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/XSLTIngestionCrosswalk.java @@ -12,6 +12,7 @@ import java.sql.SQLException; import java.util.Iterator; import java.util.List; +import java.util.stream.Collectors; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; @@ -34,13 +35,14 @@ import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.core.factory.CoreServiceFactory; -import org.jdom.Document; -import org.jdom.Element; -import org.jdom.input.SAXBuilder; -import org.jdom.output.Format; -import org.jdom.output.XMLOutputter; -import org.jdom.transform.JDOMResult; -import org.jdom.transform.JDOMSource; +import org.jdom2.Content; +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.input.SAXBuilder; +import org.jdom2.output.Format; +import org.jdom2.output.XMLOutputter; +import org.jdom2.transform.JDOMResult; +import org.jdom2.transform.JDOMSource; /** * Configurable XSLT-driven ingestion Crosswalk @@ -141,7 +143,12 @@ public void ingest(Context context, DSpaceObject dso, List metadata, try { JDOMResult result = new JDOMResult(); xform.transform(new JDOMSource(metadata), result); - ingestDIM(context, dso, result.getResult(), createMissingMetadataFields); + List contentList = result.getResult(); + // Transform List into List + List elementList = contentList.stream() + .filter(obj -> obj instanceof Element) + .map(Element.class::cast).collect(Collectors.toList()); + ingestDIM(context, dso, elementList, createMissingMetadataFields); } catch (TransformerException e) { log.error("Got error: " + e.toString()); throw new CrosswalkInternalException("XSL Transformation failed: " + e.toString(), e); diff --git a/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSDisseminator.java b/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSDisseminator.java index 471b9ba27cab..03afb5e852f2 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSDisseminator.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSDisseminator.java @@ -83,10 +83,10 @@ import org.dspace.license.service.CreativeCommonsService; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; -import org.jdom.Element; -import org.jdom.Namespace; -import org.jdom.output.Format; -import org.jdom.output.XMLOutputter; +import org.jdom2.Element; +import org.jdom2.Namespace; +import org.jdom2.output.Format; +import org.jdom2.output.XMLOutputter; /** * Base class for disseminator of diff --git a/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java b/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java index 9a7fffdec5ad..98277c4f9c06 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java @@ -51,7 +51,7 @@ import org.dspace.services.factory.DSpaceServicesFactory; import org.dspace.workflow.WorkflowException; import org.dspace.workflow.factory.WorkflowServiceFactory; -import org.jdom.Element; +import org.jdom2.Element; /** * Base class for package ingester of METS (Metadata Encoding and Transmission diff --git a/dspace-api/src/main/java/org/dspace/content/packager/DSpaceAIPIngester.java b/dspace-api/src/main/java/org/dspace/content/packager/DSpaceAIPIngester.java index 954a68bfc166..e7be7ab51190 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/DSpaceAIPIngester.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/DSpaceAIPIngester.java @@ -20,7 +20,7 @@ import org.dspace.content.crosswalk.MetadataValidationException; import org.dspace.core.Constants; import org.dspace.core.Context; -import org.jdom.Element; +import org.jdom2.Element; /** * Subclass of the METS packager framework to ingest a DSpace diff --git a/dspace-api/src/main/java/org/dspace/content/packager/DSpaceMETSIngester.java b/dspace-api/src/main/java/org/dspace/content/packager/DSpaceMETSIngester.java index da3965534f0b..380764268c2c 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/DSpaceMETSIngester.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/DSpaceMETSIngester.java @@ -23,7 +23,7 @@ import org.dspace.core.Context; import org.dspace.core.factory.CoreServiceFactory; import org.dspace.core.service.PluginService; -import org.jdom.Element; +import org.jdom2.Element; /** * Packager plugin to ingest a diff --git a/dspace-api/src/main/java/org/dspace/content/packager/METSManifest.java b/dspace-api/src/main/java/org/dspace/content/packager/METSManifest.java index 8fb8172aeb81..3399bdf0f07e 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/METSManifest.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/METSManifest.java @@ -35,15 +35,17 @@ import org.dspace.core.factory.CoreServiceFactory; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; -import org.jdom.Content; -import org.jdom.Document; -import org.jdom.Element; -import org.jdom.JDOMException; -import org.jdom.Namespace; -import org.jdom.input.SAXBuilder; -import org.jdom.output.Format; -import org.jdom.output.XMLOutputter; -import org.jdom.xpath.XPath; +import org.jdom2.Content; +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.JDOMException; +import org.jdom2.Namespace; +import org.jdom2.filter.Filters; +import org.jdom2.input.SAXBuilder; +import org.jdom2.output.Format; +import org.jdom2.output.XMLOutputter; +import org.jdom2.xpath.XPathExpression; +import org.jdom2.xpath.XPathFactory; /** *

@@ -382,15 +384,12 @@ public List getContentFiles() public List getMdFiles() throws MetadataValidationException { if (mdFiles == null) { - try { - // Use a special namespace with known prefix - // so we get the right prefix. - XPath xpath = XPath.newInstance("descendant::mets:mdRef"); - xpath.addNamespace(metsNS); - mdFiles = xpath.selectNodes(mets); - } catch (JDOMException je) { - throw new MetadataValidationException("Failed while searching for mdRef elements in manifest: ", je); - } + // Use a special namespace with known prefix + // so we get the right prefix. + XPathExpression xpath = + XPathFactory.instance() + .compile("descendant::mets:mdRef", Filters.element(), null, metsNS); + mdFiles = xpath.evaluate(mets); } return mdFiles; } @@ -414,25 +413,22 @@ public String getOriginalFilePath(Element file) { return null; } - try { - XPath xpath = XPath.newInstance( - "mets:fileSec/mets:fileGrp[@USE=\"CONTENT\"]/mets:file[@GROUPID=\"" + groupID + "\"]"); - xpath.addNamespace(metsNS); - List oFiles = xpath.selectNodes(mets); - if (oFiles.size() > 0) { - if (log.isDebugEnabled()) { - log.debug("Got ORIGINAL file for derived=" + file.toString()); - } - Element flocat = ((Element) oFiles.get(0)).getChild("FLocat", metsNS); - if (flocat != null) { - return flocat.getAttributeValue("href", xlinkNS); - } + XPathExpression xpath = + XPathFactory.instance() + .compile( + "mets:fileSec/mets:fileGrp[@USE=\"CONTENT\"]/mets:file[@GROUPID=\"" + groupID + "\"]", + Filters.element(), null, metsNS); + List oFiles = xpath.evaluate(mets); + if (oFiles.size() > 0) { + if (log.isDebugEnabled()) { + log.debug("Got ORIGINAL file for derived=" + file.toString()); + } + Element flocat = oFiles.get(0).getChild("FLocat", metsNS); + if (flocat != null) { + return flocat.getAttributeValue("href", xlinkNS); } - return null; - } catch (JDOMException je) { - log.warn("Got exception on XPATH looking for Original file, " + je.toString()); - return null; } + return null; } // translate bundle name from METS to DSpace; METS may be "CONTENT" @@ -888,20 +884,16 @@ public String getParentOwnerLink() // use only when path varies each time you call it. protected Element getElementByXPath(String path, boolean nullOk) throws MetadataValidationException { - try { - XPath xpath = XPath.newInstance(path); - xpath.addNamespace(metsNS); - xpath.addNamespace(xlinkNS); - Object result = xpath.selectSingleNode(mets); - if (result == null && nullOk) { - return null; - } else if (result instanceof Element) { - return (Element) result; - } else { - throw new MetadataValidationException("METSManifest: Failed to resolve XPath, path=\"" + path + "\""); - } - } catch (JDOMException je) { - throw new MetadataValidationException("METSManifest: Failed to resolve XPath, path=\"" + path + "\"", je); + XPathExpression xpath = + XPathFactory.instance() + .compile(path, Filters.element(), null, metsNS, xlinkNS); + Element result = xpath.evaluateFirst(mets); + if (result == null && nullOk) { + return null; + } else if (result == null && !nullOk) { + throw new MetadataValidationException("METSManifest: Failed to resolve XPath, path=\"" + path + "\""); + } else { + return result; } } diff --git a/dspace-api/src/main/java/org/dspace/content/packager/RoleDisseminator.java b/dspace-api/src/main/java/org/dspace/content/packager/RoleDisseminator.java index 8643f60f6ce0..f627779af8dc 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/RoleDisseminator.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/RoleDisseminator.java @@ -35,7 +35,7 @@ import org.dspace.eperson.factory.EPersonServiceFactory; import org.dspace.eperson.service.EPersonService; import org.dspace.eperson.service.GroupService; -import org.jdom.Namespace; +import org.jdom2.Namespace; /** * Plugin to export all Group and EPerson objects in XML, perhaps for reloading. diff --git a/dspace-api/src/main/java/org/dspace/harvest/HarvestedCollectionServiceImpl.java b/dspace-api/src/main/java/org/dspace/harvest/HarvestedCollectionServiceImpl.java index 88cec74a5816..d4bb37cf944f 100644 --- a/dspace-api/src/main/java/org/dspace/harvest/HarvestedCollectionServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/harvest/HarvestedCollectionServiceImpl.java @@ -29,10 +29,10 @@ import org.dspace.harvest.service.HarvestedCollectionService; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; -import org.jdom.Document; -import org.jdom.Element; -import org.jdom.Namespace; -import org.jdom.input.DOMBuilder; +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.Namespace; +import org.jdom2.input.DOMBuilder; import org.springframework.beans.factory.annotation.Autowired; import org.w3c.dom.DOMException; import org.xml.sax.SAXException; diff --git a/dspace-api/src/main/java/org/dspace/harvest/OAIHarvester.java b/dspace-api/src/main/java/org/dspace/harvest/OAIHarvester.java index 71e00d73d701..a15ed53a93e9 100644 --- a/dspace-api/src/main/java/org/dspace/harvest/OAIHarvester.java +++ b/dspace-api/src/main/java/org/dspace/harvest/OAIHarvester.java @@ -70,11 +70,11 @@ import org.dspace.harvest.service.HarvestedItemService; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; -import org.jdom.Document; -import org.jdom.Element; -import org.jdom.Namespace; -import org.jdom.input.DOMBuilder; -import org.jdom.output.XMLOutputter; +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.Namespace; +import org.jdom2.input.DOMBuilder; +import org.jdom2.output.XMLOutputter; import org.xml.sax.SAXException; diff --git a/dspace-api/src/main/java/org/dspace/identifier/DataCiteXMLCreator.java b/dspace-api/src/main/java/org/dspace/identifier/DataCiteXMLCreator.java index 0ea25ff3a48a..ae2cd248d417 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/DataCiteXMLCreator.java +++ b/dspace-api/src/main/java/org/dspace/identifier/DataCiteXMLCreator.java @@ -23,8 +23,8 @@ import org.dspace.handle.factory.HandleServiceFactory; import org.dspace.services.ConfigurationService; import org.dspace.utils.DSpace; -import org.jdom.Element; -import org.jdom.output.XMLOutputter; +import org.jdom2.Element; +import org.jdom2.output.XMLOutputter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/dspace-api/src/main/java/org/dspace/identifier/doi/DataCiteConnector.java b/dspace-api/src/main/java/org/dspace/identifier/doi/DataCiteConnector.java index bc8ea90957e5..57136d6143bb 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/doi/DataCiteConnector.java +++ b/dspace-api/src/main/java/org/dspace/identifier/doi/DataCiteConnector.java @@ -45,13 +45,13 @@ import org.dspace.handle.service.HandleService; import org.dspace.identifier.DOI; import org.dspace.services.ConfigurationService; -import org.jdom.Document; -import org.jdom.Element; -import org.jdom.JDOMException; -import org.jdom.filter.ElementFilter; -import org.jdom.input.SAXBuilder; -import org.jdom.output.Format; -import org.jdom.output.XMLOutputter; +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.JDOMException; +import org.jdom2.filter.ElementFilter; +import org.jdom2.input.SAXBuilder; +import org.jdom2.output.Format; +import org.jdom2.output.XMLOutputter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; diff --git a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorService.java b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorService.java index 0c061d2d6428..64450b796c17 100644 --- a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorService.java +++ b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorService.java @@ -10,7 +10,7 @@ import java.io.IOException; import java.util.Map; -import org.jdom.Document; +import org.jdom2.Document; /** * Service interface class for the Creative commons license connector service. diff --git a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java index 792c25d62929..e0039169bad6 100644 --- a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java @@ -34,11 +34,11 @@ import org.dspace.services.ConfigurationService; import org.jaxen.JaxenException; import org.jaxen.jdom.JDOMXPath; -import org.jdom.Attribute; -import org.jdom.Document; -import org.jdom.Element; -import org.jdom.JDOMException; -import org.jdom.input.SAXBuilder; +import org.jdom2.Attribute; +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.JDOMException; +import org.jdom2.input.SAXBuilder; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.xml.sax.InputSource; @@ -141,7 +141,7 @@ private List retrieveLicenses(CloseableHttpResponse response) try (StringReader stringReader = new StringReader(responseString)) { InputSource is = new InputSource(stringReader); - org.jdom.Document classDoc = this.parser.build(is); + org.jdom2.Document classDoc = this.parser.build(is); List elements = licenseClassXpath.selectNodes(classDoc); for (Element element : elements) { @@ -179,7 +179,7 @@ private CCLicense retrieveLicenseObject(final String licenseId, CloseableHttpRes try (StringReader stringReader = new StringReader(responseString)) { InputSource is = new InputSource(stringReader); - org.jdom.Document classDoc = this.parser.build(is); + org.jdom2.Document classDoc = this.parser.build(is); Object element = licenseClassXpath.selectSingleNode(classDoc); String licenseLabel = getSingleNodeValue(element, "label"); @@ -298,7 +298,7 @@ private String retrieveLicenseUri(final CloseableHttpResponse response) try (StringReader stringReader = new StringReader(responseString)) { InputSource is = new InputSource(stringReader); - org.jdom.Document classDoc = this.parser.build(is); + org.jdom2.Document classDoc = this.parser.build(is); Object node = licenseClassXpath.selectSingleNode(classDoc); String nodeValue = getNodeValue(node); diff --git a/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java b/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java index ccc660b63b8a..96f110c10196 100644 --- a/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java @@ -40,8 +40,8 @@ import org.dspace.license.service.CreativeCommonsService; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; -import org.jdom.Document; -import org.jdom.transform.JDOMSource; +import org.jdom2.Document; +import org.jdom2.transform.JDOMSource; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; diff --git a/dspace-api/src/main/java/org/dspace/license/service/CreativeCommonsService.java b/dspace-api/src/main/java/org/dspace/license/service/CreativeCommonsService.java index 1f5f1ddd029a..0f4911aa3ec1 100644 --- a/dspace-api/src/main/java/org/dspace/license/service/CreativeCommonsService.java +++ b/dspace-api/src/main/java/org/dspace/license/service/CreativeCommonsService.java @@ -18,7 +18,7 @@ import org.dspace.content.Item; import org.dspace.core.Context; import org.dspace.license.CCLicense; -import org.jdom.Document; +import org.jdom2.Document; /** * Service interface class for the Creative commons licensing. diff --git a/dspace-api/src/main/java/org/dspace/testing/PubMedToImport.java b/dspace-api/src/main/java/org/dspace/testing/PubMedToImport.java index b7ded5ecbfc4..ec51528429a4 100644 --- a/dspace-api/src/main/java/org/dspace/testing/PubMedToImport.java +++ b/dspace-api/src/main/java/org/dspace/testing/PubMedToImport.java @@ -24,10 +24,10 @@ import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.jdom.Document; -import org.jdom.Element; -import org.jdom.output.Format; -import org.jdom.output.XMLOutputter; +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.output.Format; +import org.jdom2.output.XMLOutputter; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; diff --git a/dspace-api/src/test/java/org/dspace/AbstractIntegrationTestWithDatabase.java b/dspace-api/src/test/java/org/dspace/AbstractIntegrationTestWithDatabase.java index 402947b9664b..cada4944e3b9 100644 --- a/dspace-api/src/test/java/org/dspace/AbstractIntegrationTestWithDatabase.java +++ b/dspace-api/src/test/java/org/dspace/AbstractIntegrationTestWithDatabase.java @@ -32,7 +32,7 @@ import org.dspace.statistics.MockSolrLoggerServiceImpl; import org.dspace.statistics.MockSolrStatisticsCore; import org.dspace.storage.rdbms.DatabaseUtils; -import org.jdom.Document; +import org.jdom2.Document; import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; diff --git a/dspace-api/src/test/java/org/dspace/app/packager/PackagerIT.java b/dspace-api/src/test/java/org/dspace/app/packager/PackagerIT.java index c814d2d9f6eb..7d808ab8715c 100644 --- a/dspace-api/src/test/java/org/dspace/app/packager/PackagerIT.java +++ b/dspace-api/src/test/java/org/dspace/app/packager/PackagerIT.java @@ -38,7 +38,7 @@ import org.dspace.content.service.WorkspaceItemService; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; -import org.jdom.Element; +import org.jdom2.Element; import org.junit.After; import org.junit.Before; import org.junit.Test; diff --git a/dspace-api/src/test/java/org/dspace/content/crosswalk/QDCCrosswalkTest.java b/dspace-api/src/test/java/org/dspace/content/crosswalk/QDCCrosswalkTest.java index efed8ad8dc24..2eafc03986a7 100644 --- a/dspace-api/src/test/java/org/dspace/content/crosswalk/QDCCrosswalkTest.java +++ b/dspace-api/src/test/java/org/dspace/content/crosswalk/QDCCrosswalkTest.java @@ -14,7 +14,7 @@ import org.dspace.core.service.PluginService; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; -import org.jdom.Namespace; +import org.jdom2.Namespace; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; diff --git a/dspace-api/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java b/dspace-api/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java index 8545c4187d16..30a5a3a9b51d 100644 --- a/dspace-api/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java +++ b/dspace-api/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java @@ -15,8 +15,8 @@ import java.util.Map; import org.apache.commons.lang3.StringUtils; -import org.jdom.Document; -import org.jdom.JDOMException; +import org.jdom2.Document; +import org.jdom2.JDOMException; /** * Mock implementation for the Creative commons license connector service. diff --git a/dspace-server-webapp/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java b/dspace-server-webapp/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java index bb443ab4a41d..d70499d096b8 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java +++ b/dspace-server-webapp/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java @@ -15,8 +15,8 @@ import java.util.Map; import org.apache.commons.lang3.StringUtils; -import org.jdom.Document; -import org.jdom.JDOMException; +import org.jdom2.Document; +import org.jdom2.JDOMException; /** * Mock implementation for the Creative commons license connector service. diff --git a/pom.xml b/pom.xml index 27f6597caa8d..4d140a33ddbf 100644 --- a/pom.xml +++ b/pom.xml @@ -1481,8 +1481,8 @@ org.jdom - jdom - 1.1.3 + jdom2 + 2.0.6.1 From d6df493080fbc33d2061468a3c2b7944d59ab5e7 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 16 Mar 2022 17:19:08 -0500 Subject: [PATCH 0059/1846] Fix broken tests by replacing JDOMXPath with XPathFactory --- .../CCLicenseConnectorServiceImpl.java | 68 +++++++++---------- pom.xml | 1 + 2 files changed, 32 insertions(+), 37 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java index e0039169bad6..619227432d7d 100644 --- a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java @@ -32,13 +32,14 @@ import org.apache.http.util.EntityUtils; import org.apache.logging.log4j.Logger; import org.dspace.services.ConfigurationService; -import org.jaxen.JaxenException; -import org.jaxen.jdom.JDOMXPath; import org.jdom2.Attribute; import org.jdom2.Document; import org.jdom2.Element; import org.jdom2.JDOMException; +import org.jdom2.filter.Filters; import org.jdom2.input.SAXBuilder; +import org.jdom2.xpath.XPathExpression; +import org.jdom2.xpath.XPathFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.xml.sax.InputSource; @@ -96,7 +97,7 @@ public Map retrieveLicenses(String language) { List licenses; try (CloseableHttpResponse response = client.execute(httpGet)) { licenses = retrieveLicenses(response); - } catch (JDOMException | JaxenException | IOException e) { + } catch (JDOMException | IOException e) { log.error("Error while retrieving the license details using url: " + uri, e); licenses = Collections.emptyList(); } @@ -110,7 +111,7 @@ public Map retrieveLicenses(String language) { try (CloseableHttpResponse response = client.execute(licenseHttpGet)) { CCLicense ccLicense = retrieveLicenseObject(license, response); ccLicenses.put(ccLicense.getLicenseId(), ccLicense); - } catch (JaxenException | JDOMException | IOException e) { + } catch (JDOMException | IOException e) { log.error("Error while retrieving the license details using url: " + licenseUri, e); } } @@ -125,25 +126,23 @@ public Map retrieveLicenses(String language) { * @param response The response from the API * @return a list of license identifiers for which details need to be retrieved * @throws IOException - * @throws JaxenException * @throws JDOMException */ private List retrieveLicenses(CloseableHttpResponse response) - throws IOException, JaxenException, JDOMException { + throws IOException, JDOMException { List domains = new LinkedList<>(); String[] excludedLicenses = configurationService.getArrayProperty("cc.license.classfilter"); - String responseString = EntityUtils.toString(response.getEntity()); - JDOMXPath licenseClassXpath = new JDOMXPath("//licenses/license"); - + XPathExpression licenseClassXpath = + XPathFactory.instance().compile("//licenses/license", Filters.element()); try (StringReader stringReader = new StringReader(responseString)) { InputSource is = new InputSource(stringReader); org.jdom2.Document classDoc = this.parser.build(is); - List elements = licenseClassXpath.selectNodes(classDoc); + List elements = licenseClassXpath.evaluate(classDoc); for (Element element : elements) { String licenseId = getSingleNodeValue(element, "@id"); if (StringUtils.isNotBlank(licenseId) && !ArrayUtils.contains(excludedLicenses, licenseId)) { @@ -163,30 +162,29 @@ private List retrieveLicenses(CloseableHttpResponse response) * @param response for a specific CC License response * @return the corresponding CC License Object * @throws IOException - * @throws JaxenException * @throws JDOMException */ private CCLicense retrieveLicenseObject(final String licenseId, CloseableHttpResponse response) - throws IOException, JaxenException, JDOMException { + throws IOException, JDOMException { String responseString = EntityUtils.toString(response.getEntity()); - - JDOMXPath licenseClassXpath = new JDOMXPath("//licenseclass"); - JDOMXPath licenseFieldXpath = new JDOMXPath("field"); - + XPathExpression licenseClassXpath = + XPathFactory.instance().compile("//licenseclass", Filters.fpassthrough()); + XPathExpression licenseFieldXpath = + XPathFactory.instance().compile("field", Filters.element()); try (StringReader stringReader = new StringReader(responseString)) { InputSource is = new InputSource(stringReader); org.jdom2.Document classDoc = this.parser.build(is); - Object element = licenseClassXpath.selectSingleNode(classDoc); + Object element = licenseClassXpath.evaluateFirst(classDoc); String licenseLabel = getSingleNodeValue(element, "label"); List ccLicenseFields = new LinkedList<>(); - List licenseFields = licenseFieldXpath.selectNodes(element); + List licenseFields = licenseFieldXpath.evaluate(element); for (Element licenseField : licenseFields) { CCLicenseField ccLicenseField = parseLicenseField(licenseField); ccLicenseFields.add(ccLicenseField); @@ -196,13 +194,14 @@ private CCLicense retrieveLicenseObject(final String licenseId, CloseableHttpRes } } - private CCLicenseField parseLicenseField(final Element licenseField) throws JaxenException { + private CCLicenseField parseLicenseField(final Element licenseField) { String id = getSingleNodeValue(licenseField, "@id"); String label = getSingleNodeValue(licenseField, "label"); String description = getSingleNodeValue(licenseField, "description"); - JDOMXPath enumXpath = new JDOMXPath("enum"); - List enums = enumXpath.selectNodes(licenseField); + XPathExpression enumXpath = + XPathFactory.instance().compile("enum", Filters.element()); + List enums = enumXpath.evaluate(licenseField); List ccLicenseFieldEnumList = new LinkedList<>(); @@ -215,7 +214,7 @@ private CCLicenseField parseLicenseField(final Element licenseField) throws Jaxe } - private CCLicenseFieldEnum parseEnum(final Element enumElement) throws JaxenException { + private CCLicenseFieldEnum parseEnum(final Element enumElement) { String id = getSingleNodeValue(enumElement, "@id"); String label = getSingleNodeValue(enumElement, "label"); String description = getSingleNodeValue(enumElement, "description"); @@ -236,9 +235,10 @@ private String getNodeValue(final Object el) { } } - private String getSingleNodeValue(final Object t, String query) throws JaxenException { - JDOMXPath xpath = new JDOMXPath(query); - Object singleNode = xpath.selectSingleNode(t); + private String getSingleNodeValue(final Object t, String query) { + XPathExpression xpath = + XPathFactory.instance().compile(query, Filters.fpassthrough()); + Object singleNode = xpath.evaluateFirst(t); return getNodeValue(singleNode); } @@ -273,7 +273,7 @@ public String retrieveRightsByQuestion(String licenseId, try (CloseableHttpResponse response = client.execute(httpPost)) { return retrieveLicenseUri(response); - } catch (JDOMException | JaxenException | IOException e) { + } catch (JDOMException | IOException e) { log.error("Error while retrieving the license uri for license : " + licenseId + " with answers " + answerMap.toString(), e); } @@ -286,21 +286,20 @@ public String retrieveRightsByQuestion(String licenseId, * @param response for a specific CC License URI response * @return the corresponding CC License URI as a string * @throws IOException - * @throws JaxenException * @throws JDOMException */ private String retrieveLicenseUri(final CloseableHttpResponse response) - throws IOException, JaxenException, JDOMException { + throws IOException, JDOMException { String responseString = EntityUtils.toString(response.getEntity()); - JDOMXPath licenseClassXpath = new JDOMXPath("//result/license-uri"); - + XPathExpression licenseClassXpath = + XPathFactory.instance().compile("//result/license-uri", Filters.fpassthrough()); try (StringReader stringReader = new StringReader(responseString)) { InputSource is = new InputSource(stringReader); org.jdom2.Document classDoc = this.parser.build(is); - Object node = licenseClassXpath.selectSingleNode(classDoc); + Object node = licenseClassXpath.evaluateFirst(classDoc); String nodeValue = getNodeValue(node); if (StringUtils.isNotBlank(nodeValue)) { @@ -364,12 +363,7 @@ public Document retrieveLicenseRDFDoc(String licenseURI) throws IOException { * @return the license name */ public String retrieveLicenseName(final Document doc) { - try { - return getSingleNodeValue(doc, "//result/license-name"); - } catch (JaxenException e) { - log.error("Error while retrieving the license name from the license document", e); - } - return null; + return getSingleNodeValue(doc, "//result/license-name"); } } diff --git a/pom.xml b/pom.xml index 4d140a33ddbf..55cffa40835e 100644 --- a/pom.xml +++ b/pom.xml @@ -1468,6 +1468,7 @@ 3.1.0 + jaxen jaxen From 27fdab0dfb8543a5c533006efa2276915cde16d8 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Thu, 17 Mar 2022 15:53:30 -0500 Subject: [PATCH 0060/1846] Upgrade Rome to avoid pulling in old version of JDOM --- dspace-api/pom.xml | 7 +++- .../app/util/OpenSearchServiceImpl.java | 8 ++-- .../org/dspace/app/util/SyndicationFeed.java | 40 +++++++++---------- pom.xml | 18 +++++++++ 4 files changed, 47 insertions(+), 26 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 1ac1c3742ee9..d5759d415e65 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -581,9 +581,12 @@ - org.rometools + com.rometools + rome + + + com.rometools rome-modules - 1.0 org.jbibtex diff --git a/dspace-api/src/main/java/org/dspace/app/util/OpenSearchServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/util/OpenSearchServiceImpl.java index 474ee4c99cea..89ca477442f3 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/OpenSearchServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/util/OpenSearchServiceImpl.java @@ -16,10 +16,10 @@ import java.util.List; import java.util.Map; -import com.sun.syndication.feed.module.opensearch.OpenSearchModule; -import com.sun.syndication.feed.module.opensearch.entity.OSQuery; -import com.sun.syndication.feed.module.opensearch.impl.OpenSearchModuleImpl; -import com.sun.syndication.io.FeedException; +import com.rometools.modules.opensearch.OpenSearchModule; +import com.rometools.modules.opensearch.entity.OSQuery; +import com.rometools.modules.opensearch.impl.OpenSearchModuleImpl; +import com.rometools.rome.io.FeedException; import org.apache.logging.log4j.Logger; import org.dspace.app.util.service.OpenSearchService; import org.dspace.content.DSpaceObject; diff --git a/dspace-api/src/main/java/org/dspace/app/util/SyndicationFeed.java b/dspace-api/src/main/java/org/dspace/app/util/SyndicationFeed.java index 2576df0193be..8bbec234c90b 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/SyndicationFeed.java +++ b/dspace-api/src/main/java/org/dspace/app/util/SyndicationFeed.java @@ -15,26 +15,26 @@ import java.util.Map; import javax.servlet.http.HttpServletRequest; -import com.sun.syndication.feed.module.DCModule; -import com.sun.syndication.feed.module.DCModuleImpl; -import com.sun.syndication.feed.module.Module; -import com.sun.syndication.feed.module.itunes.EntryInformation; -import com.sun.syndication.feed.module.itunes.EntryInformationImpl; -import com.sun.syndication.feed.module.itunes.types.Duration; -import com.sun.syndication.feed.synd.SyndContent; -import com.sun.syndication.feed.synd.SyndContentImpl; -import com.sun.syndication.feed.synd.SyndEnclosure; -import com.sun.syndication.feed.synd.SyndEnclosureImpl; -import com.sun.syndication.feed.synd.SyndEntry; -import com.sun.syndication.feed.synd.SyndEntryImpl; -import com.sun.syndication.feed.synd.SyndFeed; -import com.sun.syndication.feed.synd.SyndFeedImpl; -import com.sun.syndication.feed.synd.SyndImage; -import com.sun.syndication.feed.synd.SyndImageImpl; -import com.sun.syndication.feed.synd.SyndPerson; -import com.sun.syndication.feed.synd.SyndPersonImpl; -import com.sun.syndication.io.FeedException; -import com.sun.syndication.io.SyndFeedOutput; +import com.rometools.modules.itunes.EntryInformation; +import com.rometools.modules.itunes.EntryInformationImpl; +import com.rometools.modules.itunes.types.Duration; +import com.rometools.rome.feed.module.DCModule; +import com.rometools.rome.feed.module.DCModuleImpl; +import com.rometools.rome.feed.module.Module; +import com.rometools.rome.feed.synd.SyndContent; +import com.rometools.rome.feed.synd.SyndContentImpl; +import com.rometools.rome.feed.synd.SyndEnclosure; +import com.rometools.rome.feed.synd.SyndEnclosureImpl; +import com.rometools.rome.feed.synd.SyndEntry; +import com.rometools.rome.feed.synd.SyndEntryImpl; +import com.rometools.rome.feed.synd.SyndFeed; +import com.rometools.rome.feed.synd.SyndFeedImpl; +import com.rometools.rome.feed.synd.SyndImage; +import com.rometools.rome.feed.synd.SyndImageImpl; +import com.rometools.rome.feed.synd.SyndPerson; +import com.rometools.rome.feed.synd.SyndPersonImpl; +import com.rometools.rome.io.FeedException; +import com.rometools.rome.io.SyndFeedOutput; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; diff --git a/pom.xml b/pom.xml index 55cffa40835e..4dfe21d9a604 100644 --- a/pom.xml +++ b/pom.xml @@ -41,6 +41,7 @@ 2.17.1 2.0.24 3.17 + 1.18.0 1.7.25 @@ -1161,6 +1162,23 @@ ${hibernate-validator.version} + + + com.rometools + rome + ${rome.version} + + + com.rometools + rome-modules + ${rome.version} + + + com.rometools + rome-utils + ${rome.version} + + org.springframework spring-orm From 8ed314f60870ab5f32c40f23c44f4f2b0aa0e2fb Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Tue, 22 Mar 2022 17:24:59 -0500 Subject: [PATCH 0061/1846] Remove Axiom from dspace-api, replacing it with JDOM2 --- dspace-api/pom.xml | 38 --------- .../ArXivIdMetadataContributor.java | 4 +- .../ArXivImportMetadataSourceServiceImpl.java | 69 +++++++++------- .../SimpleXpathMetadatumContributor.java | 78 ++++++++++--------- ...PubmedImportMetadataSourceServiceImpl.java | 67 +++++++++------- dspace-swordv2/pom.xml | 3 +- pom.xml | 3 +- 7 files changed, 125 insertions(+), 137 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index d5759d415e65..66ff4e69bcf9 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -818,44 +818,6 @@ jaxb-runtime - - - org.apache.ws.commons.axiom - axiom-impl - ${axiom.version} - - - - org.apache.geronimo.specs - * - - - - org.codehaus.woodstox - woodstox-core-asl - - - - - org.apache.ws.commons.axiom - axiom-api - ${axiom.version} - - - - org.apache.geronimo.specs - * - - - - org.codehaus.woodstox - woodstox-core-asl - - - - org.glassfish.jersey.core diff --git a/dspace-api/src/main/java/org/dspace/importer/external/arxiv/metadatamapping/contributor/ArXivIdMetadataContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/arxiv/metadatamapping/contributor/ArXivIdMetadataContributor.java index ed5ac5960b8b..7bd42cf07a4c 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/arxiv/metadatamapping/contributor/ArXivIdMetadataContributor.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/arxiv/metadatamapping/contributor/ArXivIdMetadataContributor.java @@ -9,10 +9,10 @@ import java.util.Collection; -import org.apache.axiom.om.OMElement; import org.dspace.importer.external.metadatamapping.MetadatumDTO; import org.dspace.importer.external.metadatamapping.contributor.MetadataContributor; import org.dspace.importer.external.metadatamapping.contributor.SimpleXpathMetadatumContributor; +import org.jdom2.Element; /** * Arxiv specific implementation of {@link MetadataContributor} @@ -32,7 +32,7 @@ public class ArXivIdMetadataContributor extends SimpleXpathMetadatumContributor * @return a collection of import records. Only the identifier of the found records may be put in the record. */ @Override - public Collection contributeMetadata(OMElement t) { + public Collection contributeMetadata(Element t) { Collection values = super.contributeMetadata(t); parseValue(values); return values; diff --git a/dspace-api/src/main/java/org/dspace/importer/external/arxiv/service/ArXivImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/arxiv/service/ArXivImportMetadataSourceServiceImpl.java index 6b418423fac6..96689e62ba75 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/arxiv/service/ArXivImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/arxiv/service/ArXivImportMetadataSourceServiceImpl.java @@ -7,8 +7,10 @@ */ package org.dspace.importer.external.arxiv.service; +import java.io.IOException; import java.io.StringReader; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.concurrent.Callable; @@ -20,10 +22,6 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import org.apache.axiom.om.OMElement; -import org.apache.axiom.om.OMXMLBuilderFactory; -import org.apache.axiom.om.OMXMLParserWrapper; -import org.apache.axiom.om.xpath.AXIOMXPath; import org.apache.commons.lang3.StringUtils; import org.dspace.content.Item; import org.dspace.importer.external.datamodel.ImportRecord; @@ -31,7 +29,14 @@ import org.dspace.importer.external.exception.MetadataSourceException; import org.dspace.importer.external.service.AbstractImportMetadataSourceService; import org.dspace.importer.external.service.components.QuerySource; -import org.jaxen.JaxenException; +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.JDOMException; +import org.jdom2.Namespace; +import org.jdom2.filter.Filters; +import org.jdom2.input.SAXBuilder; +import org.jdom2.xpath.XPathExpression; +import org.jdom2.xpath.XPathFactory; /** * Implements a data source for querying ArXiv @@ -39,7 +44,7 @@ * @author Pasquale Cavallo (pasquale.cavallo at 4Science dot it) * */ -public class ArXivImportMetadataSourceServiceImpl extends AbstractImportMetadataSourceService +public class ArXivImportMetadataSourceServiceImpl extends AbstractImportMetadataSourceService implements QuerySource { private WebTarget webTarget; @@ -213,15 +218,20 @@ public Integer call() throws Exception { Response response = invocationBuilder.get(); if (response.getStatus() == 200) { String responseString = response.readEntity(String.class); - OMXMLParserWrapper records = OMXMLBuilderFactory.createOMBuilder(new StringReader(responseString)); - OMElement element = records.getDocumentElement(); - AXIOMXPath xpath = null; + + SAXBuilder saxBuilder = new SAXBuilder(); + Document document = saxBuilder.build(new StringReader(responseString)); + Element root = document.getRootElement(); + + List namespaces = Arrays.asList(Namespace.getNamespace("opensearch", + "http://a9.com/-/spec/opensearch/1.1/")); + XPathExpression xpath = + XPathFactory.instance().compile("opensearch:totalResults", Filters.element(), null, namespaces); + + Element count = xpath.evaluateFirst(root); try { - xpath = new AXIOMXPath("opensearch:totalResults"); - xpath.addNamespace("opensearch", "http://a9.com/-/spec/opensearch/1.1/"); - OMElement count = (OMElement) xpath.selectSingleNode(element); return Integer.parseInt(count.getText()); - } catch (JaxenException e) { + } catch (NumberFormatException e) { return null; } } else { @@ -274,8 +284,8 @@ public List call() throws Exception { Response response = invocationBuilder.get(); if (response.getStatus() == 200) { String responseString = response.readEntity(String.class); - List omElements = splitToRecords(responseString); - for (OMElement record : omElements) { + List elements = splitToRecords(responseString); + for (Element record : elements) { results.add(transformSourceRecords(record)); } return results; @@ -321,8 +331,8 @@ public List call() throws Exception { Response response = invocationBuilder.get(); if (response.getStatus() == 200) { String responseString = response.readEntity(String.class); - List omElements = splitToRecords(responseString); - for (OMElement record : omElements) { + List elements = splitToRecords(responseString); + for (Element record : elements) { results.add(transformSourceRecords(record)); } return results; @@ -359,8 +369,8 @@ public List call() throws Exception { Response response = invocationBuilder.get(); if (response.getStatus() == 200) { String responseString = response.readEntity(String.class); - List omElements = splitToRecords(responseString); - for (OMElement record : omElements) { + List elements = splitToRecords(responseString); + for (Element record : elements) { results.add(transformSourceRecords(record)); } return results; @@ -387,16 +397,21 @@ private String getQuery(Query query) { } } - private List splitToRecords(String recordsSrc) { - OMXMLParserWrapper records = OMXMLBuilderFactory.createOMBuilder(new StringReader(recordsSrc)); - OMElement element = records.getDocumentElement(); - AXIOMXPath xpath = null; + private List splitToRecords(String recordsSrc) { + try { - xpath = new AXIOMXPath("ns:entry"); - xpath.addNamespace("ns", "http://www.w3.org/2005/Atom"); - List recordsList = xpath.selectNodes(element); + SAXBuilder saxBuilder = new SAXBuilder(); + Document document = saxBuilder.build(new StringReader(recordsSrc)); + Element root = document.getRootElement(); + + List namespaces = Arrays.asList(Namespace.getNamespace("ns", + "http://www.w3.org/2005/Atom")); + XPathExpression xpath = + XPathFactory.instance().compile("ns:entry", Filters.element(), null, namespaces); + + List recordsList = xpath.evaluate(root); return recordsList; - } catch (JaxenException e) { + } catch (JDOMException | IOException e) { return null; } } diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumContributor.java index 87cdbfa6ed04..65d6d6694758 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumContributor.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumContributor.java @@ -7,33 +7,36 @@ */ package org.dspace.importer.external.metadatamapping.contributor; +import java.util.ArrayList; import java.util.Collection; import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.annotation.Resource; -import org.apache.axiom.om.OMAttribute; -import org.apache.axiom.om.OMElement; -import org.apache.axiom.om.OMText; -import org.apache.axiom.om.xpath.AXIOMXPath; +import org.apache.logging.log4j.Logger; import org.dspace.importer.external.metadatamapping.MetadataFieldConfig; import org.dspace.importer.external.metadatamapping.MetadataFieldMapping; import org.dspace.importer.external.metadatamapping.MetadatumDTO; -import org.jaxen.JaxenException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.jdom2.Attribute; +import org.jdom2.Element; +import org.jdom2.Namespace; +import org.jdom2.Text; +import org.jdom2.filter.Filters; +import org.jdom2.xpath.XPathExpression; +import org.jdom2.xpath.XPathFactory; import org.springframework.beans.factory.annotation.Autowired; /** - * Metadata contributor that takes an axiom OMElement and turns it into a metadatum + * Metadata contributor that takes a JDOM Element and turns it into a metadatum * * @author Roeland Dillen (roeland at atmire dot com) */ -public class SimpleXpathMetadatumContributor implements MetadataContributor { +public class SimpleXpathMetadatumContributor implements MetadataContributor { private MetadataFieldConfig field; - private static final Logger log = LoggerFactory.getLogger(SimpleXpathMetadatumContributor.class); + private static final Logger log + = org.apache.logging.log4j.LogManager.getLogger(); /** * Return prefixToNamespaceMapping @@ -44,14 +47,14 @@ public Map getPrefixToNamespaceMapping() { return prefixToNamespaceMapping; } - private MetadataFieldMapping> metadataFieldMapping; + private MetadataFieldMapping> metadataFieldMapping; /** * Return metadataFieldMapping * * @return MetadataFieldMapping */ - public MetadataFieldMapping> getMetadataFieldMapping() { + public MetadataFieldMapping> getMetadataFieldMapping() { return metadataFieldMapping; } @@ -62,7 +65,7 @@ public MetadataFieldMapping> getMetada */ @Override public void setMetadataFieldMapping( - MetadataFieldMapping> metadataFieldMapping) { + MetadataFieldMapping> metadataFieldMapping) { this.metadataFieldMapping = metadataFieldMapping; } @@ -140,36 +143,35 @@ public void setQuery(String query) { * Depending on the retrieved node (using the query), different types of values will be added to the MetadatumDTO * list * - * @param t A class to retrieve metadata from. + * @param t An element to retrieve metadata from. * @return a collection of import records. Only the identifier of the found records may be put in the record. */ @Override - public Collection contributeMetadata(OMElement t) { + public Collection contributeMetadata(Element t) { List values = new LinkedList<>(); - try { - AXIOMXPath xpath = new AXIOMXPath(query); - for (String ns : prefixToNamespaceMapping.keySet()) { - xpath.addNamespace(prefixToNamespaceMapping.get(ns), ns); - } - List nodes = xpath.selectNodes(t); - for (Object el : nodes) { - if (el instanceof OMElement) { - values.add(metadataFieldMapping.toDCValue(field, ((OMElement) el).getText())); - } else if (el instanceof OMAttribute) { - values.add(metadataFieldMapping.toDCValue(field, ((OMAttribute) el).getAttributeValue())); - } else if (el instanceof String) { - values.add(metadataFieldMapping.toDCValue(field, (String) el)); - } else if (el instanceof OMText) { - values.add(metadataFieldMapping.toDCValue(field, ((OMText) el).getText())); - } else { - log.error("node of type: " + el.getClass()); - } - } - return values; - } catch (JaxenException e) { - log.error(query, e); - throw new RuntimeException(e); + + List namespaces = new ArrayList<>(); + for (String ns : prefixToNamespaceMapping.keySet()) { + namespaces.add(Namespace.getNamespace(prefixToNamespaceMapping.get(ns), ns)); } + XPathExpression xpath = + XPathFactory.instance().compile(query, Filters.fpassthrough(), null, namespaces); + + List nodes = xpath.evaluate(t); + for (Object el : nodes) { + if (el instanceof Element) { + values.add(metadataFieldMapping.toDCValue(field, ((Element) el).getText())); + } else if (el instanceof Attribute) { + values.add(metadataFieldMapping.toDCValue(field, ((Attribute) el).getValue())); + } else if (el instanceof String) { + values.add(metadataFieldMapping.toDCValue(field, (String) el)); + } else if (el instanceof Text) { + values.add(metadataFieldMapping.toDCValue(field, ((Text) el).getText())); + } else { + log.error("Encountered unsupported XML node of type: {}. Skipped that node.", el.getClass()); + } + } + return values; } } diff --git a/dspace-api/src/main/java/org/dspace/importer/external/pubmed/service/PubmedImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/pubmed/service/PubmedImportMetadataSourceServiceImpl.java index 4802dcfa1787..73768330d865 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/pubmed/service/PubmedImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/pubmed/service/PubmedImportMetadataSourceServiceImpl.java @@ -25,10 +25,6 @@ import javax.ws.rs.core.Response; import com.google.common.io.CharStreams; -import org.apache.axiom.om.OMElement; -import org.apache.axiom.om.OMXMLBuilderFactory; -import org.apache.axiom.om.OMXMLParserWrapper; -import org.apache.axiom.om.xpath.AXIOMXPath; import org.dspace.content.Item; import org.dspace.importer.external.datamodel.ImportRecord; import org.dspace.importer.external.datamodel.Query; @@ -38,7 +34,13 @@ import org.dspace.importer.external.service.AbstractImportMetadataSourceService; import org.dspace.importer.external.service.components.FileSource; import org.dspace.importer.external.service.components.QuerySource; -import org.jaxen.JaxenException; +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.JDOMException; +import org.jdom2.filter.Filters; +import org.jdom2.input.SAXBuilder; +import org.jdom2.xpath.XPathExpression; +import org.jdom2.xpath.XPathFactory; /** * Implements a data source for querying PubMed Central @@ -46,7 +48,7 @@ * @author Roeland Dillen (roeland at atmire dot com) * @author Pasquale Cavallo (pasquale.cavallo at 4science dot it) */ -public class PubmedImportMetadataSourceServiceImpl extends AbstractImportMetadataSourceService +public class PubmedImportMetadataSourceServiceImpl extends AbstractImportMetadataSourceService implements QuerySource, FileSource { private String baseAddress; @@ -59,7 +61,7 @@ public class PubmedImportMetadataSourceServiceImpl extends AbstractImportMetadat /** * Set the file extensions supported by this metadata service * - * @param supportedExtensionsthe file extensions (xml,txt,...) supported by this service + * @param supportedExtensions the file extensions (xml,txt,...) supported by this service */ public void setSupportedExtensions(List supportedExtensions) { this.supportedExtensions = supportedExtensions; @@ -243,17 +245,21 @@ public Integer call() throws Exception { private String getSingleElementValue(String src, String elementName) { - OMXMLParserWrapper records = OMXMLBuilderFactory.createOMBuilder(new StringReader(src)); - OMElement element = records.getDocumentElement(); - AXIOMXPath xpath = null; String value = null; + try { - xpath = new AXIOMXPath("//" + elementName); - List recordsList = xpath.selectNodes(element); - if (!recordsList.isEmpty()) { - value = recordsList.get(0).getText(); + SAXBuilder saxBuilder = new SAXBuilder(); + Document document = saxBuilder.build(new StringReader(src)); + Element root = document.getRootElement(); + + XPathExpression xpath = + XPathFactory.instance().compile("//" + elementName, Filters.element()); + + Element record = xpath.evaluateFirst(root); + if (record != null) { + value = record.getText(); } - } catch (JaxenException e) { + } catch (JDOMException | IOException e) { value = null; } return value; @@ -314,9 +320,9 @@ public Collection call() throws Exception { invocationBuilder = getRecordsTarget.request(MediaType.TEXT_PLAIN_TYPE); response = invocationBuilder.get(); - List omElements = splitToRecords(response.readEntity(String.class)); + List elements = splitToRecords(response.readEntity(String.class)); - for (OMElement record : omElements) { + for (Element record : elements) { records.add(transformSourceRecords(record)); } @@ -324,15 +330,18 @@ public Collection call() throws Exception { } } - private List splitToRecords(String recordsSrc) { - OMXMLParserWrapper records = OMXMLBuilderFactory.createOMBuilder(new StringReader(recordsSrc)); - OMElement element = records.getDocumentElement(); - AXIOMXPath xpath = null; + private List splitToRecords(String recordsSrc) { try { - xpath = new AXIOMXPath("//PubmedArticle"); - List recordsList = xpath.selectNodes(element); + SAXBuilder saxBuilder = new SAXBuilder(); + Document document = saxBuilder.build(new StringReader(recordsSrc)); + Element root = document.getRootElement(); + + XPathExpression xpath = + XPathFactory.instance().compile("//PubmedArticle", Filters.element()); + + List recordsList = xpath.evaluate(root); return recordsList; - } catch (JaxenException e) { + } catch (JDOMException | IOException e) { return null; } } @@ -362,13 +371,13 @@ public ImportRecord call() throws Exception { Response response = invocationBuilder.get(); - List omElements = splitToRecords(response.readEntity(String.class)); + List elements = splitToRecords(response.readEntity(String.class)); - if (omElements.size() == 0) { + if (elements.isEmpty()) { return null; } - return transformSourceRecords(omElements.get(0)); + return transformSourceRecords(elements.get(0)); } } @@ -441,8 +450,8 @@ public ImportRecord getRecord(InputStream inputStream) throws FileSourceExceptio private List parseXMLString(String xml) { List records = new LinkedList(); - List omElements = splitToRecords(xml); - for (OMElement record : omElements) { + List elements = splitToRecords(xml); + for (Element record : elements) { records.add(transformSourceRecords(record)); } return records; diff --git a/dspace-swordv2/pom.xml b/dspace-swordv2/pom.xml index 3fbc6cf469b2..6bf09475ef3c 100644 --- a/dspace-swordv2/pom.xml +++ b/dspace-swordv2/pom.xml @@ -135,7 +135,8 @@ org.apache.ws.commons.axiom fom-impl - ${axiom.version} + + 1.2.22 diff --git a/pom.xml b/pom.xml index 4dfe21d9a604..e03dcb3b07ba 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,6 @@ 42.3.3 8.8.1 - 1.2.22 3.4.0 2.10.0 @@ -1117,7 +1116,7 @@ 3.4.14 - + org.apache.james apache-mime4j-core From aeac1ee1ea09eb180135ad2515e1b39370422516 Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Wed, 23 Mar 2022 22:37:00 +0100 Subject: [PATCH 0062/1846] 88675: Fix place of opposite item --- .../DefaultItemVersionProvider.java | 12 +- .../VersioningWithRelationshipsTest.java | 300 +++++++++--------- 2 files changed, 161 insertions(+), 151 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/versioning/DefaultItemVersionProvider.java b/dspace-api/src/main/java/org/dspace/versioning/DefaultItemVersionProvider.java index d4590ae24ea2..e12acfca33c3 100644 --- a/dspace-api/src/main/java/org/dspace/versioning/DefaultItemVersionProvider.java +++ b/dspace-api/src/main/java/org/dspace/versioning/DefaultItemVersionProvider.java @@ -148,8 +148,12 @@ protected void copyRelationships( newItem, // new item oldRelationship.getRightItem(), oldRelationship.getRelationshipType(), + // NOTE: on the side of the new version, we start with an empty list of relationships + // => insert at the same position as the ancestral relationship oldRelationship.getLeftPlace(), - oldRelationship.getRightPlace(), + // NOTE: on the opposite side of the new version, the ancestral relationship already takes our + // desired place => insert AFTER the ancestral relationship + oldRelationship.getRightPlace() + 1, oldRelationship.getLeftwardValue(), oldRelationship.getRightwardValue(), Relationship.LatestVersionStatus.RIGHT_ONLY // only mark the opposite side as "latest" for now @@ -161,7 +165,11 @@ protected void copyRelationships( oldRelationship.getLeftItem(), newItem, // new item oldRelationship.getRelationshipType(), - oldRelationship.getLeftPlace(), + // NOTE: on the opposite side of the new version, the ancestral relationship already takes our + // desired place => insert AFTER the ancestral relationship + oldRelationship.getLeftPlace() + 1, + // NOTE: on the side of the new version, we start with an empty list of relationships + // => insert at the same position as the ancestral relationship oldRelationship.getRightPlace(), oldRelationship.getLeftwardValue(), oldRelationship.getRightwardValue(), diff --git a/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java b/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java index 272d2ccb2bb5..a1aee64f388c 100644 --- a/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java +++ b/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java @@ -139,14 +139,16 @@ public void setUp() throws Exception { .build(); } - protected Matcher isRelationship( - Item leftItem, RelationshipType relationshipType, Item rightItem, LatestVersionStatus latestVersionStatus + protected Matcher isRel( + Item leftItem, RelationshipType relationshipType, Item rightItem, LatestVersionStatus latestVersionStatus, + int leftPlace, int rightPlace ) { return allOf( hasProperty("leftItem", is(leftItem)), hasProperty("relationshipType", is(relationshipType)), hasProperty("rightItem", is(rightItem)), - // NOTE: place is not checked + hasProperty("leftPlace", is(leftPlace)), + hasProperty("rightPlace", is(rightPlace)), hasProperty("leftwardValue", nullValue()), hasProperty("rightwardValue", nullValue()), hasProperty("latestVersionStatus", is(latestVersionStatus)) @@ -195,30 +197,30 @@ public void test_createNewVersionOfItemOnLeftSideOfRelationships() throws Except assertThat( relationshipService.findByItem(context, originalPublication, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH), - isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH), - isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH) + isRel(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 0), + isRel(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH, 0, 0), + isRel(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, person1, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH) + isRel(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, project1, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH) + isRel(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, orgUnit1, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH) + isRel(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH, 0, 0) )) ); @@ -237,39 +239,39 @@ public void test_createNewVersionOfItemOnLeftSideOfRelationships() throws Except assertThat( relationshipService.findByItem(context, originalPublication, -1, -1, false, true), containsInAnyOrder(List.of( - isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH), - isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH), - isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH) + isRel(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 0), + isRel(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH, 0, 0), + isRel(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, person1, -1, -1, false, true), containsInAnyOrder(List.of( - isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH) + isRel(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, project1, -1, -1, false, true), containsInAnyOrder(List.of( - isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH) + isRel(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, orgUnit1, -1, -1, false, true), containsInAnyOrder(List.of( - isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH) + isRel(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, newPublication, -1, -1, false, true), containsInAnyOrder(List.of( - isRelationship(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY), - isRelationship(newPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY), - isRelationship(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY) + isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY, 0, 1), + isRel(newPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY, 0, 1), + isRel(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY, 0, 1) )) ); @@ -280,42 +282,42 @@ public void test_createNewVersionOfItemOnLeftSideOfRelationships() throws Except assertThat( relationshipService.findByItem(context, originalPublication, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH), - isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH), - isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH) + isRel(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 0), + isRel(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH, 0, 0), + isRel(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, person1, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH), - isRelationship(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY) + isRel(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 0), + isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY, 0, 1) )) ); assertThat( relationshipService.findByItem(context, project1, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH), - isRelationship(newPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY) + isRel(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH, 0, 0), + isRel(newPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY, 0, 1) )) ); assertThat( relationshipService.findByItem(context, orgUnit1, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH), - isRelationship(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY) + isRel(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH, 0, 0), + isRel(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY, 0, 1) )) ); assertThat( relationshipService.findByItem(context, newPublication, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY), - isRelationship(newPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY), - isRelationship(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY) + isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY, 0, 1), + isRel(newPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY, 0, 1), + isRel(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY, 0, 1) )) ); @@ -334,39 +336,39 @@ public void test_createNewVersionOfItemOnLeftSideOfRelationships() throws Except assertThat( relationshipService.findByItem(context, originalPublication, -1, -1, false, true), containsInAnyOrder(List.of( - isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY), - isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY), - isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY) + isRel(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY, 0, 0), + isRel(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY, 0, 0), + isRel(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY, 0, 0) )) ); assertThat( relationshipService.findByItem(context, person1, -1, -1, false, true), containsInAnyOrder(List.of( - isRelationship(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH) + isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 1) )) ); assertThat( relationshipService.findByItem(context, project1, -1, -1, false, true), containsInAnyOrder(List.of( - isRelationship(newPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH) + isRel(newPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH, 0, 1) )) ); assertThat( relationshipService.findByItem(context, orgUnit1, -1, -1, false, true), containsInAnyOrder(List.of( - isRelationship(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH) + isRel(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH, 0, 1) )) ); assertThat( relationshipService.findByItem(context, newPublication, -1, -1, false, true), containsInAnyOrder(List.of( - isRelationship(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH), - isRelationship(newPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH), - isRelationship(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH) + isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 1), + isRel(newPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH, 0, 1), + isRel(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH, 0, 1) )) ); @@ -377,42 +379,42 @@ public void test_createNewVersionOfItemOnLeftSideOfRelationships() throws Except assertThat( relationshipService.findByItem(context, originalPublication, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY), - isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY), - isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY) + isRel(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY, 0, 0), + isRel(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY, 0, 0), + isRel(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY, 0, 0) )) ); assertThat( relationshipService.findByItem(context, person1, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY), - isRelationship(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH) + isRel(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY, 0, 0), + isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 1) )) ); assertThat( relationshipService.findByItem(context, project1, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY), - isRelationship(newPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH) + isRel(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY, 0, 0), + isRel(newPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH, 0, 1) )) ); assertThat( relationshipService.findByItem(context, orgUnit1, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY), - isRelationship(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH) + isRel(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY, 0, 0), + isRel(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH, 0, 1) )) ); assertThat( relationshipService.findByItem(context, newPublication, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH), - isRelationship(newPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH), - isRelationship(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH) + isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 1), + isRel(newPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH, 0, 1), + isRel(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH, 0, 1) )) ); @@ -471,30 +473,30 @@ public void test_createNewVersionOfItemAndModifyRelationships() throws Exception assertThat( relationshipService.findByItem(context, originalPublication, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH), - isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH), - isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH) + isRel(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 0), + isRel(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH, 0, 0), + isRel(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, person1, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH) + isRel(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, project1, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH) + isRel(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, orgUnit1, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH) + isRel(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH, 0, 0) )) ); @@ -545,16 +547,16 @@ public void test_createNewVersionOfItemAndModifyRelationships() throws Exception assertThat( relationshipService.findByItem(context, originalPublication, -1, -1, false, true), containsInAnyOrder(List.of( - isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH), - isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH), - isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH) + isRel(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 0), + isRel(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH, 0, 0), + isRel(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, person1, -1, -1, false, true), containsInAnyOrder(List.of( - isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH) + isRel(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 0) )) ); @@ -562,21 +564,21 @@ public void test_createNewVersionOfItemAndModifyRelationships() throws Exception relationshipService.findByItem(context, person2, -1, -1, false, true), containsInAnyOrder(List.of( // NOTE: BOTH because new relationship - isRelationship(newPublication, isAuthorOfPublication, person2, LatestVersionStatus.BOTH) + isRel(newPublication, isAuthorOfPublication, person2, LatestVersionStatus.BOTH, 1, 0) )) ); assertThat( relationshipService.findByItem(context, project1, -1, -1, false, true), containsInAnyOrder(List.of( - isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH) + isRel(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, orgUnit1, -1, -1, false, true), containsInAnyOrder(List.of( - isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH) + isRel(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH, 0, 0) )) ); @@ -584,17 +586,17 @@ public void test_createNewVersionOfItemAndModifyRelationships() throws Exception relationshipService.findByItem(context, orgUnit2, -1, -1, false, true), containsInAnyOrder(List.of( // NOTE: BOTH because new relationship - isRelationship(newPublication, isOrgUnitOfPublication, orgUnit2, LatestVersionStatus.BOTH) + isRel(newPublication, isOrgUnitOfPublication, orgUnit2, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, newPublication, -1, -1, false, true), containsInAnyOrder(List.of( - isRelationship(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY), + isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY, 0, 1), // NOTE: BOTH because new relationship - isRelationship(newPublication, isAuthorOfPublication, person2, LatestVersionStatus.BOTH), - isRelationship(newPublication, isOrgUnitOfPublication, orgUnit2, LatestVersionStatus.BOTH) + isRel(newPublication, isAuthorOfPublication, person2, LatestVersionStatus.BOTH, 1, 0), + isRel(newPublication, isOrgUnitOfPublication, orgUnit2, LatestVersionStatus.BOTH, 0, 0) )) ); @@ -605,17 +607,17 @@ public void test_createNewVersionOfItemAndModifyRelationships() throws Exception assertThat( relationshipService.findByItem(context, originalPublication, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH), - isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH), - isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH) + isRel(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 0), + isRel(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH, 0, 0), + isRel(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, person1, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH), - isRelationship(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY) + isRel(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 0), + isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY, 0, 1) )) ); @@ -623,21 +625,21 @@ public void test_createNewVersionOfItemAndModifyRelationships() throws Exception relationshipService.findByItem(context, person2, -1, -1, false, false), containsInAnyOrder(List.of( // NOTE: BOTH because new relationship - isRelationship(newPublication, isAuthorOfPublication, person2, LatestVersionStatus.BOTH) + isRel(newPublication, isAuthorOfPublication, person2, LatestVersionStatus.BOTH, 1, 0) )) ); assertThat( relationshipService.findByItem(context, project1, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH) + isRel(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, orgUnit1, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH) + isRel(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH, 0, 0) )) ); @@ -645,17 +647,17 @@ public void test_createNewVersionOfItemAndModifyRelationships() throws Exception relationshipService.findByItem(context, orgUnit2, -1, -1, false, false), containsInAnyOrder(List.of( // NOTE: BOTH because new relationship - isRelationship(newPublication, isOrgUnitOfPublication, orgUnit2, LatestVersionStatus.BOTH) + isRel(newPublication, isOrgUnitOfPublication, orgUnit2, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, newPublication, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY), + isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY, 0, 1), // NOTE: BOTH because new relationship - isRelationship(newPublication, isAuthorOfPublication, person2, LatestVersionStatus.BOTH), - isRelationship(newPublication, isOrgUnitOfPublication, orgUnit2, LatestVersionStatus.BOTH) + isRel(newPublication, isAuthorOfPublication, person2, LatestVersionStatus.BOTH, 1, 0), + isRel(newPublication, isOrgUnitOfPublication, orgUnit2, LatestVersionStatus.BOTH, 0, 0) )) ); @@ -674,23 +676,23 @@ public void test_createNewVersionOfItemAndModifyRelationships() throws Exception assertThat( relationshipService.findByItem(context, originalPublication, -1, -1, false, true), containsInAnyOrder(List.of( - isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY), - isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY), - isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY) + isRel(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY, 0, 0), + isRel(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY, 0, 0), + isRel(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY, 0, 0) )) ); assertThat( relationshipService.findByItem(context, person1, -1, -1, false, true), containsInAnyOrder(List.of( - isRelationship(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH) + isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 1) )) ); assertThat( relationshipService.findByItem(context, person2, -1, -1, false, true), containsInAnyOrder(List.of( - isRelationship(newPublication, isAuthorOfPublication, person2, LatestVersionStatus.BOTH) + isRel(newPublication, isAuthorOfPublication, person2, LatestVersionStatus.BOTH, 1, 0) )) ); @@ -707,16 +709,16 @@ public void test_createNewVersionOfItemAndModifyRelationships() throws Exception assertThat( relationshipService.findByItem(context, orgUnit2, -1, -1, false, true), containsInAnyOrder(List.of( - isRelationship(newPublication, isOrgUnitOfPublication, orgUnit2, LatestVersionStatus.BOTH) + isRel(newPublication, isOrgUnitOfPublication, orgUnit2, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, newPublication, -1, -1, false, true), containsInAnyOrder(List.of( - isRelationship(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH), - isRelationship(newPublication, isAuthorOfPublication, person2, LatestVersionStatus.BOTH), - isRelationship(newPublication, isOrgUnitOfPublication, orgUnit2, LatestVersionStatus.BOTH) + isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 1), + isRel(newPublication, isAuthorOfPublication, person2, LatestVersionStatus.BOTH, 1, 0), + isRel(newPublication, isOrgUnitOfPublication, orgUnit2, LatestVersionStatus.BOTH, 0, 0) )) ); @@ -727,54 +729,54 @@ public void test_createNewVersionOfItemAndModifyRelationships() throws Exception assertThat( relationshipService.findByItem(context, originalPublication, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY), - isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY), - isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY) + isRel(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY, 0, 0), + isRel(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY, 0, 0), + isRel(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY, 0, 0) )) ); assertThat( relationshipService.findByItem(context, person1, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY), - isRelationship(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH) + isRel(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY, 0, 0), + isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 1) )) ); assertThat( relationshipService.findByItem(context, person2, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(newPublication, isAuthorOfPublication, person2, LatestVersionStatus.BOTH) + isRel(newPublication, isAuthorOfPublication, person2, LatestVersionStatus.BOTH, 1, 0) )) ); assertThat( relationshipService.findByItem(context, project1, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY) + isRel(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY, 0, 0) )) ); assertThat( relationshipService.findByItem(context, orgUnit1, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY) + isRel(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY, 0, 0) )) ); assertThat( relationshipService.findByItem(context, orgUnit2, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(newPublication, isOrgUnitOfPublication, orgUnit2, LatestVersionStatus.BOTH) + isRel(newPublication, isOrgUnitOfPublication, orgUnit2, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, newPublication, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH), - isRelationship(newPublication, isAuthorOfPublication, person2, LatestVersionStatus.BOTH), - isRelationship(newPublication, isOrgUnitOfPublication, orgUnit2, LatestVersionStatus.BOTH) + isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 1), + isRel(newPublication, isAuthorOfPublication, person2, LatestVersionStatus.BOTH, 1, 0), + isRel(newPublication, isOrgUnitOfPublication, orgUnit2, LatestVersionStatus.BOTH, 0, 0) )) ); @@ -831,30 +833,30 @@ public void test_createNewVersionOfItemOnRightSideOfRelationships() throws Excep assertThat( relationshipService.findByItem(context, originalPerson, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(publication1, isAuthorOfPublication, originalPerson, LatestVersionStatus.BOTH), - isRelationship(project1, isMemberOfProject, originalPerson, LatestVersionStatus.BOTH), - isRelationship(orgUnit1, isMemberOfOrgUnit, originalPerson, LatestVersionStatus.BOTH) + isRel(publication1, isAuthorOfPublication, originalPerson, LatestVersionStatus.BOTH, 0, 0), + isRel(project1, isMemberOfProject, originalPerson, LatestVersionStatus.BOTH, 0, 0), + isRel(orgUnit1, isMemberOfOrgUnit, originalPerson, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, publication1, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(publication1, isAuthorOfPublication, originalPerson, LatestVersionStatus.BOTH) + isRel(publication1, isAuthorOfPublication, originalPerson, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, project1, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(project1, isMemberOfProject, originalPerson, LatestVersionStatus.BOTH) + isRel(project1, isMemberOfProject, originalPerson, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, orgUnit1, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(orgUnit1, isMemberOfOrgUnit, originalPerson, LatestVersionStatus.BOTH) + isRel(orgUnit1, isMemberOfOrgUnit, originalPerson, LatestVersionStatus.BOTH, 0, 0) )) ); @@ -873,39 +875,39 @@ public void test_createNewVersionOfItemOnRightSideOfRelationships() throws Excep assertThat( relationshipService.findByItem(context, originalPerson, -1, -1, false, true), containsInAnyOrder(List.of( - isRelationship(publication1, isAuthorOfPublication, originalPerson, LatestVersionStatus.BOTH), - isRelationship(project1, isMemberOfProject, originalPerson, LatestVersionStatus.BOTH), - isRelationship(orgUnit1, isMemberOfOrgUnit, originalPerson, LatestVersionStatus.BOTH) + isRel(publication1, isAuthorOfPublication, originalPerson, LatestVersionStatus.BOTH, 0, 0), + isRel(project1, isMemberOfProject, originalPerson, LatestVersionStatus.BOTH, 0, 0), + isRel(orgUnit1, isMemberOfOrgUnit, originalPerson, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, publication1, -1, -1, false, true), containsInAnyOrder(List.of( - isRelationship(publication1, isAuthorOfPublication, originalPerson, LatestVersionStatus.BOTH) + isRel(publication1, isAuthorOfPublication, originalPerson, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, project1, -1, -1, false, true), containsInAnyOrder(List.of( - isRelationship(project1, isMemberOfProject, originalPerson, LatestVersionStatus.BOTH) + isRel(project1, isMemberOfProject, originalPerson, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, orgUnit1, -1, -1, false, true), containsInAnyOrder(List.of( - isRelationship(orgUnit1, isMemberOfOrgUnit, originalPerson, LatestVersionStatus.BOTH) + isRel(orgUnit1, isMemberOfOrgUnit, originalPerson, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, newPerson, -1, -1, false, true), containsInAnyOrder(List.of( - isRelationship(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.LEFT_ONLY), - isRelationship(project1, isMemberOfProject, newPerson, LatestVersionStatus.LEFT_ONLY), - isRelationship(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.LEFT_ONLY) + isRel(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.LEFT_ONLY, 1, 0), + isRel(project1, isMemberOfProject, newPerson, LatestVersionStatus.LEFT_ONLY, 1, 0), + isRel(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.LEFT_ONLY, 1, 0) )) ); @@ -916,42 +918,42 @@ public void test_createNewVersionOfItemOnRightSideOfRelationships() throws Excep assertThat( relationshipService.findByItem(context, originalPerson, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(publication1, isAuthorOfPublication, originalPerson, LatestVersionStatus.BOTH), - isRelationship(project1, isMemberOfProject, originalPerson, LatestVersionStatus.BOTH), - isRelationship(orgUnit1, isMemberOfOrgUnit, originalPerson, LatestVersionStatus.BOTH) + isRel(publication1, isAuthorOfPublication, originalPerson, LatestVersionStatus.BOTH, 0, 0), + isRel(project1, isMemberOfProject, originalPerson, LatestVersionStatus.BOTH, 0, 0), + isRel(orgUnit1, isMemberOfOrgUnit, originalPerson, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, publication1, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(publication1, isAuthorOfPublication, originalPerson, LatestVersionStatus.BOTH), - isRelationship(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.LEFT_ONLY) + isRel(publication1, isAuthorOfPublication, originalPerson, LatestVersionStatus.BOTH, 0, 0), + isRel(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.LEFT_ONLY, 1, 0) )) ); assertThat( relationshipService.findByItem(context, project1, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(project1, isMemberOfProject, originalPerson, LatestVersionStatus.BOTH), - isRelationship(project1, isMemberOfProject, newPerson, LatestVersionStatus.LEFT_ONLY) + isRel(project1, isMemberOfProject, originalPerson, LatestVersionStatus.BOTH, 0, 0), + isRel(project1, isMemberOfProject, newPerson, LatestVersionStatus.LEFT_ONLY, 1, 0) )) ); assertThat( relationshipService.findByItem(context, orgUnit1, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(orgUnit1, isMemberOfOrgUnit, originalPerson, LatestVersionStatus.BOTH), - isRelationship(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.LEFT_ONLY) + isRel(orgUnit1, isMemberOfOrgUnit, originalPerson, LatestVersionStatus.BOTH, 0, 0), + isRel(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.LEFT_ONLY, 1, 0) )) ); assertThat( relationshipService.findByItem(context, newPerson, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.LEFT_ONLY), - isRelationship(project1, isMemberOfProject, newPerson, LatestVersionStatus.LEFT_ONLY), - isRelationship(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.LEFT_ONLY) + isRel(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.LEFT_ONLY, 1, 0), + isRel(project1, isMemberOfProject, newPerson, LatestVersionStatus.LEFT_ONLY, 1, 0), + isRel(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.LEFT_ONLY, 1, 0) )) ); @@ -970,39 +972,39 @@ public void test_createNewVersionOfItemOnRightSideOfRelationships() throws Excep assertThat( relationshipService.findByItem(context, originalPerson, -1, -1, false, true), containsInAnyOrder(List.of( - isRelationship(publication1, isAuthorOfPublication, originalPerson, LatestVersionStatus.LEFT_ONLY), - isRelationship(project1, isMemberOfProject, originalPerson, LatestVersionStatus.LEFT_ONLY), - isRelationship(orgUnit1, isMemberOfOrgUnit, originalPerson, LatestVersionStatus.LEFT_ONLY) + isRel(publication1, isAuthorOfPublication, originalPerson, LatestVersionStatus.LEFT_ONLY, 0, 0), + isRel(project1, isMemberOfProject, originalPerson, LatestVersionStatus.LEFT_ONLY, 0, 0), + isRel(orgUnit1, isMemberOfOrgUnit, originalPerson, LatestVersionStatus.LEFT_ONLY, 0, 0) )) ); assertThat( relationshipService.findByItem(context, publication1, -1, -1, false, true), containsInAnyOrder(List.of( - isRelationship(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.BOTH) + isRel(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.BOTH, 1, 0) )) ); assertThat( relationshipService.findByItem(context, project1, -1, -1, false, true), containsInAnyOrder(List.of( - isRelationship(project1, isMemberOfProject, newPerson, LatestVersionStatus.BOTH) + isRel(project1, isMemberOfProject, newPerson, LatestVersionStatus.BOTH, 1, 0) )) ); assertThat( relationshipService.findByItem(context, orgUnit1, -1, -1, false, true), containsInAnyOrder(List.of( - isRelationship(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.BOTH) + isRel(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.BOTH, 1, 0) )) ); assertThat( relationshipService.findByItem(context, newPerson, -1, -1, false, true), containsInAnyOrder(List.of( - isRelationship(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.BOTH), - isRelationship(project1, isMemberOfProject, newPerson, LatestVersionStatus.BOTH), - isRelationship(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.BOTH) + isRel(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.BOTH, 1, 0), + isRel(project1, isMemberOfProject, newPerson, LatestVersionStatus.BOTH, 1, 0), + isRel(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.BOTH, 1, 0) )) ); @@ -1013,42 +1015,42 @@ public void test_createNewVersionOfItemOnRightSideOfRelationships() throws Excep assertThat( relationshipService.findByItem(context, originalPerson, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(publication1, isAuthorOfPublication, originalPerson, LatestVersionStatus.LEFT_ONLY), - isRelationship(project1, isMemberOfProject, originalPerson, LatestVersionStatus.LEFT_ONLY), - isRelationship(orgUnit1, isMemberOfOrgUnit, originalPerson, LatestVersionStatus.LEFT_ONLY) + isRel(publication1, isAuthorOfPublication, originalPerson, LatestVersionStatus.LEFT_ONLY, 0, 0), + isRel(project1, isMemberOfProject, originalPerson, LatestVersionStatus.LEFT_ONLY, 0, 0), + isRel(orgUnit1, isMemberOfOrgUnit, originalPerson, LatestVersionStatus.LEFT_ONLY, 0, 0) )) ); assertThat( relationshipService.findByItem(context, publication1, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(publication1, isAuthorOfPublication, originalPerson, LatestVersionStatus.LEFT_ONLY), - isRelationship(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.BOTH) + isRel(publication1, isAuthorOfPublication, originalPerson, LatestVersionStatus.LEFT_ONLY, 0, 0), + isRel(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.BOTH, 1, 0) )) ); assertThat( relationshipService.findByItem(context, project1, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(project1, isMemberOfProject, originalPerson, LatestVersionStatus.LEFT_ONLY), - isRelationship(project1, isMemberOfProject, newPerson, LatestVersionStatus.BOTH) + isRel(project1, isMemberOfProject, originalPerson, LatestVersionStatus.LEFT_ONLY, 0, 0), + isRel(project1, isMemberOfProject, newPerson, LatestVersionStatus.BOTH, 1, 0) )) ); assertThat( relationshipService.findByItem(context, orgUnit1, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(orgUnit1, isMemberOfOrgUnit, originalPerson, LatestVersionStatus.LEFT_ONLY), - isRelationship(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.BOTH) + isRel(orgUnit1, isMemberOfOrgUnit, originalPerson, LatestVersionStatus.LEFT_ONLY, 0, 0), + isRel(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.BOTH, 1, 0) )) ); assertThat( relationshipService.findByItem(context, newPerson, -1, -1, false, false), containsInAnyOrder(List.of( - isRelationship(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.BOTH), - isRelationship(project1, isMemberOfProject, newPerson, LatestVersionStatus.BOTH), - isRelationship(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.BOTH) + isRel(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.BOTH, 1, 0), + isRel(project1, isMemberOfProject, newPerson, LatestVersionStatus.BOTH, 1, 0), + isRel(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.BOTH, 1, 0) )) ); From aa77b0a3bb4a16c4114bc6a55d823decf6c57e09 Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Wed, 23 Mar 2022 23:20:22 +0100 Subject: [PATCH 0063/1846] 88675: WIP: Test virtual metadata on new version --- .../VersioningWithRelationshipsTest.java | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java b/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java index a1aee64f388c..96cf432ab9d5 100644 --- a/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java +++ b/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java @@ -1340,4 +1340,150 @@ public void test_createNewVersionOfItemAndVerifyMetadataOrder() throws Exception } } + @Test + public void test_virtualMetadataPreserved() throws Exception { + ////////////////////////////////////////////// + // create a publication and link two people // + ////////////////////////////////////////////// + + Item publication1V1 = ItemBuilder.createItem(context, collection) + .withTitle("publication 1V1") + .withMetadata("dspace", "entity", "type", publicationEntityType.getLabel()) + .build(); + + Item person1V1 = ItemBuilder.createItem(context, collection) + .withTitle("person 1V1") + .withMetadata("dspace", "entity", "type", personEntityType.getLabel()) + .withPersonIdentifierFirstName("Donald") + .withPersonIdentifierLastName("Smith") + .build(); + + Item person2V1 = ItemBuilder.createItem(context, collection) + .withTitle("person 2V1") + .withMetadata("dspace", "entity", "type", personEntityType.getLabel()) + .withPersonIdentifierFirstName("Jane") + .withPersonIdentifierLastName("Doe") + .build(); + + RelationshipBuilder.createRelationshipBuilder(context, publication1V1, person1V1, isAuthorOfPublication) + .build(); + + RelationshipBuilder.createRelationshipBuilder(context, publication1V1, person2V1, isAuthorOfPublication) + .withRightwardValue("Doe, J.") + .build(); + + /////////////////////////////////////////////// + // test dc.contributor.author of publication // + /////////////////////////////////////////////// + + List mdvs1 = itemService.getMetadata( + publication1V1, "dc", "contributor", "author", Item.ANY + ); + assertEquals(2, mdvs1.size()); + + assertTrue(mdvs1.get(0) instanceof RelationshipMetadataValue); + assertEquals("Smith, Donald", mdvs1.get(0).getValue()); + assertEquals(0, mdvs1.get(0).getPlace()); + + assertTrue(mdvs1.get(1) instanceof RelationshipMetadataValue); + assertEquals("Doe, J.", mdvs1.get(1).getValue()); + assertEquals(1, mdvs1.get(1).getPlace()); + + /////////////////////////////////////////////////////// + // create a new version of publication 1 and archive // + /////////////////////////////////////////////////////// + + Item publication1V2 = versioningService.createNewVersion(context, publication1V1).getItem(); + installItemService.installItem(context, workspaceItemService.findByItem(context, publication1V2)); + context.dispatchEvents(); + + //////////////////////////////////// + // create new version of person 1 // + //////////////////////////////////// + + Item person1V2 = versioningService.createNewVersion(context, person1V1).getItem(); + // update "Smith, Donald" to "Smith, D." + itemService.replaceMetadata( + context, person1V2, "person", "givenName", null, null, "D.", + null, -1, 0 + ); + itemService.update(context, person1V2); + + /////////////////////////////////////////////////// + // test dc.contributor.author of old publication // + /////////////////////////////////////////////////// + + List mdvs2 = itemService.getMetadata( + publication1V1, "dc", "contributor", "author", Item.ANY + ); + assertEquals(2, mdvs2.size()); + + assertTrue(mdvs2.get(0) instanceof RelationshipMetadataValue); + assertEquals("Smith, Donald", mdvs2.get(0).getValue()); + assertEquals(0, mdvs2.get(0).getPlace()); + + assertTrue(mdvs2.get(1) instanceof RelationshipMetadataValue); + assertEquals("Doe, J.", mdvs2.get(1).getValue()); + assertEquals(1, mdvs2.get(1).getPlace()); + + /////////////////////////////////////////////////// + // test dc.contributor.author of new publication // + /////////////////////////////////////////////////// + + List mdvs3 = itemService.getMetadata( + publication1V2, "dc", "contributor", "author", Item.ANY + ); + assertEquals(2, mdvs3.size()); + + assertTrue(mdvs3.get(0) instanceof RelationshipMetadataValue); + assertEquals("Smith, Donald", mdvs3.get(0).getValue()); + assertEquals(0, mdvs3.get(0).getPlace()); + + assertTrue(mdvs3.get(1) instanceof RelationshipMetadataValue); + assertEquals("Doe, J.", mdvs3.get(1).getValue()); + assertEquals(1, mdvs3.get(1).getPlace()); + + ///////////////////////////////////// + // archive new version of person 1 // + ///////////////////////////////////// + + installItemService.installItem(context, workspaceItemService.findByItem(context, person1V2)); + context.dispatchEvents(); + + /////////////////////////////////////////////////// + // test dc.contributor.author of old publication // + /////////////////////////////////////////////////// + + List mdvs4 = itemService.getMetadata( + publication1V1, "dc", "contributor", "author", Item.ANY + ); + assertEquals(2, mdvs4.size()); + + assertTrue(mdvs4.get(0) instanceof RelationshipMetadataValue); + assertEquals("Smith, Donald", mdvs4.get(0).getValue()); + assertEquals(0, mdvs4.get(0).getPlace()); + + assertTrue(mdvs4.get(1) instanceof RelationshipMetadataValue); + assertEquals("Doe, J.", mdvs4.get(1).getValue()); + assertEquals(1, mdvs4.get(1).getPlace()); + + /////////////////////////////////////////////////// + // test dc.contributor.author of new publication // + /////////////////////////////////////////////////// + + List mdvs5 = itemService.getMetadata( + publication1V2, "dc", "contributor", "author", Item.ANY + ); + assertEquals(2, mdvs5.size()); + + assertTrue(mdvs5.get(0) instanceof RelationshipMetadataValue); + assertEquals("Smith, D.", mdvs5.get(0).getValue()); + assertEquals(0, mdvs5.get(0).getPlace()); + + assertTrue(mdvs5.get(1) instanceof RelationshipMetadataValue); + assertEquals("Doe, J.", mdvs5.get(1).getValue()); + assertEquals(1, mdvs5.get(1).getPlace()); + } + + // TODO } From cc23600fe96aa09b15e0206ad2cdf94f1815ee84 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Thu, 24 Mar 2022 13:48:17 +0100 Subject: [PATCH 0064/1846] [CST-5288] Improved health actuator --- .../authority/AuthoritySolrServiceImpl.java | 2 +- .../configuration/ActuatorConfiguration.java | 38 ++++++++++++++++++- .../app/rest/health/GeoIpHealthIndicator.java | 4 +- .../dspace/app/rest/HealthIndicatorsIT.java | 23 ++++++----- .../link/search/HealthIndicatorMatcher.java | 2 +- dspace/config/dspace.cfg | 1 + 6 files changed, 55 insertions(+), 15 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/authority/AuthoritySolrServiceImpl.java b/dspace-api/src/main/java/org/dspace/authority/AuthoritySolrServiceImpl.java index dab8cd5b2e03..2fb7a513a633 100644 --- a/dspace-api/src/main/java/org/dspace/authority/AuthoritySolrServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/authority/AuthoritySolrServiceImpl.java @@ -50,7 +50,7 @@ protected AuthoritySolrServiceImpl() { */ protected SolrClient solr = null; - protected SolrClient getSolr() + public SolrClient getSolr() throws MalformedURLException, SolrServerException, IOException { if (solr == null) { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/configuration/ActuatorConfiguration.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/configuration/ActuatorConfiguration.java index cb91ae4152b1..f2054a71019e 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/configuration/ActuatorConfiguration.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/configuration/ActuatorConfiguration.java @@ -7,15 +7,23 @@ */ package org.dspace.app.rest.configuration; +import java.io.IOException; +import java.net.MalformedURLException; import java.util.Arrays; +import org.apache.solr.client.solrj.SolrServerException; import org.dspace.app.rest.DiscoverableEndpointsService; import org.dspace.app.rest.health.GeoIpHealthIndicator; +import org.dspace.authority.AuthoritySolrServiceImpl; import org.dspace.discovery.SolrSearchCore; +import org.dspace.statistics.SolrStatisticsCore; +import org.dspace.xoai.services.api.solr.SolrServerResolver; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; +import org.springframework.boot.actuate.health.Status; import org.springframework.boot.actuate.solr.SolrHealthIndicator; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -31,6 +39,8 @@ @Configuration public class ActuatorConfiguration { + public static final Status UP_WITH_ISSUES_STATUS = new Status("UP_WITH_ISSUES"); + @Autowired private DiscoverableEndpointsService discoverableEndpointsService; @@ -43,11 +53,35 @@ public void registerActuatorEndpoints() { } @Bean - @ConditionalOnEnabledHealthIndicator("solr") - public SolrHealthIndicator solrHealthIndicator(SolrSearchCore solrSearchCore) { + @ConditionalOnEnabledHealthIndicator("solrSearch") + @ConditionalOnProperty("discovery.search.server") + public SolrHealthIndicator solrSearchCoreHealthIndicator(SolrSearchCore solrSearchCore) { return new SolrHealthIndicator(solrSearchCore.getSolr()); } + @Bean + @ConditionalOnEnabledHealthIndicator("solrStatistics") + @ConditionalOnProperty("solr-statistics.server") + public SolrHealthIndicator solrStatisticsCoreHealthIndicator(SolrStatisticsCore solrStatisticsCore) { + return new SolrHealthIndicator(solrStatisticsCore.getSolr()); + } + + @Bean + @ConditionalOnEnabledHealthIndicator("solrAuthority") + @ConditionalOnProperty("solr.authority.server") + public SolrHealthIndicator solrAuthorityCoreHealthIndicator(AuthoritySolrServiceImpl authoritySolrService) + throws MalformedURLException, SolrServerException, IOException { + return new SolrHealthIndicator(authoritySolrService.getSolr()); + } + + @Bean + @ConditionalOnEnabledHealthIndicator("solrOai") + @ConditionalOnProperty("oai.solr.url") + public SolrHealthIndicator solrOaiCoreHealthIndicator(SolrServerResolver solrServerResolver) + throws SolrServerException { + return new SolrHealthIndicator(solrServerResolver.getServer()); + } + @Bean @ConditionalOnEnabledHealthIndicator("geoIp") public GeoIpHealthIndicator geoIpHealthIndicator() { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/health/GeoIpHealthIndicator.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/health/GeoIpHealthIndicator.java index 5c8ad92dbb82..191bb4f1efaf 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/health/GeoIpHealthIndicator.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/health/GeoIpHealthIndicator.java @@ -7,6 +7,8 @@ */ package org.dspace.app.rest.health; +import static org.dspace.app.rest.configuration.ActuatorConfiguration.UP_WITH_ISSUES_STATUS; + import org.dspace.statistics.GeoIpService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.health.AbstractHealthIndicator; @@ -32,7 +34,7 @@ protected void doHealthCheck(Builder builder) throws Exception { geoIpService.getDatabaseReader(); builder.up(); } catch (IllegalStateException ex) { - builder.outOfService().withDetail("reason", ex.getMessage()); + builder.status(UP_WITH_ISSUES_STATUS).withDetail("reason", ex.getMessage()); } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/HealthIndicatorsIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/HealthIndicatorsIT.java index c28e172e61ad..4c63cc1da81a 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/HealthIndicatorsIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/HealthIndicatorsIT.java @@ -7,8 +7,9 @@ */ package org.dspace.app.rest; +import static org.dspace.app.rest.configuration.ActuatorConfiguration.UP_WITH_ISSUES_STATUS; import static org.dspace.app.rest.link.search.HealthIndicatorMatcher.match; -import static org.dspace.app.rest.link.search.HealthIndicatorMatcher.matchDb; +import static org.dspace.app.rest.link.search.HealthIndicatorMatcher.matchDatabase; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.is; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -35,8 +36,8 @@ public class HealthIndicatorsIT extends AbstractControllerIntegrationTest { public void testWithAnonymousUser() throws Exception { getClient().perform(get(HEALTH_PATH)) - .andExpect(status().isServiceUnavailable()) - .andExpect(jsonPath("$.status", is(Status.OUT_OF_SERVICE.getCode()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status", is(UP_WITH_ISSUES_STATUS.getCode()))) .andExpect(jsonPath("$.components").doesNotExist()); } @@ -47,8 +48,8 @@ public void testWithNotAdminUser() throws Exception { String token = getAuthToken(eperson.getEmail(), password); getClient(token).perform(get(HEALTH_PATH)) - .andExpect(status().isServiceUnavailable()) - .andExpect(jsonPath("$.status", is(Status.OUT_OF_SERVICE.getCode()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status", is(UP_WITH_ISSUES_STATUS.getCode()))) .andExpect(jsonPath("$.components").doesNotExist()); } @@ -59,12 +60,14 @@ public void testWithAdminUser() throws Exception { String token = getAuthToken(admin.getEmail(), password); getClient(token).perform(get(HEALTH_PATH)) - .andExpect(status().isServiceUnavailable()) - .andExpect(jsonPath("$.status", is(Status.OUT_OF_SERVICE.getCode()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status", is(UP_WITH_ISSUES_STATUS.getCode()))) .andExpect(jsonPath("$.components", allOf( - matchDb(Status.UP), - match("solr", Status.UP, Map.of("status", 0, "detectedPathType", "root")), - match("geoIp", Status.OUT_OF_SERVICE, + matchDatabase(Status.UP), + match("solrSearchCore", Status.UP, Map.of("status", 0, "detectedPathType", "root")), + match("solrOaiCore", Status.UP, Map.of("status", 0, "detectedPathType", "particular core")), + match("solrStatisticsCore", Status.UP, Map.of("status", 0, "detectedPathType", "root")), + match("geoIp", UP_WITH_ISSUES_STATUS, Map.of("reason", "The required 'dbfile' configuration is missing in solr-statistics.cfg!")) ))); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/link/search/HealthIndicatorMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/link/search/HealthIndicatorMatcher.java index d2ee1e2c70ab..5bd0d972d9c5 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/link/search/HealthIndicatorMatcher.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/link/search/HealthIndicatorMatcher.java @@ -28,7 +28,7 @@ private HealthIndicatorMatcher() { } - public static Matcher matchDb(Status status) { + public static Matcher matchDatabase(Status status) { return allOf( hasJsonPath("$.db"), hasJsonPath("$.db.status", is(status.getCode())), diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index fcec7471cc0a..fed722461a48 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -1578,6 +1578,7 @@ request.item.helpdesk.override = false management.endpoint.health.show-details = when-authorized management.endpoint.health.roles = ADMIN +management.endpoint.health.status.order= down, out-of-service, up-with-issues, up, unknown management.health.ping.enabled = false management.health.diskSpace.enabled = false From ba7750a8a097297033268641ed1ac838ba6adeb9 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Fri, 25 Mar 2022 11:24:31 +0100 Subject: [PATCH 0065/1846] [CST-5288] Improved info actuator --- dspace/config/dspace.cfg | 4 +--- dspace/config/modules/info-actuator.cfg | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 dspace/config/modules/info-actuator.cfg diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index fed722461a48..4c0d04dbff9d 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -1583,9 +1583,6 @@ management.endpoint.health.status.order= down, out-of-service, up-with-issues, u management.health.ping.enabled = false management.health.diskSpace.enabled = false -info.app.name = ${dspace.name} - - #------------------------------------------------------------------# #-------------------MODULE CONFIGURATIONS--------------------------# #------------------------------------------------------------------# @@ -1631,6 +1628,7 @@ include = ${module_dir}/openaire-client.cfg include = ${module_dir}/rdf.cfg include = ${module_dir}/rest.cfg include = ${module_dir}/iiif.cfg +include = ${module_dir}/info-actuator.cfg include = ${module_dir}/solr-statistics.cfg include = ${module_dir}/solrauthority.cfg include = ${module_dir}/spring.cfg diff --git a/dspace/config/modules/info-actuator.cfg b/dspace/config/modules/info-actuator.cfg new file mode 100644 index 000000000000..9d0617ad8fa3 --- /dev/null +++ b/dspace/config/modules/info-actuator.cfg @@ -0,0 +1,19 @@ + +info.app.name = ${dspace.name} +info.app.version = 7.3 + +info.app.dir = ${dspace.dir} +info.app.url = ${dspace.server.url} +info.app.db = ${db.url} +info.app.solr.server = ${solr.server} +info.app.solr.prefix = ${solr.multicorePrefix} +info.app.mail.server = ${mail.server} +info.app.mail.from-address = ${mail.from.address} +info.app.mail.feedback-recipient = ${feedback.recipient} +info.app.mail.mail-admin = ${mail.admin} +info.app.mail.mail-helpdesk = ${mail.helpdesk} +info.app.mail.alert-recipient = ${alert.recipient} + +info.app.cors.allowed-origins = ${rest.cors.allowed-origins} + +info.app.ui.url = ${dspace.ui.url} From a24f75cc082ed7fc4af7d234159e087d4dded562 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Fri, 25 Mar 2022 11:44:24 +0100 Subject: [PATCH 0066/1846] [CST-5288] Fixed GeoIpHealthIndicatorTest --- .../org/dspace/app/rest/health/GeoIpHealthIndicatorTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/health/GeoIpHealthIndicatorTest.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/health/GeoIpHealthIndicatorTest.java index ba454a8f1eea..d47493f23c93 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/health/GeoIpHealthIndicatorTest.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/health/GeoIpHealthIndicatorTest.java @@ -15,6 +15,7 @@ import java.util.Map; import com.maxmind.geoip2.DatabaseReader; +import org.dspace.app.rest.configuration.ActuatorConfiguration; import org.dspace.statistics.GeoIpService; import org.junit.Test; import org.junit.runner.RunWith; @@ -58,7 +59,7 @@ public void testWithGeoIpWrongConfiguration() { Health health = geoIpHealthIndicator.health(); - assertThat(health.getStatus(), is(Status.OUT_OF_SERVICE)); + assertThat(health.getStatus(), is(ActuatorConfiguration.UP_WITH_ISSUES_STATUS)); assertThat(health.getDetails(), is(Map.of("reason", "Missing db file"))); } From bf7ef19322c92424ca0c7a52692998900c0015a3 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Fri, 25 Mar 2022 13:08:15 +0100 Subject: [PATCH 0067/1846] [CST-5288] Disabled solr oai core health indicator during tests --- dspace-api/src/test/data/dspaceFolder/config/local.cfg | 2 ++ .../src/test/java/org/dspace/app/rest/HealthIndicatorsIT.java | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/test/data/dspaceFolder/config/local.cfg b/dspace-api/src/test/data/dspaceFolder/config/local.cfg index 3c19a68e9fd1..bca7fc81bfea 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/local.cfg +++ b/dspace-api/src/test/data/dspaceFolder/config/local.cfg @@ -144,3 +144,5 @@ authentication-ip.Student = 6.6.6.6 useProxies = true proxies.trusted.ipranges = 7.7.7.7 proxies.trusted.include_ui_ip = true + +management.health.solrOai.enabled = false diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/HealthIndicatorsIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/HealthIndicatorsIT.java index 4c63cc1da81a..8c1c534de14c 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/HealthIndicatorsIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/HealthIndicatorsIT.java @@ -65,7 +65,6 @@ public void testWithAdminUser() throws Exception { .andExpect(jsonPath("$.components", allOf( matchDatabase(Status.UP), match("solrSearchCore", Status.UP, Map.of("status", 0, "detectedPathType", "root")), - match("solrOaiCore", Status.UP, Map.of("status", 0, "detectedPathType", "particular core")), match("solrStatisticsCore", Status.UP, Map.of("status", 0, "detectedPathType", "root")), match("geoIp", UP_WITH_ISSUES_STATUS, Map.of("reason", "The required 'dbfile' configuration is missing in solr-statistics.cfg!")) From af21d54f97137ec5c3ea831286a04c13cf1a27f1 Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Fri, 25 Mar 2022 15:42:17 +0100 Subject: [PATCH 0068/1846] [CST-5303] implemented http live import client --- .../scopus/service/LiveImportClient.java | 22 +++++ .../scopus/service/LiveImportClientImpl.java | 94 +++++++++++++++++++ .../config/spring/api/external-services.xml | 2 + 3 files changed, 118 insertions(+) create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/scopus/service/LiveImportClient.java create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/scopus/service/LiveImportClientImpl.java diff --git a/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/LiveImportClient.java b/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/LiveImportClient.java new file mode 100644 index 000000000000..50006fd486cd --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/LiveImportClient.java @@ -0,0 +1,22 @@ +/** + * 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.importer.external.scopus.service; + +import java.io.InputStream; +import java.util.Map; + +/** + * Interface for classes that allow to contact LiveImport clients. + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com) + */ +public interface LiveImportClient { + + public InputStream executeHttpGetRequest(int timeout, String URL, Map requestParams); + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/LiveImportClientImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/LiveImportClientImpl.java new file mode 100644 index 000000000000..f11e2fc4f207 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/LiveImportClientImpl.java @@ -0,0 +1,94 @@ +/** + * 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.importer.external.scopus.service; + +import java.io.InputStream; +import java.net.URISyntaxException; +import java.util.Map; +import java.util.Objects; + +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpHost; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.config.RequestConfig.Builder; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.DefaultProxyRoutePlanner; +import org.apache.log4j.Logger; +import org.dspace.services.ConfigurationService; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Implementation of {@link LiveImportClient}. + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science dot com) + */ +public class LiveImportClientImpl implements LiveImportClient { + + private static final Logger log = Logger.getLogger(LiveImportClientImpl.class); + + @Autowired + private ConfigurationService configurationService; + + @Override + public InputStream executeHttpGetRequest(int timeout, String URL, Map requestParams) { + HttpGet method = null; + String proxyHost = configurationService.getProperty("http.proxy.host"); + String proxyPort = configurationService.getProperty("http.proxy.port"); + try { + HttpClientBuilder hcBuilder = HttpClients.custom(); + Builder requestConfigBuilder = RequestConfig.custom(); + requestConfigBuilder.setConnectionRequestTimeout(timeout); + + if (StringUtils.isNotBlank(proxyHost) && StringUtils.isNotBlank(proxyPort)) { + HttpHost proxy = new HttpHost(proxyHost, Integer.parseInt(proxyPort), "http"); + DefaultProxyRoutePlanner routePlanner = new DefaultProxyRoutePlanner(proxy); + hcBuilder.setRoutePlanner(routePlanner); + } + + method = new HttpGet(getSearchUrl(URL, requestParams)); + method.setConfig(requestConfigBuilder.build()); + + HttpClient client = hcBuilder.build(); + HttpResponse httpResponse = client.execute(method); + if (isNotSuccessfull(httpResponse)) { + throw new RuntimeException(); + } + return httpResponse.getEntity().getContent(); + } catch (Exception e1) { + log.error(e1.getMessage(), e1); + } finally { + if (Objects.nonNull(method)) { + method.releaseConnection(); + } + } + return null; + } + + private String getSearchUrl(String URL, Map requestParams) throws URISyntaxException { + URIBuilder uriBuilder = new URIBuilder(URL); + for (String param : requestParams.keySet()) { + uriBuilder.setParameter(param, requestParams.get(param)); + } + return uriBuilder.toString(); + } + + private boolean isNotSuccessfull(HttpResponse response) { + int statusCode = getStatusCode(response); + return statusCode < 200 || statusCode > 299; + } + + private int getStatusCode(HttpResponse response) { + return response.getStatusLine().getStatusCode(); + } + +} \ No newline at end of file diff --git a/dspace/config/spring/api/external-services.xml b/dspace/config/spring/api/external-services.xml index 9e28e5d55985..7f1295f839df 100644 --- a/dspace/config/spring/api/external-services.xml +++ b/dspace/config/spring/api/external-services.xml @@ -5,6 +5,8 @@ + + From 97a9c705790813627fbe31734bd47d6e0f6d9da4 Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Fri, 25 Mar 2022 19:43:53 +0100 Subject: [PATCH 0069/1846] 88599: WIP: Fix place algorithm --- .../content/RelationshipServiceImpl.java | 41 ++++++++++++++----- .../DefaultItemVersionProvider.java | 12 +----- .../VersioningWithRelationshipsTest.java | 12 +++--- 3 files changed, 39 insertions(+), 26 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/RelationshipServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/RelationshipServiceImpl.java index 70333530ca8a..644253aa9b0a 100644 --- a/dspace-api/src/main/java/org/dspace/content/RelationshipServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/RelationshipServiceImpl.java @@ -227,10 +227,10 @@ private void updatePlaceInRelationship( Item rightItem = relationship.getRightItem(); List leftRelationships = findByItemAndRelationshipType( - context, leftItem, relationship.getRelationshipType(), true + context, leftItem, relationship.getRelationshipType(), true, -1, -1, false ); List rightRelationships = findByItemAndRelationshipType( - context, rightItem, relationship.getRelationshipType(), false + context, rightItem, relationship.getRelationshipType(), false, -1, -1, false ); // These relationships are only deleted from the temporary lists in case they're present in them so that we can @@ -250,6 +250,7 @@ private void updatePlaceInRelationship( int oldLeftPlace = relationship.getLeftPlace(); int oldRightPlace = relationship.getRightPlace(); + boolean movedUpLeft = resolveRelationshipPlace( relationship, true, leftRelationships, leftMetadata, oldLeftPlace, newLeftPlace ); @@ -259,14 +260,18 @@ private void updatePlaceInRelationship( context.turnOffAuthorisationSystem(); - shiftSiblings( - relationship, true, oldLeftPlace, movedUpLeft, insertLeft, deletedFromLeft, - leftRelationships, leftMetadata - ); - shiftSiblings( - relationship, false, oldRightPlace, movedUpRight, insertRight, deletedFromRight, - rightRelationships, rightMetadata - ); + if (otherSideIsLatest(true, relationship.getLatestVersionStatus())) { + shiftSiblings( + relationship, true, oldLeftPlace, movedUpLeft, insertLeft, deletedFromLeft, + leftRelationships, leftMetadata + ); + } + if (otherSideIsLatest(false, relationship.getLatestVersionStatus())) { + shiftSiblings( + relationship, false, oldRightPlace, movedUpRight, insertRight, deletedFromRight, + rightRelationships, rightMetadata + ); + } updateItem(context, leftItem); updateItem(context, rightItem); @@ -474,6 +479,22 @@ private void shiftSiblings( } } + /** + * Given a latest version status, check if the other side is "latest". + * If we look from the left, this implies BOTH and RIGHT_ONLY return true. + * If we look from the right, this implies BOTH and LEFT_ONLY return true. + * @param isLeft whether we should look from the left or right side. + * @param latestVersionStatus the latest version status. + * @return true if the other side has "latest" status, false otherwise. + */ + private boolean otherSideIsLatest(boolean isLeft, LatestVersionStatus latestVersionStatus) { + if (latestVersionStatus == LatestVersionStatus.BOTH) { + return true; + } + + return latestVersionStatus == (isLeft ? LatestVersionStatus.RIGHT_ONLY : LatestVersionStatus.LEFT_ONLY); + } + private int getPlace(Relationship relationship, boolean isLeft) { if (isLeft) { return relationship.getLeftPlace(); diff --git a/dspace-api/src/main/java/org/dspace/versioning/DefaultItemVersionProvider.java b/dspace-api/src/main/java/org/dspace/versioning/DefaultItemVersionProvider.java index e12acfca33c3..d4590ae24ea2 100644 --- a/dspace-api/src/main/java/org/dspace/versioning/DefaultItemVersionProvider.java +++ b/dspace-api/src/main/java/org/dspace/versioning/DefaultItemVersionProvider.java @@ -148,12 +148,8 @@ protected void copyRelationships( newItem, // new item oldRelationship.getRightItem(), oldRelationship.getRelationshipType(), - // NOTE: on the side of the new version, we start with an empty list of relationships - // => insert at the same position as the ancestral relationship oldRelationship.getLeftPlace(), - // NOTE: on the opposite side of the new version, the ancestral relationship already takes our - // desired place => insert AFTER the ancestral relationship - oldRelationship.getRightPlace() + 1, + oldRelationship.getRightPlace(), oldRelationship.getLeftwardValue(), oldRelationship.getRightwardValue(), Relationship.LatestVersionStatus.RIGHT_ONLY // only mark the opposite side as "latest" for now @@ -165,11 +161,7 @@ protected void copyRelationships( oldRelationship.getLeftItem(), newItem, // new item oldRelationship.getRelationshipType(), - // NOTE: on the opposite side of the new version, the ancestral relationship already takes our - // desired place => insert AFTER the ancestral relationship - oldRelationship.getLeftPlace() + 1, - // NOTE: on the side of the new version, we start with an empty list of relationships - // => insert at the same position as the ancestral relationship + oldRelationship.getLeftPlace(), oldRelationship.getRightPlace(), oldRelationship.getLeftwardValue(), oldRelationship.getRightwardValue(), diff --git a/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java b/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java index 96cf432ab9d5..dc4905dc29be 100644 --- a/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java +++ b/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java @@ -156,7 +156,7 @@ protected Matcher isRel( } @Test - public void test_createNewVersionOfItemOnLeftSideOfRelationships() throws Exception { + public void test_createNewVersionOfItemOnLeftSideOfRelationships() throws Exception { // TODO /////////////////////////////////////////////// // create a publication with 3 relationships // /////////////////////////////////////////////// @@ -430,7 +430,7 @@ public void test_createNewVersionOfItemOnLeftSideOfRelationships() throws Except } @Test - public void test_createNewVersionOfItemAndModifyRelationships() throws Exception { + public void test_createNewVersionOfItemAndModifyRelationships() throws Exception { // TODO /////////////////////////////////////////////// // create a publication with 3 relationships // /////////////////////////////////////////////// @@ -792,7 +792,7 @@ public void test_createNewVersionOfItemAndModifyRelationships() throws Exception } @Test - public void test_createNewVersionOfItemOnRightSideOfRelationships() throws Exception { + public void test_createNewVersionOfItemOnRightSideOfRelationships() throws Exception { // TODO ////////////////////////////////////////// // create a person with 3 relationships // ////////////////////////////////////////// @@ -1066,7 +1066,7 @@ public void test_createNewVersionOfItemOnRightSideOfRelationships() throws Excep } @Test - public void test_createNewVersionOfItemAndVerifyMetadataOrder() throws Exception { + public void test_createNewVersionOfItemAndVerifyMetadataOrder() throws Exception { // TODO ///////////////////////////////////////// // create a publication with 6 authors // ///////////////////////////////////////// @@ -1341,7 +1341,7 @@ public void test_createNewVersionOfItemAndVerifyMetadataOrder() throws Exception } @Test - public void test_virtualMetadataPreserved() throws Exception { + public void test_virtualMetadataPreserved() throws Exception { // TODO ////////////////////////////////////////////// // create a publication and link two people // ////////////////////////////////////////////// @@ -1477,7 +1477,7 @@ public void test_virtualMetadataPreserved() throws Exception { assertEquals(2, mdvs5.size()); assertTrue(mdvs5.get(0) instanceof RelationshipMetadataValue); - assertEquals("Smith, D.", mdvs5.get(0).getValue()); + assertEquals("Smith, D.", mdvs5.get(0).getValue());// TODO fix assertEquals(0, mdvs5.get(0).getPlace()); assertTrue(mdvs5.get(1) instanceof RelationshipMetadataValue); From 758b02f65c92f00954b7db2326cb46aac6bc6b22 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Fri, 25 Mar 2022 17:00:04 -0400 Subject: [PATCH 0070/1846] [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 0071/1846] [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 0072/1846] [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 2f437b701815623bee4ac954a71fb3d600562574 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Tue, 29 Mar 2022 16:05:48 -0400 Subject: [PATCH 0073/1846] [DS-4300] Refactor for testability. --- .../org/dspace/administer/StructBuilder.java | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/administer/StructBuilder.java b/dspace-api/src/main/java/org/dspace/administer/StructBuilder.java index fcf6fd0c23fd..6e69f93572f1 100644 --- a/dspace-api/src/main/java/org/dspace/administer/StructBuilder.java +++ b/dspace-api/src/main/java/org/dspace/administer/StructBuilder.java @@ -117,8 +117,6 @@ public class StructBuilder { protected static final HandleService handleService = HandleServiceFactory.getInstance().getHandleService(); - private static boolean keepHandles; - /** * Default constructor */ @@ -229,8 +227,8 @@ public static void main(String[] argv) inputStream = new FileInputStream(input); } - keepHandles = options.hasOption("k"); - importStructure(context, inputStream, outputStream); + boolean keepHandles = options.hasOption("k"); + importStructure(context, inputStream, outputStream, keepHandles); // save changes from import context.complete(); } @@ -243,6 +241,7 @@ public static void main(String[] argv) * @param context * @param input XML which describes the new communities and collections. * @param output input, annotated with the new objects' identifiers. + * @param keepHandles true if Handles should be set from input. * @throws IOException * @throws ParserConfigurationException * @throws SAXException @@ -250,7 +249,7 @@ public static void main(String[] argv) * @throws SQLException */ static void importStructure(Context context, InputStream input, - OutputStream output) + OutputStream output, boolean keepHandles) throws IOException, ParserConfigurationException, SQLException, TransformerException { @@ -314,7 +313,7 @@ static void importStructure(Context context, InputStream input, NodeList first = XPathAPI.selectNodeList(document, "/import_structure/community"); // run the import starting with the top level communities - elements = handleCommunities(context, first, null); + elements = handleCommunities(context, first, null, keepHandles); } catch (TransformerException ex) { System.err.format("Input content not understood: %s%n", ex.getMessage()); System.exit(1); @@ -633,10 +632,12 @@ private static String getStringValue(Node node) { * @param context the context of the request * @param communities a nodelist of communities to create along with their sub-structures * @param parent the parent community of the nodelist of communities to create + * @param keepHandles use Handles from input. * @return an element array containing additional information regarding the * created communities (e.g. the handles they have been assigned) */ - private static Element[] handleCommunities(Context context, NodeList communities, Community parent) + private static Element[] handleCommunities(Context context, NodeList communities, + Community parent, boolean keepHandles) throws TransformerException, SQLException, AuthorizeException { Element[] elements = new Element[communities.getLength()]; @@ -728,11 +729,13 @@ private static Element[] handleCommunities(Context context, NodeList communities // handle sub communities NodeList subCommunities = XPathAPI.selectNodeList(tn, "community"); - Element[] subCommunityElements = handleCommunities(context, subCommunities, community); + Element[] subCommunityElements = handleCommunities(context, + subCommunities, community, keepHandles); // handle collections NodeList collections = XPathAPI.selectNodeList(tn, "collection"); - Element[] collectionElements = handleCollections(context, collections, community); + Element[] collectionElements = handleCollections(context, + collections, community, keepHandles); int j; for (j = 0; j < subCommunityElements.length; j++) { @@ -757,7 +760,8 @@ private static Element[] handleCommunities(Context context, NodeList communities * @return an Element array containing additional information about the * created collections (e.g. the handle) */ - private static Element[] handleCollections(Context context, NodeList collections, Community parent) + private static Element[] handleCollections(Context context, + NodeList collections, Community parent, boolean keepHandles) throws TransformerException, SQLException, AuthorizeException { Element[] elements = new Element[collections.getLength()]; From 0e98044e95b0592e4b0501c006c7541fb756840a Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Tue, 29 Mar 2022 16:06:22 -0400 Subject: [PATCH 0074/1846] [DS-4300] Test with specified handles enabled. --- .../dspace/administer/StructBuilderIT.java | 89 ++++++++++++++++--- 1 file changed, 76 insertions(+), 13 deletions(-) diff --git a/dspace-api/src/test/java/org/dspace/administer/StructBuilderIT.java b/dspace-api/src/test/java/org/dspace/administer/StructBuilderIT.java index 7abe3618ed5a..5bd6144fbef7 100644 --- a/dspace-api/src/test/java/org/dspace/administer/StructBuilderIT.java +++ b/dspace-api/src/test/java/org/dspace/administer/StructBuilderIT.java @@ -8,6 +8,7 @@ package org.dspace.administer; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -18,9 +19,10 @@ import java.util.Iterator; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.Source; -import javax.xml.transform.TransformerException; import javax.xml.transform.stream.StreamSource; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.AbstractIntegrationTest; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Collection; @@ -29,13 +31,12 @@ import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.service.CollectionService; import org.dspace.content.service.CommunityService; +import org.dspace.handle.Handle; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.w3c.dom.Attr; import org.w3c.dom.Node; import org.xml.sax.SAXException; @@ -53,7 +54,7 @@ */ public class StructBuilderIT extends AbstractIntegrationTest { - private static final Logger log = LoggerFactory.getLogger(StructBuilderIT.class); + private static final Logger log = LogManager.getLogger(); private static final CommunityService communityService = ContentServiceFactory.getInstance().getCommunityService(); @@ -93,23 +94,24 @@ public void setUp() throws SQLException, AuthorizeException, IOException { public void tearDown() { } + private static final String COMMUNITY_1_HANDLE = "https://hdl.handle.net/1/1"; /** Test structure document. */ private static final String IMPORT_DOCUMENT = "\n" + "\n" + - " \n" + + " \n" + " Top Community 0\n" + " A top level community\n" + " Testing 1 2 3\n" + " 1969\n" + " A sidebar\n" + - " \n" + + " \n" + " Sub Community 0.0\n" + " A sub community\n" + " Live from New York....\n" + " 1957\n" + " Another sidebar\n" + - " \n" + + " \n" + " Collection 0.0.0\n" + " A collection\n" + " Our next guest needs no introduction\n" + @@ -119,7 +121,7 @@ public void tearDown() { " Testing\n" + " \n" + " \n" + - " \n" + + " \n" + " Collection 0.1\n" + " Another collection\n" + " Fourscore and seven years ago\n" + @@ -160,15 +162,76 @@ public void testImportStructure() byte[] inputBytes = IMPORT_DOCUMENT.getBytes(StandardCharsets.UTF_8); context.turnOffAuthorisationSystem(); try (InputStream input = new ByteArrayInputStream(inputBytes);) { - StructBuilder.importStructure(context, input, outputDocument); - } catch (IOException | SQLException - | ParserConfigurationException | TransformerException ex) { - System.err.println(ex.getMessage()); - System.exit(1); + StructBuilder.importStructure(context, input, outputDocument, false); + } finally { + context.restoreAuthSystemState(); + } + + // Compare import's output with its input. + // N.B. here we rely on StructBuilder to emit communities and + // collections in the same order as the input document. If that changes, + // we will need a smarter NodeMatcher, probably based on children. + Source output = new StreamSource( + new ByteArrayInputStream(outputDocument.toByteArray())); + Source reference = new StreamSource( + new ByteArrayInputStream( + IMPORT_DOCUMENT.getBytes(StandardCharsets.UTF_8))); + Diff myDiff = DiffBuilder.compare(reference).withTest(output) + .normalizeWhitespace() +// .withNodeFilter(new MyNodeFilter()) + .withAttributeFilter((Attr attr) -> + !attr.getName().equals("identifier")) + .checkForIdentical() + .build(); + + // Was there a difference? + // Always output differences -- one is expected. + ComparisonFormatter formatter = new DefaultComparisonFormatter(); + for (Difference difference : myDiff.getDifferences()) { + System.err.println(difference.toString(formatter)); + } + // Test for *significant* differences. + assertFalse("Output does not match input.", isDifferent(myDiff)); + + // TODO spot-check some objects. + } + + /** + * Test of importStructure method, with given Handles. + * + * @throws java.lang.Exception passed through. + */ + @Test + public void testImportStructureWithHandles() + throws Exception { + System.out.println("importStructure"); + + // Run the method under test and collect its output. + ByteArrayOutputStream outputDocument + = new ByteArrayOutputStream(IMPORT_DOCUMENT.length() * 2 * 2); + byte[] inputBytes = IMPORT_DOCUMENT.getBytes(StandardCharsets.UTF_8); + context.turnOffAuthorisationSystem(); + try (InputStream input = new ByteArrayInputStream(inputBytes);) { + StructBuilder.importStructure(context, input, outputDocument, true); } finally { context.restoreAuthSystemState(); } + // Check a chosen object for the right Handle. + boolean found = false; + for (Community community : communityService.findAllTop(context)) { + for (Handle handle : community.getHandles()) { + if (handle.getHandle().equals(COMMUNITY_1_HANDLE)) { + found = true; + break; + } + if (found) { + break; + } + } + } + assertTrue("A community should have its specified handle", found); + // Compare import's output with its input. // N.B. here we rely on StructBuilder to emit communities and // collections in the same order as the input document. If that changes, From c4c8a4bc07dd08c49dfd63a8c70ced39dd1a7972 Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Wed, 30 Mar 2022 02:15:32 +0200 Subject: [PATCH 0075/1846] 88599: Update expected places --- .../VersioningWithRelationshipsTest.java | 106 +++++++++--------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java b/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java index dc4905dc29be..f375917fa956 100644 --- a/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java +++ b/dspace-api/src/test/java/org/dspace/content/VersioningWithRelationshipsTest.java @@ -156,7 +156,7 @@ protected Matcher isRel( } @Test - public void test_createNewVersionOfItemOnLeftSideOfRelationships() throws Exception { // TODO + public void test_createNewVersionOfItemOnLeftSideOfRelationships() throws Exception { /////////////////////////////////////////////// // create a publication with 3 relationships // /////////////////////////////////////////////// @@ -269,9 +269,9 @@ public void test_createNewVersionOfItemOnLeftSideOfRelationships() throws Except assertThat( relationshipService.findByItem(context, newPublication, -1, -1, false, true), containsInAnyOrder(List.of( - isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY, 0, 1), - isRel(newPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY, 0, 1), - isRel(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY, 0, 1) + isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY, 0, 0), + isRel(newPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY, 0, 0), + isRel(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY, 0, 0) )) ); @@ -292,7 +292,7 @@ public void test_createNewVersionOfItemOnLeftSideOfRelationships() throws Except relationshipService.findByItem(context, person1, -1, -1, false, false), containsInAnyOrder(List.of( isRel(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 0), - isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY, 0, 1) + isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY, 0, 0) )) ); @@ -300,7 +300,7 @@ public void test_createNewVersionOfItemOnLeftSideOfRelationships() throws Except relationshipService.findByItem(context, project1, -1, -1, false, false), containsInAnyOrder(List.of( isRel(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH, 0, 0), - isRel(newPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY, 0, 1) + isRel(newPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY, 0, 0) )) ); @@ -308,16 +308,16 @@ public void test_createNewVersionOfItemOnLeftSideOfRelationships() throws Except relationshipService.findByItem(context, orgUnit1, -1, -1, false, false), containsInAnyOrder(List.of( isRel(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH, 0, 0), - isRel(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY, 0, 1) + isRel(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY, 0, 0) )) ); assertThat( relationshipService.findByItem(context, newPublication, -1, -1, false, false), containsInAnyOrder(List.of( - isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY, 0, 1), - isRel(newPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY, 0, 1), - isRel(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY, 0, 1) + isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY, 0, 0), + isRel(newPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY, 0, 0), + isRel(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY, 0, 0) )) ); @@ -345,30 +345,30 @@ public void test_createNewVersionOfItemOnLeftSideOfRelationships() throws Except assertThat( relationshipService.findByItem(context, person1, -1, -1, false, true), containsInAnyOrder(List.of( - isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 1) + isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, project1, -1, -1, false, true), containsInAnyOrder(List.of( - isRel(newPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH, 0, 1) + isRel(newPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, orgUnit1, -1, -1, false, true), containsInAnyOrder(List.of( - isRel(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH, 0, 1) + isRel(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, newPublication, -1, -1, false, true), containsInAnyOrder(List.of( - isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 1), - isRel(newPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH, 0, 1), - isRel(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH, 0, 1) + isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 0), + isRel(newPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH, 0, 0), + isRel(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH, 0, 0) )) ); @@ -389,7 +389,7 @@ public void test_createNewVersionOfItemOnLeftSideOfRelationships() throws Except relationshipService.findByItem(context, person1, -1, -1, false, false), containsInAnyOrder(List.of( isRel(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY, 0, 0), - isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 1) + isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 0) )) ); @@ -397,7 +397,7 @@ public void test_createNewVersionOfItemOnLeftSideOfRelationships() throws Except relationshipService.findByItem(context, project1, -1, -1, false, false), containsInAnyOrder(List.of( isRel(originalPublication, isProjectOfPublication, project1, LatestVersionStatus.RIGHT_ONLY, 0, 0), - isRel(newPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH, 0, 1) + isRel(newPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH, 0, 0) )) ); @@ -405,16 +405,16 @@ public void test_createNewVersionOfItemOnLeftSideOfRelationships() throws Except relationshipService.findByItem(context, orgUnit1, -1, -1, false, false), containsInAnyOrder(List.of( isRel(originalPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.RIGHT_ONLY, 0, 0), - isRel(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH, 0, 1) + isRel(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, newPublication, -1, -1, false, false), containsInAnyOrder(List.of( - isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 1), - isRel(newPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH, 0, 1), - isRel(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH, 0, 1) + isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 0), + isRel(newPublication, isProjectOfPublication, project1, LatestVersionStatus.BOTH, 0, 0), + isRel(newPublication, isOrgUnitOfPublication, orgUnit1, LatestVersionStatus.BOTH, 0, 0) )) ); @@ -430,7 +430,7 @@ public void test_createNewVersionOfItemOnLeftSideOfRelationships() throws Except } @Test - public void test_createNewVersionOfItemAndModifyRelationships() throws Exception { // TODO + public void test_createNewVersionOfItemAndModifyRelationships() throws Exception { /////////////////////////////////////////////// // create a publication with 3 relationships // /////////////////////////////////////////////// @@ -593,7 +593,7 @@ public void test_createNewVersionOfItemAndModifyRelationships() throws Exception assertThat( relationshipService.findByItem(context, newPublication, -1, -1, false, true), containsInAnyOrder(List.of( - isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY, 0, 1), + isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY, 0, 0), // NOTE: BOTH because new relationship isRel(newPublication, isAuthorOfPublication, person2, LatestVersionStatus.BOTH, 1, 0), isRel(newPublication, isOrgUnitOfPublication, orgUnit2, LatestVersionStatus.BOTH, 0, 0) @@ -617,7 +617,7 @@ public void test_createNewVersionOfItemAndModifyRelationships() throws Exception relationshipService.findByItem(context, person1, -1, -1, false, false), containsInAnyOrder(List.of( isRel(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 0), - isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY, 0, 1) + isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY, 0, 0) )) ); @@ -654,7 +654,7 @@ public void test_createNewVersionOfItemAndModifyRelationships() throws Exception assertThat( relationshipService.findByItem(context, newPublication, -1, -1, false, false), containsInAnyOrder(List.of( - isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY, 0, 1), + isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY, 0, 0), // NOTE: BOTH because new relationship isRel(newPublication, isAuthorOfPublication, person2, LatestVersionStatus.BOTH, 1, 0), isRel(newPublication, isOrgUnitOfPublication, orgUnit2, LatestVersionStatus.BOTH, 0, 0) @@ -685,7 +685,7 @@ public void test_createNewVersionOfItemAndModifyRelationships() throws Exception assertThat( relationshipService.findByItem(context, person1, -1, -1, false, true), containsInAnyOrder(List.of( - isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 1) + isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 0) )) ); @@ -716,7 +716,7 @@ public void test_createNewVersionOfItemAndModifyRelationships() throws Exception assertThat( relationshipService.findByItem(context, newPublication, -1, -1, false, true), containsInAnyOrder(List.of( - isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 1), + isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 0), isRel(newPublication, isAuthorOfPublication, person2, LatestVersionStatus.BOTH, 1, 0), isRel(newPublication, isOrgUnitOfPublication, orgUnit2, LatestVersionStatus.BOTH, 0, 0) )) @@ -739,7 +739,7 @@ public void test_createNewVersionOfItemAndModifyRelationships() throws Exception relationshipService.findByItem(context, person1, -1, -1, false, false), containsInAnyOrder(List.of( isRel(originalPublication, isAuthorOfPublication, person1, LatestVersionStatus.RIGHT_ONLY, 0, 0), - isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 1) + isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 0) )) ); @@ -774,7 +774,7 @@ public void test_createNewVersionOfItemAndModifyRelationships() throws Exception assertThat( relationshipService.findByItem(context, newPublication, -1, -1, false, false), containsInAnyOrder(List.of( - isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 1), + isRel(newPublication, isAuthorOfPublication, person1, LatestVersionStatus.BOTH, 0, 0), isRel(newPublication, isAuthorOfPublication, person2, LatestVersionStatus.BOTH, 1, 0), isRel(newPublication, isOrgUnitOfPublication, orgUnit2, LatestVersionStatus.BOTH, 0, 0) )) @@ -792,7 +792,7 @@ public void test_createNewVersionOfItemAndModifyRelationships() throws Exception } @Test - public void test_createNewVersionOfItemOnRightSideOfRelationships() throws Exception { // TODO + public void test_createNewVersionOfItemOnRightSideOfRelationships() throws Exception { ////////////////////////////////////////// // create a person with 3 relationships // ////////////////////////////////////////// @@ -905,9 +905,9 @@ public void test_createNewVersionOfItemOnRightSideOfRelationships() throws Excep assertThat( relationshipService.findByItem(context, newPerson, -1, -1, false, true), containsInAnyOrder(List.of( - isRel(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.LEFT_ONLY, 1, 0), - isRel(project1, isMemberOfProject, newPerson, LatestVersionStatus.LEFT_ONLY, 1, 0), - isRel(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.LEFT_ONLY, 1, 0) + isRel(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.LEFT_ONLY, 0, 0), + isRel(project1, isMemberOfProject, newPerson, LatestVersionStatus.LEFT_ONLY, 0, 0), + isRel(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.LEFT_ONLY, 0, 0) )) ); @@ -928,7 +928,7 @@ public void test_createNewVersionOfItemOnRightSideOfRelationships() throws Excep relationshipService.findByItem(context, publication1, -1, -1, false, false), containsInAnyOrder(List.of( isRel(publication1, isAuthorOfPublication, originalPerson, LatestVersionStatus.BOTH, 0, 0), - isRel(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.LEFT_ONLY, 1, 0) + isRel(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.LEFT_ONLY, 0, 0) )) ); @@ -936,7 +936,7 @@ public void test_createNewVersionOfItemOnRightSideOfRelationships() throws Excep relationshipService.findByItem(context, project1, -1, -1, false, false), containsInAnyOrder(List.of( isRel(project1, isMemberOfProject, originalPerson, LatestVersionStatus.BOTH, 0, 0), - isRel(project1, isMemberOfProject, newPerson, LatestVersionStatus.LEFT_ONLY, 1, 0) + isRel(project1, isMemberOfProject, newPerson, LatestVersionStatus.LEFT_ONLY, 0, 0) )) ); @@ -944,16 +944,16 @@ public void test_createNewVersionOfItemOnRightSideOfRelationships() throws Excep relationshipService.findByItem(context, orgUnit1, -1, -1, false, false), containsInAnyOrder(List.of( isRel(orgUnit1, isMemberOfOrgUnit, originalPerson, LatestVersionStatus.BOTH, 0, 0), - isRel(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.LEFT_ONLY, 1, 0) + isRel(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.LEFT_ONLY, 0, 0) )) ); assertThat( relationshipService.findByItem(context, newPerson, -1, -1, false, false), containsInAnyOrder(List.of( - isRel(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.LEFT_ONLY, 1, 0), - isRel(project1, isMemberOfProject, newPerson, LatestVersionStatus.LEFT_ONLY, 1, 0), - isRel(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.LEFT_ONLY, 1, 0) + isRel(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.LEFT_ONLY, 0, 0), + isRel(project1, isMemberOfProject, newPerson, LatestVersionStatus.LEFT_ONLY, 0, 0), + isRel(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.LEFT_ONLY, 0, 0) )) ); @@ -981,30 +981,30 @@ public void test_createNewVersionOfItemOnRightSideOfRelationships() throws Excep assertThat( relationshipService.findByItem(context, publication1, -1, -1, false, true), containsInAnyOrder(List.of( - isRel(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.BOTH, 1, 0) + isRel(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, project1, -1, -1, false, true), containsInAnyOrder(List.of( - isRel(project1, isMemberOfProject, newPerson, LatestVersionStatus.BOTH, 1, 0) + isRel(project1, isMemberOfProject, newPerson, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, orgUnit1, -1, -1, false, true), containsInAnyOrder(List.of( - isRel(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.BOTH, 1, 0) + isRel(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, newPerson, -1, -1, false, true), containsInAnyOrder(List.of( - isRel(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.BOTH, 1, 0), - isRel(project1, isMemberOfProject, newPerson, LatestVersionStatus.BOTH, 1, 0), - isRel(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.BOTH, 1, 0) + isRel(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.BOTH, 0, 0), + isRel(project1, isMemberOfProject, newPerson, LatestVersionStatus.BOTH, 0, 0), + isRel(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.BOTH, 0, 0) )) ); @@ -1025,7 +1025,7 @@ public void test_createNewVersionOfItemOnRightSideOfRelationships() throws Excep relationshipService.findByItem(context, publication1, -1, -1, false, false), containsInAnyOrder(List.of( isRel(publication1, isAuthorOfPublication, originalPerson, LatestVersionStatus.LEFT_ONLY, 0, 0), - isRel(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.BOTH, 1, 0) + isRel(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.BOTH, 0, 0) )) ); @@ -1033,7 +1033,7 @@ public void test_createNewVersionOfItemOnRightSideOfRelationships() throws Excep relationshipService.findByItem(context, project1, -1, -1, false, false), containsInAnyOrder(List.of( isRel(project1, isMemberOfProject, originalPerson, LatestVersionStatus.LEFT_ONLY, 0, 0), - isRel(project1, isMemberOfProject, newPerson, LatestVersionStatus.BOTH, 1, 0) + isRel(project1, isMemberOfProject, newPerson, LatestVersionStatus.BOTH, 0, 0) )) ); @@ -1041,16 +1041,16 @@ public void test_createNewVersionOfItemOnRightSideOfRelationships() throws Excep relationshipService.findByItem(context, orgUnit1, -1, -1, false, false), containsInAnyOrder(List.of( isRel(orgUnit1, isMemberOfOrgUnit, originalPerson, LatestVersionStatus.LEFT_ONLY, 0, 0), - isRel(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.BOTH, 1, 0) + isRel(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.BOTH, 0, 0) )) ); assertThat( relationshipService.findByItem(context, newPerson, -1, -1, false, false), containsInAnyOrder(List.of( - isRel(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.BOTH, 1, 0), - isRel(project1, isMemberOfProject, newPerson, LatestVersionStatus.BOTH, 1, 0), - isRel(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.BOTH, 1, 0) + isRel(publication1, isAuthorOfPublication, newPerson, LatestVersionStatus.BOTH, 0, 0), + isRel(project1, isMemberOfProject, newPerson, LatestVersionStatus.BOTH, 0, 0), + isRel(orgUnit1, isMemberOfOrgUnit, newPerson, LatestVersionStatus.BOTH, 0, 0) )) ); @@ -1066,7 +1066,7 @@ public void test_createNewVersionOfItemOnRightSideOfRelationships() throws Excep } @Test - public void test_createNewVersionOfItemAndVerifyMetadataOrder() throws Exception { // TODO + public void test_createNewVersionOfItemAndVerifyMetadataOrder() throws Exception { ///////////////////////////////////////// // create a publication with 6 authors // ///////////////////////////////////////// From 6a1cdd6e2db4244e958e91ba210f37e2f2722168 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Wed, 30 Mar 2022 12:48:54 +0200 Subject: [PATCH 0076/1846] [CST-5306] Migrate Researcher Profile (REST) --- .../exception/ResourceConflictException.java | 39 + .../dspace/app/profile/ResearcherProfile.java | 85 ++ .../profile/ResearcherProfileServiceImpl.java | 312 +++++ .../AfterResearcherProfileCreationAction.java | 35 + .../service/ResearcherProfileService.java | 86 ++ .../authorize/AuthorizeServiceImpl.java | 4 + .../authorize/service/AuthorizeService.java | 1 + .../org/dspace/content/ItemServiceImpl.java | 48 + .../content/authority/EPersonAuthority.java | 93 ++ .../dspace/content/service/ItemService.java | 39 +- .../test/data/dspaceFolder/config/local.cfg | 2 +- .../org/dspace/app/matcher/LambdaMatcher.java | 55 + .../app/matcher/MetadataValueMatcher.java | 96 ++ .../java/org/dspace/builder/ItemBuilder.java | 18 + .../converter/ResearcherProfileConverter.java | 94 ++ .../DSpaceApiExceptionControllerAdvice.java | 17 + .../app/rest/login/PostLoggedInAction.java | 27 + .../rest/login/impl/LoginEventFireAction.java | 46 + .../impl/ResearcherProfileAutomaticClaim.java | 129 +++ .../app/rest/model/ResearcherProfileRest.java | 134 +++ .../hateoas/ResearcherProfileResource.java | 29 + ...esearcherProfileEPersonLinkRepository.java | 78 ++ .../ResearcherProfileItemLinkRepository.java | 67 ++ .../ResearcherProfileRestRepository.java | 189 +++ ...earcherProfileVisibleReplaceOperation.java | 67 ++ .../EPersonRestAuthenticationProvider.java | 18 + ...rProfileRestPermissionEvaluatorPlugin.java | 74 ++ .../ResearcherProfileRestRepositoryIT.java | 1032 +++++++++++++++++ .../app/rest/matcher/MetadataMatcher.java | 17 + dspace/config/modules/authority.cfg | 15 + dspace/config/registries/dspace-types.xml | 8 + dspace/config/spring/api/core-services.xml | 2 + .../spring/rest/post-logged-in-actions.xml | 15 + 33 files changed, 2969 insertions(+), 2 deletions(-) create mode 100644 dspace-api/src/main/java/org/dspace/app/exception/ResourceConflictException.java create mode 100644 dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfile.java create mode 100644 dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfileServiceImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/app/profile/service/AfterResearcherProfileCreationAction.java create mode 100644 dspace-api/src/main/java/org/dspace/app/profile/service/ResearcherProfileService.java create mode 100644 dspace-api/src/main/java/org/dspace/content/authority/EPersonAuthority.java create mode 100644 dspace-api/src/test/java/org/dspace/app/matcher/LambdaMatcher.java create mode 100644 dspace-api/src/test/java/org/dspace/app/matcher/MetadataValueMatcher.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ResearcherProfileConverter.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/login/PostLoggedInAction.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/login/impl/LoginEventFireAction.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/login/impl/ResearcherProfileAutomaticClaim.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ResearcherProfileRest.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/ResearcherProfileResource.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResearcherProfileEPersonLinkRepository.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResearcherProfileItemLinkRepository.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResearcherProfileRestRepository.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ResearcherProfileVisibleReplaceOperation.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResearcherProfileRestPermissionEvaluatorPlugin.java create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/ResearcherProfileRestRepositoryIT.java create mode 100644 dspace/config/modules/authority.cfg create mode 100644 dspace/config/spring/rest/post-logged-in-actions.xml diff --git a/dspace-api/src/main/java/org/dspace/app/exception/ResourceConflictException.java b/dspace-api/src/main/java/org/dspace/app/exception/ResourceConflictException.java new file mode 100644 index 000000000000..14e415aeb857 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/exception/ResourceConflictException.java @@ -0,0 +1,39 @@ +/** + * 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.exception; + +/** + * This class provides an exception to be used when a conflict on a resource + * occurs. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +public class ResourceConflictException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + private final Object resource; + + /** + * Create a ResourceConflictException with a message and the conflicting + * resource. + * + * @param message the error message + * @param resource the resource that caused the conflict + */ + public ResourceConflictException(String message, Object resource) { + super(message); + this.resource = resource; + } + + public Object getResource() { + return resource; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfile.java b/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfile.java new file mode 100644 index 000000000000..59f4a0cfff8b --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfile.java @@ -0,0 +1,85 @@ +/** + * 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.profile; + +import org.dspace.content.Item; +import org.dspace.content.MetadataValue; +import org.dspace.util.UUIDUtils; +import org.springframework.util.Assert; + +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Stream; + +import static org.dspace.core.Constants.READ; +import static org.dspace.eperson.Group.ANONYMOUS; + +/** + * Object representing a Researcher Profile. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +public class ResearcherProfile { + + private final Item item; + + private final MetadataValue dspaceObjectOwner; + + /** + * Create a new ResearcherProfile object from the given item. + * + * @param item the profile item + * @throws IllegalArgumentException if the given item has not a dspace.object.owner + * metadata with a valid authority + */ + public ResearcherProfile(Item item) { + Assert.notNull(item, "A researcher profile requires an item"); + this.item = item; + this.dspaceObjectOwner = getDspaceObjectOwnerMetadata(item); + } + + public UUID getId() { + return UUIDUtils.fromString(dspaceObjectOwner.getAuthority()); + } + + public String getFullName() { + return dspaceObjectOwner.getValue(); + } + + public boolean isVisible() { + return item.getResourcePolicies().stream() + .filter(policy -> policy.getGroup() != null) + .anyMatch(policy -> READ == policy.getAction() && ANONYMOUS.equals(policy.getGroup().getName())); + } + + public Item getItem() { + return item; + } + +// public Optional getOrcid() { +// return getMetadataValue(item, "person.identifier.orcid") +// .map(metadataValue -> metadataValue.getValue()); +// } + + private MetadataValue getDspaceObjectOwnerMetadata(Item item) { + return getMetadataValue(item, "dspace.object.owner") + .filter(metadata -> UUIDUtils.fromString(metadata.getAuthority()) != null) + .orElseThrow(() -> new IllegalArgumentException("A profile item must have a valid dspace.object.owner metadata")); + } + + private Optional getMetadataValue(Item item, String metadataField) { + return getMetadataValues(item, metadataField).findFirst(); + } + + private Stream getMetadataValues(Item item, String metadataField) { + return item.getMetadata().stream() + .filter(metadata -> metadataField.equals(metadata.getMetadataField().toString('.'))); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfileServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfileServiceImpl.java new file mode 100644 index 000000000000..8b9964972ce0 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfileServiceImpl.java @@ -0,0 +1,312 @@ +/** + * 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.profile; + +import static org.dspace.content.authority.Choices.CF_ACCEPTED; +import static org.dspace.core.Constants.READ; +import static org.dspace.eperson.Group.ANONYMOUS; + +import java.io.IOException; +import java.net.URI; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import javax.annotation.PostConstruct; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang.StringUtils; +import org.dspace.app.exception.ResourceConflictException; +import org.dspace.app.profile.service.AfterResearcherProfileCreationAction; +import org.dspace.app.profile.service.ResearcherProfileService; +import org.dspace.authorize.AuthorizeException; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.content.Collection; +import org.dspace.content.Item; +import org.dspace.content.MetadataValue; +import org.dspace.content.WorkspaceItem; +import org.dspace.content.service.CollectionService; +import org.dspace.content.service.InstallItemService; +import org.dspace.content.service.ItemService; +import org.dspace.content.service.WorkspaceItemService; +import org.dspace.core.Context; +import org.dspace.discovery.DiscoverQuery; +import org.dspace.discovery.DiscoverResult; +import org.dspace.discovery.IndexableObject; +import org.dspace.discovery.SearchService; +import org.dspace.discovery.SearchServiceException; +import org.dspace.discovery.indexobject.IndexableCollection; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.Group; +import org.dspace.eperson.service.GroupService; +import org.dspace.services.ConfigurationService; +import org.dspace.util.UUIDUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; + +/** + * Implementation of {@link ResearcherProfileService}. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +public class ResearcherProfileServiceImpl implements ResearcherProfileService { + + private static Logger log = LoggerFactory.getLogger(ResearcherProfileServiceImpl.class); + + @Autowired + private ItemService itemService; + + @Autowired + private WorkspaceItemService workspaceItemService; + + @Autowired + private InstallItemService installItemService; + + @Autowired + private ConfigurationService configurationService; + + @Autowired + private CollectionService collectionService; + + @Autowired + private SearchService searchService; + + @Autowired + private GroupService groupService; + + @Autowired + private AuthorizeService authorizeService; + + @Autowired(required = false) + private List afterCreationActions; + + @PostConstruct + public void postConstruct() { + + if (afterCreationActions == null) { + afterCreationActions = Collections.emptyList(); + } + + } + + @Override + public ResearcherProfile findById(Context context, UUID id) throws SQLException, AuthorizeException { + Assert.notNull(id, "An id must be provided to find a researcher profile"); + + Item profileItem = findResearcherProfileItemById(context, id); + if (profileItem == null) { + return null; + } + + return new ResearcherProfile(profileItem); + } + + @Override + public ResearcherProfile createAndReturn(Context context, EPerson ePerson) + throws AuthorizeException, SQLException, SearchServiceException { + + Item profileItem = findResearcherProfileItemById(context, ePerson.getID()); + if (profileItem != null) { + ResearcherProfile profile = new ResearcherProfile(profileItem); + throw new ResourceConflictException("A profile is already linked to the provided User", profile); + } + + Collection collection = findProfileCollection(context); + if (collection == null) { + throw new IllegalStateException("No collection found for researcher profiles"); + } + + context.turnOffAuthorisationSystem(); + Item item = createProfileItem(context, ePerson, collection); + context.restoreAuthSystemState(); + + ResearcherProfile researcherProfile = new ResearcherProfile(item); + + for (AfterResearcherProfileCreationAction afterCreationAction : afterCreationActions) { + afterCreationAction.perform(context, researcherProfile, ePerson); + } + + return researcherProfile; + } + + @Override + public void deleteById(Context context, UUID id) throws SQLException, AuthorizeException { + Assert.notNull(id, "An id must be provided to find a researcher profile"); + + Item profileItem = findResearcherProfileItemById(context, id); + if (profileItem == null) { + return; + } + + if (isHardDeleteEnabled()) { + deleteItem(context, profileItem); + } else { + removeDspaceObjectOwnerMetadata(context, profileItem); + } + + } + + @Override + public void changeVisibility(Context context, ResearcherProfile profile, boolean visible) + throws AuthorizeException, SQLException { + + if (profile.isVisible() == visible) { + return; + } + + Item item = profile.getItem(); + Group anonymous = groupService.findByName(context, ANONYMOUS); + + if (visible) { + authorizeService.addPolicy(context, item, READ, anonymous); + } else { + authorizeService.removeGroupPolicies(context, item, anonymous); + } + + } + + @Override + public ResearcherProfile claim(final Context context, final EPerson ePerson, final URI uri) + throws SQLException, AuthorizeException, SearchServiceException { + Item profileItem = findResearcherProfileItemById(context, ePerson.getID()); + if (profileItem != null) { + ResearcherProfile profile = new ResearcherProfile(profileItem); + throw new ResourceConflictException("A profile is already linked to the provided User", profile); + } + + Collection collection = findProfileCollection(context); + if (collection == null) { + throw new IllegalStateException("No collection found for researcher profiles"); + } + + final String path = uri.getPath(); + final UUID uuid = UUIDUtils.fromString(path.substring(path.lastIndexOf("/") + 1 )); + Item item = itemService.find(context, uuid); + if (Objects.isNull(item) || !item.isArchived() || item.isWithdrawn() || notClaimableEntityType(item)) { + throw new IllegalArgumentException("Provided uri does not represent a valid Item to be claimed"); + } + final String existingOwner = itemService.getMetadataFirstValue(item, "dspace", "object", + "owner", null); + + if (StringUtils.isNotBlank(existingOwner)) { + throw new IllegalArgumentException("Item with provided uri has already an owner"); + } + + context.turnOffAuthorisationSystem(); + itemService.addMetadata(context, item, "dspace", "object", "owner", null, ePerson.getName(), + ePerson.getID().toString(), CF_ACCEPTED); + + context.restoreAuthSystemState(); + return new ResearcherProfile(item); + } + + private boolean notClaimableEntityType(final Item item) { + final String entityType = itemService.getEntityType(item); + return Arrays.stream(configurationService.getArrayProperty("claimable.entityType")) + .noneMatch(entityType::equals); + } + + private Item findResearcherProfileItemById(Context context, UUID id) throws SQLException, AuthorizeException { + + String profileType = getProfileType(); + + Iterator items = itemService.findByAuthorityValue(context, "dspace", "object", "owner", id.toString()); + while (items.hasNext()) { + Item item = items.next(); + if (hasEntityTypeMetadataEqualsTo(item, profileType)) { + return item; + } + } + return null; + } + + @SuppressWarnings("rawtypes") + private Collection findProfileCollection(Context context) throws SQLException, SearchServiceException { + UUID uuid = UUIDUtils.fromString(configurationService.getProperty("researcher-profile.collection.uuid")); + if (uuid != null) { + return collectionService.find(context, uuid); + } + + String profileType = getProfileType(); + + DiscoverQuery discoverQuery = new DiscoverQuery(); + discoverQuery.setDSpaceObjectFilter(IndexableCollection.TYPE); + discoverQuery.addFilterQueries("dspace.entity.type:" + profileType); + + DiscoverResult discoverResult = searchService.search(context, discoverQuery); + List indexableObjects = discoverResult.getIndexableObjects(); + + if (CollectionUtils.isEmpty(indexableObjects)) { + return null; + } + + if (indexableObjects.size() > 1) { + log.warn("Multiple " + profileType + " type collections were found during profile creation"); + return null; + } + + return (Collection) indexableObjects.get(0).getIndexedObject(); + } + + private Item createProfileItem(Context context, EPerson ePerson, Collection collection) + throws AuthorizeException, SQLException { + + String id = ePerson.getID().toString(); + + WorkspaceItem workspaceItem = workspaceItemService.create(context, collection, true); + Item item = workspaceItem.getItem(); + itemService.addMetadata(context, item, "dc", "title", null, null, ePerson.getFullName()); + itemService.addMetadata(context, item, "dspace", "object", "owner", null, ePerson.getFullName(), id, CF_ACCEPTED); + + item = installItemService.installItem(context, workspaceItem); + + Group anonymous = groupService.findByName(context, ANONYMOUS); + authorizeService.removeGroupPolicies(context, item, anonymous); + authorizeService.addPolicy(context, item, READ, ePerson); + + return item; + } + + private boolean hasEntityTypeMetadataEqualsTo(Item item, String entityType) { + return item.getMetadata().stream().anyMatch(metadataValue -> { + return "dspace.entity.type".equals(metadataValue.getMetadataField().toString('.')) && + entityType.equals(metadataValue.getValue()); + }); + } + + private boolean isHardDeleteEnabled() { + return configurationService.getBooleanProperty("researcher-profile.hard-delete.enabled"); + } + + private void removeDspaceObjectOwnerMetadata(Context context, Item profileItem) throws SQLException { + List metadata = itemService.getMetadata(profileItem, "dspace", "object", "owner", Item.ANY); + itemService.removeMetadataValues(context, profileItem, metadata); + } + + private void deleteItem(Context context, Item profileItem) throws SQLException, AuthorizeException { + try { + context.turnOffAuthorisationSystem(); + itemService.delete(context, profileItem); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + context.restoreAuthSystemState(); + } + } + + private String getProfileType() { + return configurationService.getProperty("researcher-profile.type", "Person"); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/profile/service/AfterResearcherProfileCreationAction.java b/dspace-api/src/main/java/org/dspace/app/profile/service/AfterResearcherProfileCreationAction.java new file mode 100644 index 000000000000..7bf5c97aa869 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/profile/service/AfterResearcherProfileCreationAction.java @@ -0,0 +1,35 @@ +/** + * 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.profile.service; + +import org.dspace.app.profile.ResearcherProfile; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; + +import java.sql.SQLException; + +/** + * Interface to mark classes that allow to perform additional logic on created + * researcher profile. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +public interface AfterResearcherProfileCreationAction { + + /** + * Perform some actions on the given researcher profile and returns the updated + * profile. + * + * @param context the DSpace context + * @param researcherProfile the created researcher profile + * @param owner the EPerson that is owner of the given profile + * @throws SQLException if a SQL error occurs + */ + void perform(Context context, ResearcherProfile researcherProfile, EPerson owner) throws SQLException; +} diff --git a/dspace-api/src/main/java/org/dspace/app/profile/service/ResearcherProfileService.java b/dspace-api/src/main/java/org/dspace/app/profile/service/ResearcherProfileService.java new file mode 100644 index 000000000000..0d565f07549e --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/profile/service/ResearcherProfileService.java @@ -0,0 +1,86 @@ +/** + * 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.profile.service; + +import org.dspace.app.profile.ResearcherProfile; +import org.dspace.authorize.AuthorizeException; +import org.dspace.core.Context; +import org.dspace.discovery.SearchServiceException; +import org.dspace.eperson.EPerson; + +import java.net.URI; +import java.sql.SQLException; +import java.util.UUID; + +/** + * Service interface class for the {@link ResearcherProfile} object. The + * implementation of this class is responsible for all business logic calls for + * the {@link ResearcherProfile} object. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +public interface ResearcherProfileService { + + /** + * Find the ResearcherProfile by UUID. + * + * @param context the relevant DSpace Context. + * @param id the ResearcherProfile id + * @return the found ResearcherProfile + * @throws SQLException + * @throws AuthorizeException + */ + public ResearcherProfile findById(Context context, UUID id) throws SQLException, AuthorizeException; + + /** + * Create a new researcher profile for the given ePerson. + * + * @param context the relevant DSpace Context. + * @param ePerson the ePerson + * @return the created profile + * @throws SQLException + * @throws AuthorizeException + * @throws SearchServiceException + */ + public ResearcherProfile createAndReturn(Context context, EPerson ePerson) + throws AuthorizeException, SQLException, SearchServiceException; + + /** + * Removes the association between the researcher profile and eperson related to + * the input uuid. + * + * @param context the relevant DSpace Context. + * @param id the researcher profile id + * @throws AuthorizeException + * @throws SQLException + */ + public void deleteById(Context context, UUID id) throws SQLException, AuthorizeException; + + /** + * Changes the visibility of the given profile using the given new visible value + * + * @param context the relevant DSpace Context. + * @param profile the researcher profile to update + * @param visible the visible value to set + * @throws SQLException + * @throws AuthorizeException + */ + public void changeVisibility(Context context, ResearcherProfile profile, boolean visible) + throws AuthorizeException, SQLException; + + /** + * Claims and links an eperson to an existing DSpaceObject + * @param context the relevant DSpace Context. + * @param ePerson the ePerson + * @param uri uri of existing DSpaceObject to be linked to the eperson + * @return + */ + ResearcherProfile claim(Context context, EPerson ePerson, URI uri) + throws SQLException, AuthorizeException, SearchServiceException; +} diff --git a/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java b/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java index 919e82f14f58..57300e8e1864 100644 --- a/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java @@ -959,4 +959,8 @@ private String formatCustomQuery(String query) { return query + " AND "; } } + @Override + public boolean isPartOfTheGroup(Context c, String egroup) throws SQLException { + return false; + } } diff --git a/dspace-api/src/main/java/org/dspace/authorize/service/AuthorizeService.java b/dspace-api/src/main/java/org/dspace/authorize/service/AuthorizeService.java index 9f6171a22030..65bf0f31f9a7 100644 --- a/dspace-api/src/main/java/org/dspace/authorize/service/AuthorizeService.java +++ b/dspace-api/src/main/java/org/dspace/authorize/service/AuthorizeService.java @@ -592,4 +592,5 @@ List findAdminAuthorizedCollection(Context context, String query, in */ long countAdminAuthorizedCollection(Context context, String query) throws SearchServiceException, SQLException; + public boolean isPartOfTheGroup(Context c, String egroup) throws SQLException; } diff --git a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java index dbde9745fb84..1da8a3a4eb79 100644 --- a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java @@ -1131,6 +1131,50 @@ private boolean shouldBeAppended(Context context, DSpaceObject dso, ResourcePoli return !(hasCustomPolicy && isAnonimousGroup && datesAreNull); } + /** + * Returns an iterator of Items possessing the passed metadata field, or only + * those matching the passed value, if value is not Item.ANY + * + * @param context DSpace context object + * @param schema metadata field schema + * @param element metadata field element + * @param qualifier metadata field qualifier + * @param value field value or Item.ANY to match any value + * @return an iterator over the items matching that authority value + * @throws SQLException if database error + * An exception that provides information on a database access error or other errors. + * @throws AuthorizeException if authorization error + * Exception indicating the current user of the context does not have permission + * to perform a particular action. + */ + @Override + public Iterator findArchivedByMetadataField(Context context, + String schema, String element, String qualifier, String value) + throws SQLException, AuthorizeException { + MetadataSchema mds = metadataSchemaService.find(context, schema); + if (mds == null) { + throw new IllegalArgumentException("No such metadata schema: " + schema); + } + MetadataField mdf = metadataFieldService.findByElement(context, mds, element, qualifier); + if (mdf == null) { + throw new IllegalArgumentException( + "No such metadata field: schema=" + schema + ", element=" + element + ", qualifier=" + qualifier); + } + + if (Item.ANY.equals(value)) { + return itemDAO.findByMetadataField(context, mdf, null, true); + } else { + return itemDAO.findByMetadataField(context, mdf, value, true); + } + } + + @Override + public Iterator findArchivedByMetadataField(Context context, String metadataField, String value) + throws SQLException, AuthorizeException { + String[] mdValueByField = getMDValueByField(metadataField); + return findArchivedByMetadataField(context, mdValueByField[0], mdValueByField[1], mdValueByField[2], value); + } + /** * Returns an iterator of Items possessing the passed metadata field, or only * those matching the passed value, if value is not Item.ANY @@ -1535,5 +1579,9 @@ public MetadataValue addMetadata(Context context, Item dso, String schema, Strin .stream().findFirst().orElse(null); } + @Override + public String getEntityType(Item item) { + return getMetadataFirstValue(item, new MetadataFieldName("dspace.entity.type"), Item.ANY); + } } diff --git a/dspace-api/src/main/java/org/dspace/content/authority/EPersonAuthority.java b/dspace-api/src/main/java/org/dspace/content/authority/EPersonAuthority.java new file mode 100644 index 000000000000..7a1510d7214a --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/authority/EPersonAuthority.java @@ -0,0 +1,93 @@ +/** + * 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.content.authority; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.apache.log4j.Logger; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.factory.EPersonServiceFactory; +import org.dspace.eperson.service.EPersonService; +import org.dspace.util.UUIDUtils; + +/** + * + * @author Mykhaylo Boychuk (4science.it) + */ +public class EPersonAuthority implements ChoiceAuthority { + private static final Logger log = Logger.getLogger(EPersonAuthority.class); + + /** + * the name assigned to the specific instance by the PluginService, @see + * {@link NameAwarePlugin} + **/ + private String authorityName; + + private EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService(); + + @Override + public Choices getBestMatch(String text, String locale) { + return getMatches(text, 0, 2, locale); + } + + @Override + public Choices getMatches(String text, int start, int limit, String locale) { + Context context = null; + if (limit <= 0) { + limit = 20; + } + context = new Context(); + List ePersons = null; + try { + ePersons = ePersonService.search(context, text, start, limit); + } catch (SQLException e) { + log.error(e.getMessage(), e); + throw new RuntimeException(e.getMessage(), e); + } + List choiceList = new ArrayList(); + for (EPerson eperson : ePersons) { + choiceList.add(new Choice(eperson.getID().toString(), eperson.getFullName(), eperson.getFullName())); + } + Choice[] results = new Choice[choiceList.size()]; + results = choiceList.toArray(results); + return new Choices(results, start, ePersons.size(), Choices.CF_AMBIGUOUS, ePersons.size() > (start + limit), 0); + } + + @Override + public String getLabel(String key, String locale) { + + UUID uuid = UUIDUtils.fromString(key); + if (uuid == null) { + return null; + } + + Context context = new Context(); + try { + EPerson ePerson = ePersonService.find(context, uuid); + return ePerson != null ? ePerson.getFullName() : null; + } catch (SQLException e) { + log.error(e.getMessage(), e); + throw new RuntimeException(e.getMessage(), e); + } + + } + + @Override + public String getPluginInstanceName() { + return authorityName; + } + + @Override + public void setPluginInstanceName(String name) { + this.authorityName = name; + } +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/content/service/ItemService.java b/dspace-api/src/main/java/org/dspace/content/service/ItemService.java index d5e2f6776783..2d5468029925 100644 --- a/dspace-api/src/main/java/org/dspace/content/service/ItemService.java +++ b/dspace-api/src/main/java/org/dspace/content/service/ItemService.java @@ -579,6 +579,36 @@ public void move(Context context, Item item, Collection from, Collection to, boo */ public boolean canCreateNewVersion(Context context, Item item) throws SQLException; + /** + * Returns an iterator of in archive items possessing the passed metadata field, or only + * those matching the passed value, if value is not Item.ANY + * + * @param context DSpace context object + * @param schema metadata field schema + * @param element metadata field element + * @param qualifier metadata field qualifier + * @param value field value or Item.ANY to match any value + * @return an iterator over the items matching that authority value + * @throws SQLException if database error + * @throws AuthorizeException if authorization error + */ + public Iterator findArchivedByMetadataField(Context context, String schema, String element, + String qualifier, String value) throws SQLException, AuthorizeException; + + /** + * Returns an iterator of in archive items possessing the passed metadata field, or only + * those matching the passed value, if value is not Item.ANY + * + * @param context DSpace context object + * @param metadataField metadata + * @param value field value or Item.ANY to match any value + * @return an iterator over the items matching that authority value + * @throws SQLException if database error + * @throws AuthorizeException if authorization error + */ + public Iterator findArchivedByMetadataField(Context context, String metadataField, String value) + throws SQLException, AuthorizeException; + /** * Returns an iterator of Items possessing the passed metadata field, or only * those matching the passed value, if value is not Item.ANY @@ -618,7 +648,7 @@ public Iterator findByMetadataQuery(Context context, List findByAuthorityValue(Context context, String schema, String element, String qualifier, String value) - throws SQLException, AuthorizeException, IOException; + throws SQLException, AuthorizeException; public Iterator findByMetadataFieldAuthority(Context context, String mdString, String authority) @@ -782,5 +812,12 @@ public Iterator findByLastModifiedSince(Context context, Date last) */ public List getMetadata(Item item, String schema, String element, String qualifier, String lang, boolean enableVirtualMetadata); + /** + * Returns the item's entity type, if any. + * + * @param item the item + * @return the entity type as string, if any + */ + public String getEntityType(Item item); } diff --git a/dspace-api/src/test/data/dspaceFolder/config/local.cfg b/dspace-api/src/test/data/dspaceFolder/config/local.cfg index 3c19a68e9fd1..11a344fa0dc4 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/local.cfg +++ b/dspace-api/src/test/data/dspaceFolder/config/local.cfg @@ -20,7 +20,7 @@ # For example, including "dspace.dir" in this local.cfg will override the # default value of "dspace.dir" in the dspace.cfg file. # - +researcher-profile.type = Person ########################## # SERVER CONFIGURATION # ########################## diff --git a/dspace-api/src/test/java/org/dspace/app/matcher/LambdaMatcher.java b/dspace-api/src/test/java/org/dspace/app/matcher/LambdaMatcher.java new file mode 100644 index 000000000000..641aee57040a --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/app/matcher/LambdaMatcher.java @@ -0,0 +1,55 @@ +/** + * 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.matcher; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; + +import java.util.function.Predicate; + +/** + * Matcher based on an {@link Predicate}. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * @param the type of the instance to match + */ +public class LambdaMatcher extends BaseMatcher { + + private final Predicate matcher; + private final String description; + + public static LambdaMatcher matches(Predicate matcher) { + return new LambdaMatcher(matcher, "Matches the given predicate"); + } + + public static LambdaMatcher matches(Predicate matcher, String description) { + return new LambdaMatcher(matcher, description); + } + + public static Matcher> has(Predicate matcher) { + return Matchers.hasItem(matches(matcher)); + } + + private LambdaMatcher(Predicate matcher, String description) { + this.matcher = matcher; + this.description = description; + } + + @Override + @SuppressWarnings("unchecked") + public boolean matches(Object argument) { + return matcher.test((T) argument); + } + + @Override + public void describeTo(Description description) { + description.appendText(this.description); + } +} diff --git a/dspace-api/src/test/java/org/dspace/app/matcher/MetadataValueMatcher.java b/dspace-api/src/test/java/org/dspace/app/matcher/MetadataValueMatcher.java new file mode 100644 index 000000000000..eb2158a9ce14 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/app/matcher/MetadataValueMatcher.java @@ -0,0 +1,96 @@ +/** + * 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.matcher; + +import org.dspace.content.MetadataValue; +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; + +import java.util.Objects; + +/** + * Implementation of {@link org.hamcrest.Matcher} to match a MetadataValue by + * all its attributes. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +public class MetadataValueMatcher extends TypeSafeMatcher { + + private String field; + + private String value; + + private String language; + + private String authority; + + private Integer place; + + private Integer confidence; + + private MetadataValueMatcher(String field, String value, String language, String authority, Integer place, + Integer confidence) { + + this.field = field; + this.value = value; + this.language = language; + this.authority = authority; + this.place = place; + this.confidence = confidence; + + } + + @Override + public void describeTo(Description description) { + description.appendText("MetadataValue with the following attributes [field=" + field + ", value=" + + value + ", language=" + language + ", authority=" + authority + ", place=" + place + ", confidence=" + + confidence + "]"); + } + + @Override + protected void describeMismatchSafely(MetadataValue item, Description mismatchDescription) { + mismatchDescription.appendText("was ") + .appendValue("MetadataValue [metadataField=").appendValue(item.getMetadataField().toString('.')) + .appendValue(", value=").appendValue(item.getValue()).appendValue(", language=").appendValue(language) + .appendValue(", place=").appendValue(item.getPlace()).appendValue(", authority=") + .appendValue(item.getAuthority()).appendValue(", confidence=").appendValue(item.getConfidence() + "]"); + } + + @Override + protected boolean matchesSafely(MetadataValue metadataValue) { + return Objects.equals(metadataValue.getValue(), value) && + Objects.equals(metadataValue.getMetadataField().toString('.'), field) && + Objects.equals(metadataValue.getLanguage(), language) && + Objects.equals(metadataValue.getAuthority(), authority) && + Objects.equals(metadataValue.getPlace(), place) && + Objects.equals(metadataValue.getConfidence(), confidence); + } + + public static MetadataValueMatcher with(String field, String value, String language, + String authority, Integer place, Integer confidence) { + return new MetadataValueMatcher(field, value, language, authority, place, confidence); + } + + public static MetadataValueMatcher with(String field, String value) { + return with(field, value, null, null, 0, -1); + } + + public static MetadataValueMatcher with(String field, String value, String authority, int place, int confidence) { + return with(field, value, null, authority, place, confidence); + } + + public static MetadataValueMatcher with(String field, String value, String authority, int confidence) { + return with(field, value, null, authority, 0, confidence); + } + + public static MetadataValueMatcher with(String field, String value, int place) { + return with(field, value, null, null, place, -1); + } + +} diff --git a/dspace-api/src/test/java/org/dspace/builder/ItemBuilder.java b/dspace-api/src/test/java/org/dspace/builder/ItemBuilder.java index aad0e86b1e90..ac32e9a2ae84 100644 --- a/dspace-api/src/test/java/org/dspace/builder/ItemBuilder.java +++ b/dspace-api/src/test/java/org/dspace/builder/ItemBuilder.java @@ -22,6 +22,9 @@ import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; +import static org.dspace.content.MetadataSchemaEnum.DC; +import static org.dspace.content.authority.Choices.CF_ACCEPTED; + /** * Builder to construct Item objects * @@ -73,6 +76,11 @@ public ItemBuilder withIdentifierOther(final String identifierOther) { public ItemBuilder withAuthor(final String authorName) { return addMetadataValue(item, MetadataSchemaEnum.DC.getName(), "contributor", "author", authorName); } + + public ItemBuilder withAuthor(final String authorName, final String authority) { + return addMetadataValue(item, DC.getName(), "contributor", "author", null, authorName, authority, 600); + } + public ItemBuilder withAuthor(final String authorName, final String authority, final int confidence) { return addMetadataValue(item, MetadataSchemaEnum.DC.getName(), "contributor", "author", null, authorName, authority, confidence); @@ -144,6 +152,13 @@ public ItemBuilder withMetadata(final String schema, final String element, final return addMetadataValue(item, schema, element, qualifier, value); } + public ItemBuilder withDspaceObjectOwner(String value, String authority) { + return addMetadataValue(item, "dspace", "object", "owner", null, value, authority, CF_ACCEPTED); + } + + public ItemBuilder withDspaceObjectOwner(EPerson ePerson) { + return withDspaceObjectOwner(ePerson.getFullName(), ePerson.getID().toString()); + } public ItemBuilder makeUnDiscoverable() { item.setDiscoverable(false); return this; @@ -181,6 +196,9 @@ public ItemBuilder withAdminUser(EPerson ePerson) throws SQLException, Authorize return setAdminPermission(item, ePerson, null); } + public ItemBuilder withPersonEmail(String email) { + return addMetadataValue(item, "person", "email", null, email); + } @Override public Item build() { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ResearcherProfileConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ResearcherProfileConverter.java new file mode 100644 index 000000000000..4eb291d8d18d --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ResearcherProfileConverter.java @@ -0,0 +1,94 @@ +/** + * 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.rest.converter; + +//import static org.dspace.app.orcid.model.OrcidEntityType.FUNDING; +//import static org.dspace.app.orcid.model.OrcidEntityType.PUBLICATION; + +import java.util.List; +import java.util.stream.Collectors; + +//import org.dspace.app.orcid.service.OrcidSynchronizationService; +//import org.dspace.app.profile.OrcidEntitySyncPreference; +//import org.dspace.app.profile.OrcidProfileSyncPreference; +//import org.dspace.app.profile.OrcidSynchronizationMode; +import org.dspace.app.profile.ResearcherProfile; +import org.dspace.app.rest.model.ResearcherProfileRest; +//import org.dspace.app.rest.model.ResearcherProfileRest.OrcidSynchronizationRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.content.Item; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * This converter is responsible for transforming an model that represent a + * ResearcherProfile to the REST representation of an ResearcherProfile. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +@Component +public class ResearcherProfileConverter implements DSpaceConverter { + +// @Autowired +// private OrcidSynchronizationService orcidSynchronizationService; + + @Override + public ResearcherProfileRest convert(ResearcherProfile profile, Projection projection) { + ResearcherProfileRest researcherProfileRest = new ResearcherProfileRest(); + + researcherProfileRest.setVisible(profile.isVisible()); + researcherProfileRest.setId(profile.getId()); + researcherProfileRest.setProjection(projection); + + Item item = profile.getItem(); + +// if (orcidSynchronizationService.isLinkedToOrcid(item)) { +// profile.getOrcid().ifPresent(researcherProfileRest::setOrcid); +// +// OrcidSynchronizationRest orcidSynchronization = new OrcidSynchronizationRest(); +// orcidSynchronization.setMode(getMode(item)); +// orcidSynchronization.setProfilePreferences(getProfilePreferences(item)); +// orcidSynchronization.setFundingsPreference(getFundingsPreference(item)); +// orcidSynchronization.setPublicationsPreference(getPublicationsPreference(item)); +// researcherProfileRest.setOrcidSynchronization(orcidSynchronization); +// } + + return researcherProfileRest; + } + +// private String getPublicationsPreference(Item item) { +// return orcidSynchronizationService.getEntityPreference(item, PUBLICATION) +// .map(OrcidEntitySyncPreference::name) +// .orElse(OrcidEntitySyncPreference.DISABLED.name()); +// } +// +// private String getFundingsPreference(Item item) { +// return orcidSynchronizationService.getEntityPreference(item, FUNDING) +// .map(OrcidEntitySyncPreference::name) +// .orElse(OrcidEntitySyncPreference.DISABLED.name()); +// } +// +// private List getProfilePreferences(Item item) { +// return orcidSynchronizationService.getProfilePreferences(item).stream() +// .map(OrcidProfileSyncPreference::name) +// .collect(Collectors.toList()); +// } +// +// private String getMode(Item item) { +// return orcidSynchronizationService.getSynchronizationMode(item) +// .map(OrcidSynchronizationMode::name) +// .orElse(OrcidSynchronizationMode.MANUAL.name()); +// } + + @Override + public Class getModelClass() { + return ResearcherProfile.class; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java index 70bc2c7eed29..c3ebc1ca54ee 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java @@ -20,11 +20,16 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.exception.ResourceConflictException; +import org.dspace.app.rest.converter.ConverterService; +import org.dspace.app.rest.model.RestModel; import org.dspace.app.rest.utils.ContextUtil; +import org.dspace.app.rest.utils.Utils; import org.dspace.authorize.AuthorizeException; import org.dspace.core.Context; import org.dspace.services.ConfigurationService; import org.springframework.beans.TypeMismatchException; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.repository.support.QueryMethodParameterConversionException; import org.springframework.http.HttpHeaders; @@ -67,6 +72,12 @@ public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionH @Inject private ConfigurationService configurationService; + @Autowired + private ConverterService converterService; + + @Autowired + private Utils utils; + @ExceptionHandler({AuthorizeException.class, RESTAuthorizationException.class, AccessDeniedException.class}) protected void handleAuthorizeException(HttpServletRequest request, HttpServletResponse response, Exception ex) throws IOException { @@ -166,6 +177,12 @@ protected void ParameterConversionException(HttpServletRequest request, HttpServ HttpStatus.BAD_REQUEST.value()); } + @ExceptionHandler(ResourceConflictException.class) + protected ResponseEntity resourceConflictException(ResourceConflictException ex) { + RestModel resource = converterService.toRest(ex.getResource(), utils.obtainProjection()); + return new ResponseEntity(resource, HttpStatus.CONFLICT); + } + @ExceptionHandler(MissingParameterException.class) protected void MissingParameterException(HttpServletRequest request, HttpServletResponse response, Exception ex) throws IOException { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/login/PostLoggedInAction.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/login/PostLoggedInAction.java new file mode 100644 index 000000000000..d288ef1ecf76 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/login/PostLoggedInAction.java @@ -0,0 +1,27 @@ +/** + * 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.rest.login; + +import org.dspace.core.Context; + +/** + * Interface for classes that need to perform some operations after the user + * login. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +public interface PostLoggedInAction { + + /** + * Perform some operations after the user login. + * + * @param context the DSpace context + */ + public void loggedIn(Context context); +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/login/impl/LoginEventFireAction.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/login/impl/LoginEventFireAction.java new file mode 100644 index 000000000000..b5df891d1025 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/login/impl/LoginEventFireAction.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.rest.login.impl; + +import org.dspace.app.rest.login.PostLoggedInAction; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.services.EventService; +import org.dspace.usage.UsageEvent; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; + +/** + * Implementation of {@link PostLoggedInAction} that fire an LOGIN event. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +public class LoginEventFireAction implements PostLoggedInAction { + + @Autowired + private EventService eventService; + + @Override + public void loggedIn(Context context) { + + HttpServletRequest request = getCurrentRequest(); + EPerson currentUser = context.getCurrentUser(); + + eventService.fireEvent(new UsageEvent(UsageEvent.Action.LOGIN, request, context, currentUser)); + + } + + private HttpServletRequest getCurrentRequest() { + return ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/login/impl/ResearcherProfileAutomaticClaim.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/login/impl/ResearcherProfileAutomaticClaim.java new file mode 100644 index 000000000000..bcc10781e98d --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/login/impl/ResearcherProfileAutomaticClaim.java @@ -0,0 +1,129 @@ +/** + * 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.rest.login.impl; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.dspace.app.profile.service.ResearcherProfileService; +import org.dspace.app.rest.login.PostLoggedInAction; +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.Item; +import org.dspace.content.MetadataFieldName; +import org.dspace.content.service.ItemService; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.service.EPersonService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; + +import java.sql.SQLException; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import static org.apache.commons.collections4.IteratorUtils.toList; +import static org.dspace.content.authority.Choices.CF_ACCEPTED; + +/** + * Implementation of {@link PostLoggedInAction} that perform an automatic claim + * between the logged eperson and possible profiles without eperson present in + * the system. This pairing between eperson and profile is done starting from + * the configured metadata of the logged in user. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +public class ResearcherProfileAutomaticClaim implements PostLoggedInAction { + + private final static Logger LOGGER = LoggerFactory.getLogger(ResearcherProfileAutomaticClaim.class); + + @Autowired + private ResearcherProfileService researcherProfileService; + + @Autowired + private ItemService itemService; + + @Autowired + private EPersonService ePersonService; + + private final String ePersonField; + + private final String profileFiled; + + public ResearcherProfileAutomaticClaim(String ePersonField, String profileField) { + Assert.notNull(ePersonField, "An eperson field is required to perform automatic claim"); + Assert.notNull(profileField, "An profile field is required to perform automatic claim"); + this.ePersonField = ePersonField; + this.profileFiled = profileField; + } + + @Override + public void loggedIn(Context context) { + + EPerson currentUser = context.getCurrentUser(); + if (currentUser == null) { + return; + } + + try { + claimProfile(context, currentUser); + } catch (SQLException | AuthorizeException e) { + LOGGER.error("An error occurs during the profile claim by email", e); + } + + } + + private void claimProfile(Context context, EPerson currentUser) throws SQLException, AuthorizeException { + + UUID id = currentUser.getID(); + String fullName = currentUser.getFullName(); + + if (currentUserHasAlreadyResearcherProfile(context)) { + return; + } + + Item item = findClaimableItem(context, currentUser); + if (item != null) { + itemService.addMetadata(context, item, "dspace", "object", "owner", null, fullName, id.toString(), CF_ACCEPTED); + } + + } + + private boolean currentUserHasAlreadyResearcherProfile(Context context) throws SQLException, AuthorizeException { + return researcherProfileService.findById(context, context.getCurrentUser().getID()) != null; + } + + private Item findClaimableItem(Context context, EPerson currentUser) + throws SQLException, AuthorizeException { + + String value = getValueToSearchFor(context, currentUser); + if (StringUtils.isEmpty(value)) { + return null; + } + + List items = toList(itemService.findArchivedByMetadataField(context, profileFiled, value)).stream() + .filter(this::hasNotCrisOwner) + .collect(Collectors.toList()); + + return items.size() == 1 ? items.get(0) : null; + } + + private String getValueToSearchFor(Context context, EPerson currentUser) { + if ("email".equals(ePersonField)) { + return currentUser.getEmail(); + } + return ePersonService.getMetadataFirstValue(currentUser, new MetadataFieldName(ePersonField), Item.ANY); + } + + private boolean hasNotCrisOwner(Item item) { + return CollectionUtils.isEmpty(itemService.getMetadata(item, "dspace", "object", "owner", Item.ANY)); + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ResearcherProfileRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ResearcherProfileRest.java new file mode 100644 index 000000000000..c1d8f692545d --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ResearcherProfileRest.java @@ -0,0 +1,134 @@ +/** + * 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.rest.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import org.dspace.app.rest.RestResourceController; + +import java.util.List; +import java.util.UUID; + +/** + * The Researcher Profile REST resource. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +@LinksRest(links = { + @LinkRest(name = ResearcherProfileRest.ITEM, method = "getItem"), + @LinkRest(name = ResearcherProfileRest.EPERSON, method = "getEPerson") +}) +public class ResearcherProfileRest extends BaseObjectRest { + + private static final long serialVersionUID = 1L; + // changed from RestModel.CRIS to RestModel.EPERSON + public static final String CATEGORY = RestModel.EPERSON; + public static final String NAME = "profile"; + + public static final String ITEM = "item"; + public static final String EPERSON = "eperson"; + + private boolean visible; + +// @JsonInclude(Include.NON_NULL) +// private String orcid; + +// @JsonInclude(Include.NON_NULL) +// private OrcidSynchronizationRest orcidSynchronization; + + public boolean isVisible() { + return visible; + } + + public void setVisible(boolean visible) { + this.visible = visible; + } + +// public OrcidSynchronizationRest getOrcidSynchronization() { +// return orcidSynchronization; +// } + +// public void setOrcidSynchronization(OrcidSynchronizationRest orcidSynchronization) { +// this.orcidSynchronization = orcidSynchronization; +// } + +// public String getOrcid() { +// return orcid; +// } +// +// public void setOrcid(String orcid) { +// this.orcid = orcid; +// } + + @Override + public String getType() { + return NAME; + } + + @Override + public String getCategory() { + return CATEGORY; + } + + @Override + public Class getController() { + return RestResourceController.class; + } + + /** + * Inner class to model ORCID synchronization preferences and mode. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +// public static class OrcidSynchronizationRest { +// +// private String mode; +// +// private String publicationsPreference; +// +// private String fundingsPreference; +// +// private List profilePreferences; +// +// public String getMode() { +// return mode; +// } +// +// public void setMode(String mode) { +// this.mode = mode; +// } +// +// public List getProfilePreferences() { +// return profilePreferences; +// } +// +// public void setProfilePreferences(List profilePreferences) { +// this.profilePreferences = profilePreferences; +// } +// +// public String getPublicationsPreference() { +// return publicationsPreference; +// } +// +// public void setPublicationsPreference(String publicationsPreference) { +// this.publicationsPreference = publicationsPreference; +// } +// +// public String getFundingsPreference() { +// return fundingsPreference; +// } +// +// public void setFundingsPreference(String fundingsPreference) { +// this.fundingsPreference = fundingsPreference; +// } +// +// } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/ResearcherProfileResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/ResearcherProfileResource.java new file mode 100644 index 000000000000..3b034c150631 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/ResearcherProfileResource.java @@ -0,0 +1,29 @@ +/** + * 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.rest.model.hateoas; + +import org.dspace.app.rest.model.ResearcherProfileRest; +import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource; +import org.dspace.app.rest.utils.Utils; + +/** + * This class serves as a wrapper class to wrap the SearchConfigurationRest into + * a HAL resource. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +@RelNameDSpaceResource(ResearcherProfileRest.NAME) +public class ResearcherProfileResource extends DSpaceResource { + + public ResearcherProfileResource(ResearcherProfileRest data, Utils utils) { + super(data, utils); + } + + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResearcherProfileEPersonLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResearcherProfileEPersonLinkRepository.java new file mode 100644 index 000000000000..ed7b28600372 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResearcherProfileEPersonLinkRepository.java @@ -0,0 +1,78 @@ +/** + * 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.rest.repository; + +import java.sql.SQLException; +import java.util.UUID; +import javax.annotation.Nullable; +import javax.servlet.http.HttpServletRequest; + +import org.dspace.app.profile.ResearcherProfile; +import org.dspace.app.profile.service.ResearcherProfileService; +import org.dspace.app.rest.model.EPersonRest; +import org.dspace.app.rest.model.ResearcherProfileRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.authorize.AuthorizeException; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.service.EPersonService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; +import org.springframework.data.rest.webmvc.ResourceNotFoundException; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Component; + +/** + * Link repository for "ePerson" subresource of an individual researcher + * profile. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +@Component(ResearcherProfileRest.CATEGORY + "." + ResearcherProfileRest.NAME + "." + ResearcherProfileRest.EPERSON) +public class ResearcherProfileEPersonLinkRepository extends AbstractDSpaceRestRepository implements LinkRestRepository { + + @Autowired + private EPersonService ePersonService; + + @Autowired + private ResearcherProfileService researcherProfileService; + + /** + * Returns the ePerson related to the Research profile with the given UUID. + * + * @param request the http servlet request + * @param id the profile UUID + * @param pageable the optional pageable + * @param projection the projection object + * @return the ePerson rest representation + */ + @PreAuthorize("hasPermission(#id, 'PROFILE', 'READ')") + public EPersonRest getEPerson(@Nullable HttpServletRequest request, UUID id, + @Nullable Pageable pageable, Projection projection) { + + try { + Context context = obtainContext(); + + ResearcherProfile profile = researcherProfileService.findById(context, id); + if (profile == null) { + throw new ResourceNotFoundException("No such profile with UUID: " + id); + } + + EPerson ePerson = ePersonService.find(context, id); + if (ePerson == null) { + throw new ResourceNotFoundException("No such eperson related to a profile with EPerson UUID: " + id); + } + + return converter.toRest(ePerson, projection); + } catch (SQLException | AuthorizeException e) { + throw new RuntimeException(e); + } + + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResearcherProfileItemLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResearcherProfileItemLinkRepository.java new file mode 100644 index 000000000000..827d55414950 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResearcherProfileItemLinkRepository.java @@ -0,0 +1,67 @@ +/** + * 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.rest.repository; + +import java.sql.SQLException; +import java.util.UUID; +import javax.annotation.Nullable; +import javax.servlet.http.HttpServletRequest; + +import org.dspace.app.profile.ResearcherProfile; +import org.dspace.app.profile.service.ResearcherProfileService; +import org.dspace.app.rest.model.ItemRest; +import org.dspace.app.rest.model.ResearcherProfileRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.authorize.AuthorizeException; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; +import org.springframework.data.rest.webmvc.ResourceNotFoundException; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Component; + +/** + * Link repository for "item" subresource of an individual researcher profile. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +@Component(ResearcherProfileRest.CATEGORY + "." + ResearcherProfileRest.NAME + "." + ResearcherProfileRest.ITEM) +public class ResearcherProfileItemLinkRepository extends AbstractDSpaceRestRepository implements LinkRestRepository { + + @Autowired + private ResearcherProfileService researcherProfileService; + + /** + * Returns the item related to the Research profile with the given UUID. + * + * @param request the http servlet request + * @param id the profile UUID + * @param pageable the optional pageable + * @param projection the projection object + * @return the item rest representation + */ + @PreAuthorize("hasPermission(#id, 'PROFILE', 'READ')") + public ItemRest getItem(@Nullable HttpServletRequest request, UUID id, + @Nullable Pageable pageable, Projection projection) { + + try { + Context context = obtainContext(); + + ResearcherProfile profile = researcherProfileService.findById(context, id); + if (profile == null) { + throw new ResourceNotFoundException("No such item related to a profile with EPerson UUID: " + id); + } + + return converter.toRest(profile.getItem(), projection); + } catch (SQLException | AuthorizeException e) { + throw new RuntimeException(e); + } + + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResearcherProfileRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResearcherProfileRestRepository.java new file mode 100644 index 000000000000..8be3720f52d8 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResearcherProfileRestRepository.java @@ -0,0 +1,189 @@ +/** + * 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.rest.repository; + +import org.apache.commons.collections.CollectionUtils; +import org.dspace.app.profile.ResearcherProfile; +import org.dspace.app.profile.service.ResearcherProfileService; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.exception.RepositoryMethodNotImplementedException; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.ResearcherProfileRest; +import org.dspace.app.rest.model.patch.Patch; +import org.dspace.app.rest.repository.patch.ResourcePatch; +import org.dspace.app.rest.security.DSpacePermissionEvaluator; +import org.dspace.authorize.AuthorizeException; +import org.dspace.core.Context; +import org.dspace.discovery.SearchServiceException; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.service.EPersonService; +import org.dspace.util.UUIDUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Conditional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.rest.webmvc.ResourceNotFoundException; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import java.net.URI; +import java.sql.SQLException; +import java.util.List; +import java.util.UUID; + +/** + * This is the repository responsible of exposing researcher profiles. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +@Component(ResearcherProfileRest.CATEGORY + "." + ResearcherProfileRest.NAME) +@ConditionalOnProperty( + value="researcher-profile.type" +) +public class ResearcherProfileRestRepository extends DSpaceRestRepository { + + public static final String NO_VISIBILITY_CHANGE_MSG = "Refused to perform the Researcher Profile patch based " + + "on a token without changing the visibility"; + + @Autowired + private ResearcherProfileService researcherProfileService; + + @Autowired + private DSpacePermissionEvaluator permissionEvaluator; + + @Autowired + private EPersonService ePersonService; + + @Autowired + private ResourcePatch resourcePatch; + + @Override + @PreAuthorize("hasPermission(#id, 'PROFILE', 'READ')") + public ResearcherProfileRest findOne(Context context, UUID id) { + try { + ResearcherProfile profile = researcherProfileService.findById(context, id); + if (profile == null) { + return null; + } + return converter.toRest(profile, utils.obtainProjection()); + } catch (SQLException | AuthorizeException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + @Override + @PreAuthorize("isAuthenticated()") + protected ResearcherProfileRest createAndReturn(Context context) throws AuthorizeException, SQLException { + + UUID id = getEPersonIdFromRequest(context); + if (isNotAuthorized(id, "WRITE")) { + throw new AuthorizeException("User unauthorized to create a new profile for user " + id); + } + + EPerson ePerson = ePersonService.find(context, id); + if (ePerson == null) { + throw new UnprocessableEntityException("No EPerson exists with id: " + id); + } + + try { + ResearcherProfile newProfile = researcherProfileService.createAndReturn(context, ePerson); + return converter.toRest(newProfile, utils.obtainProjection()); + } catch (SearchServiceException e) { + throw new RuntimeException(e.getMessage(), e); + } + + } + + @Override + protected ResearcherProfileRest createAndReturn(final Context context, final List list) + throws AuthorizeException, SQLException, RepositoryMethodNotImplementedException { + if (CollectionUtils.isEmpty(list) || list.size() > 1) { + throw new IllegalArgumentException("Uri list must contain exactly one element"); + } + + + UUID id = getEPersonIdFromRequest(context); + if (isNotAuthorized(id, "WRITE")) { + throw new AuthorizeException("User unauthorized to create a new profile for user " + id); + } + + EPerson ePerson = ePersonService.find(context, id); + if (ePerson == null) { + throw new UnprocessableEntityException("No EPerson exists with id: " + id); + } + + try { + ResearcherProfile newProfile = researcherProfileService + .claim(context, ePerson, URI.create(list.get(0))); + return converter.toRest(newProfile, utils.obtainProjection()); + } catch (SearchServiceException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + @Override + public Page findAll(Context context, Pageable pageable) { + throw new RepositoryMethodNotImplementedException("No implementation found; Method not allowed!", ""); + } + + @Override + @PreAuthorize("hasPermission(#id, 'PROFILE', 'WRITE')") + protected void delete(Context context, UUID id) { + try { + researcherProfileService.deleteById(context, id); + } catch (SQLException | AuthorizeException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + @Override + @PreAuthorize("hasPermission(#id, 'PROFILE', #patch)") + protected void patch(Context context, HttpServletRequest request, String apiCategory, String model, + UUID id, Patch patch) throws SQLException, AuthorizeException { + + ResearcherProfile profile = researcherProfileService.findById(context, id); + if (profile == null) { + throw new ResourceNotFoundException(apiCategory + "." + model + " with id: " + id + " not found"); + } + + resourcePatch.patch(context, profile, patch.getOperations()); + + } + + @Override + public Class getDomainClass() { + return ResearcherProfileRest.class; + } + + + private UUID getEPersonIdFromRequest(Context context) { + HttpServletRequest request = getRequestService().getCurrentRequest().getHttpServletRequest(); + + String ePersonId = request.getParameter("eperson"); + if (ePersonId == null) { + return context.getCurrentUser().getID(); + } + + UUID uuid = UUIDUtils.fromString(ePersonId); + if (uuid == null) { + throw new DSpaceBadRequestException("The provided eperson parameter is not a valid uuid"); + } + return uuid; + } + + private boolean isNotAuthorized(UUID id, String permission) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return !permissionEvaluator.hasPermission(authentication, id, "PROFILE", permission); + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ResearcherProfileVisibleReplaceOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ResearcherProfileVisibleReplaceOperation.java new file mode 100644 index 000000000000..a9cd66b4d2b8 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ResearcherProfileVisibleReplaceOperation.java @@ -0,0 +1,67 @@ +/** + * 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.rest.repository.patch.operation; + +import java.sql.SQLException; + +import org.dspace.app.profile.ResearcherProfile; +import org.dspace.app.profile.service.ResearcherProfileService; +import org.dspace.app.rest.exception.RESTAuthorizationException; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.authorize.AuthorizeException; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Implementation for ResearcherProfile visibility patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/cris/profiles/<:id-eperson> -H " + * Content-Type: application/json" -d '[{ "op": "replace", "path": " + * /visible", "value": true]' + * + */ +@Component +public class ResearcherProfileVisibleReplaceOperation extends PatchOperation { + + @Autowired + private ResearcherProfileService researcherProfileService; + + /** + * Path in json body of patch that uses this operation. + */ + public static final String OPERATION_VISIBLE_CHANGE = "/visible"; + + @Override + public ResearcherProfile perform(Context context, ResearcherProfile profile, Operation operation) + throws SQLException { + + Object value = operation.getValue(); + if (value == null | !(value instanceof Boolean)) { + throw new UnprocessableEntityException("The /visible value must be a boolean (true|false)"); + } + + try { + researcherProfileService.changeVisibility(context, profile, (boolean) value); + } catch (AuthorizeException e) { + throw new RESTAuthorizationException("Unauthorized user for profile visibility change"); + } + + return profile; + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + return (objectToMatch instanceof ResearcherProfile + && operation.getOp().trim().equalsIgnoreCase(OPERATION_REPLACE) + && operation.getPath().trim().equalsIgnoreCase(OPERATION_VISIBLE_CHANGE)); + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/EPersonRestAuthenticationProvider.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/EPersonRestAuthenticationProvider.java index 20844ec94642..aabf19666a04 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/EPersonRestAuthenticationProvider.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/EPersonRestAuthenticationProvider.java @@ -11,12 +11,15 @@ import static org.dspace.app.rest.security.WebSecurityConfiguration.AUTHENTICATED_GRANT; import java.sql.SQLException; +import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Objects; +import javax.annotation.PostConstruct; import javax.servlet.http.HttpServletRequest; import org.apache.commons.lang3.StringUtils; +import org.dspace.app.rest.login.PostLoggedInAction; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.app.util.AuthorizeUtil; import org.dspace.authenticate.AuthenticationMethod; @@ -62,6 +65,16 @@ public class EPersonRestAuthenticationProvider implements AuthenticationProvider @Autowired private HttpServletRequest request; + @Autowired(required = false) + private List postLoggedInActions; + + @PostConstruct + public void postConstruct() { + if (postLoggedInActions == null) { + postLoggedInActions = Collections.emptyList(); + } + } + @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { Context context = ContextUtil.obtainContext(request); @@ -122,6 +135,11 @@ private Authentication authenticateNewLogin(Authentication authentication) { .getHeader(newContext, "login", "type=explicit")); output = createAuthentication(newContext); + + for (PostLoggedInAction action : postLoggedInActions) { + action.loggedIn(newContext); + } + } else { log.info(LogHelper.getHeader(newContext, "failed_login", "email=" + name + ", result=" diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResearcherProfileRestPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResearcherProfileRestPermissionEvaluatorPlugin.java new file mode 100644 index 000000000000..7545e9ad9c21 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResearcherProfileRestPermissionEvaluatorPlugin.java @@ -0,0 +1,74 @@ +/** + * 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.rest.security; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.app.rest.model.ResearcherProfileRest; +import org.dspace.app.rest.utils.ContextUtil; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.services.RequestService; +import org.dspace.services.model.Request; +import org.dspace.util.UUIDUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import java.io.Serializable; +import java.util.UUID; + +import static org.dspace.app.rest.security.DSpaceRestPermission.*; + +/** + * + * An authenticated user is allowed to view, update or delete his or her own + * data. This {@link RestPermissionEvaluatorPlugin} implements that requirement. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +@Component +public class ResearcherProfileRestPermissionEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin { + + @Autowired + private RequestService requestService; + + @Override + public boolean hasDSpacePermission(Authentication authentication, Serializable targetId, String targetType, + DSpaceRestPermission restPermission) { + + if (!READ.equals(restPermission) && !WRITE.equals(restPermission) && !DELETE.equals(restPermission)) { + return false; + } + + if (!StringUtils.equalsIgnoreCase(targetType, ResearcherProfileRest.NAME)) { + return false; + } + + UUID id = UUIDUtils.fromString(targetId.toString()); + if (id == null) { + return false; + } + + Request request = requestService.getCurrentRequest(); + Context context = ContextUtil.obtainContext((HttpServletRequest) request.getServletRequest()); + + EPerson currentUser = context.getCurrentUser(); + if (currentUser == null) { + return false; + } + + if (id.equals(currentUser.getID())) { + return true; + } + + return false; + } + +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResearcherProfileRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResearcherProfileRestRepositoryIT.java new file mode 100644 index 000000000000..f5a460b29892 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResearcherProfileRestRepositoryIT.java @@ -0,0 +1,1032 @@ +/** + * 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.rest; + +import com.jayway.jsonpath.JsonPath; +import org.dspace.app.rest.model.MetadataValueRest; +import org.dspace.app.rest.model.patch.AddOperation; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.model.patch.RemoveOperation; +import org.dspace.app.rest.model.patch.ReplaceOperation; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.authorize.AuthorizeException; +import org.dspace.builder.*; +import org.dspace.content.Collection; +import org.dspace.content.Item; +import org.dspace.content.MetadataValue; +import org.dspace.content.service.ItemService; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.Group; +import org.dspace.eperson.service.GroupService; +import org.dspace.services.ConfigurationService; +import org.dspace.util.UUIDUtils; +import org.junit.After; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MvcResult; + +import java.io.UnsupportedEncodingException; +import java.sql.SQLException; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; + +import static com.jayway.jsonpath.JsonPath.read; +import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; +import static java.util.Arrays.asList; +import static java.util.UUID.fromString; +import static org.dspace.app.matcher.LambdaMatcher.has; +import static org.dspace.app.matcher.MetadataValueMatcher.with; +import static org.dspace.app.rest.matcher.HalMatcher.matchLinks; +import static org.dspace.app.rest.matcher.MetadataMatcher.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; +import static org.springframework.data.rest.webmvc.RestMediaTypes.TEXT_URI_LIST; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Integration tests for {@link ResearcherProfileRestRepository}. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +public class ResearcherProfileRestRepositoryIT extends AbstractControllerIntegrationTest { + + @Autowired + private ConfigurationService configurationService; + + @Autowired + private ItemService itemService; + + @Autowired + private GroupService groupService; + + private EPerson user; + + private EPerson anotherUser; + + private Collection personCollection; + + private Group administrators; + + /** + * Tests setup. + */ + @Override + public void setUp() throws Exception { + super.setUp(); + + context.turnOffAuthorisationSystem(); + + user = EPersonBuilder.createEPerson(context) + .withEmail("user@example.com") + .withPassword(password) + .build(); + + anotherUser = EPersonBuilder.createEPerson(context) + .withEmail("anotherUser@example.com") + .withPassword(password) + .build(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + personCollection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Profile Collection") + .withEntityType("Person") + .withSubmitterGroup(user) + .withTemplateItem() + .build(); + + administrators = groupService.findByName(context, Group.ADMIN); + +// itemService.addMetadata(context, personCollection.getTemplateItem(), "cris", "policy", +// "group", null, administrators.getName()); + + configurationService.setProperty("researcher-profile.collection.uuid", personCollection.getID().toString()); + configurationService.setProperty("claimable.entityType", "Person"); + + context.setCurrentUser(user); + + context.restoreAuthSystemState(); + + } + + /** + * Verify that the findById endpoint returns the own profile. + * + * @throws Exception + */ + @Test + public void testFindById() throws Exception { + + UUID id = user.getID(); + String name = user.getFullName(); + + String authToken = getAuthToken(user.getEmail(), password); + + context.turnOffAuthorisationSystem(); + + ItemBuilder.createItem(context, personCollection) + .withDspaceObjectOwner(name, id.toString()) + .build(); + + context.restoreAuthSystemState(); + + getClient(authToken).perform(get("/api/eperson/profiles/{id}", id)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(id.toString()))) + .andExpect(jsonPath("$.visible", is(true))) + .andExpect(jsonPath("$.type", is("profile"))) + .andExpect(jsonPath("$", matchLinks("http://localhost/api/eperson/profiles/" + id, "item", "eperson"))); + + getClient(authToken).perform(get("/api/eperson/profiles/{id}/item", id)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("item"))) + .andExpect(jsonPath("$.metadata", matchMetadata("dspace.object.owner", name, id.toString(), 0))) + .andExpect(jsonPath("$.metadata", matchMetadata("dspace.entity.type", "Person", 0))); + + getClient(authToken).perform(get("/api/eperson/profiles/{id}/eperson", id)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("eperson"))) + .andExpect(jsonPath("$.name", is(name))); + + } + + /** + * Verify that the an admin user can call the findById endpoint to get a + * profile. + * + * @throws Exception + */ + @Test + public void testFindByIdWithAdmin() throws Exception { + + UUID id = user.getID(); + String name = user.getFullName(); + + String authToken = getAuthToken(admin.getEmail(), password); + + context.turnOffAuthorisationSystem(); + + ItemBuilder.createItem(context, personCollection) + .withDspaceObjectOwner(name, id.toString()) + .build(); + + context.restoreAuthSystemState(); + + getClient(authToken).perform(get("/api/eperson/profiles/{id}", id)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(id.toString()))) + .andExpect(jsonPath("$.visible", is(true))) + .andExpect(jsonPath("$.type", is("profile"))) + .andExpect(jsonPath("$", matchLinks("http://localhost/api/eperson/profiles/" + id, "item", "eperson"))); + + getClient(authToken).perform(get("/api/eperson/profiles/{id}/item", id)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("item"))) + .andExpect(jsonPath("$.metadata", matchMetadata("dspace.object.owner", name, id.toString(), 0))) + .andExpect(jsonPath("$.metadata", matchMetadata("dspace.entity.type", "Person", 0))); + + getClient(authToken).perform(get("/api/eperson/profiles/{id}/eperson", id)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("eperson"))) + .andExpect(jsonPath("$.name", is(name))); + + } + + /** + * Verify that a standard user can't access the profile of another user. + * + * @throws Exception + */ + @Test + public void testFindByIdWithoutOwnerUser() throws Exception { + + UUID id = user.getID(); + String name = user.getFullName(); + + String authToken = getAuthToken(anotherUser.getEmail(), password); + + context.turnOffAuthorisationSystem(); + + ItemBuilder.createItem(context, personCollection) + .withDspaceObjectOwner(name, id.toString()) + .build(); + + context.restoreAuthSystemState(); + + getClient(authToken).perform(get("/api/eperson/profiles/{id}", id)) + .andExpect(status().isForbidden()); + + getClient(authToken).perform(get("/api/eperson/profiles/{id}/item", id)) + .andExpect(status().isForbidden()); + + getClient(authToken).perform(get("/api/eperson/profiles/{id}/eperson", id)) + .andExpect(status().isForbidden()); + + } + + /** + * Verify that the createAndReturn endpoint create a new researcher profile. + * + * @throws Exception + */ + @Test + public void testCreateAndReturn() throws Exception { + + String id = user.getID().toString(); + String name = user.getName(); + + String authToken = getAuthToken(user.getEmail(), password); + + getClient(authToken).perform(post("/api/eperson/profiles/") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id", is(id.toString()))) + .andExpect(jsonPath("$.visible", is(false))) + .andExpect(jsonPath("$.type", is("profile"))) + .andExpect(jsonPath("$", matchLinks("http://localhost/api/eperson/profiles/" + id, "item", "eperson"))); + + getClient(authToken).perform(get("/api/eperson/profiles/{id}", id)) + .andExpect(status().isOk()); + + getClient(authToken).perform(get("/api/eperson/profiles/{id}/item", id)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("item"))) + .andExpect(jsonPath("$.metadata", matchMetadata("dspace.object.owner", name, id.toString(), 0))) + //.andExpect(jsonPath("$.metadata", matchMetadata("cris.policy.group", administrators.getName(), +// UUIDUtils.toString(administrators.getID()), 0))) + .andExpect(jsonPath("$.metadata", matchMetadata("dspace.entity.type", "Person", 0))); + + getClient(authToken).perform(get("/api/eperson/profiles/{id}/eperson", id)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("eperson"))) + .andExpect(jsonPath("$.name", is(name))); + } + + /** + * Verify that an admin can call the createAndReturn endpoint to store a new + * researcher profile related to another user. + * + * @throws Exception + */ + @Test + public void testCreateAndReturnWithAdmin() throws Exception { + + String id = user.getID().toString(); + String name = user.getName(); + + configurationService.setProperty("researcher-profile.collection.uuid", null); + + String authToken = getAuthToken(admin.getEmail(), password); + + getClient(authToken).perform(post("/api/eperson/profiles/") + .param("eperson", id) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id", is(id.toString()))) + .andExpect(jsonPath("$.visible", is(false))) + .andExpect(jsonPath("$.type", is("profile"))) + .andExpect(jsonPath("$", matchLinks("http://localhost/api/eperson/profiles/" + id, "item", "eperson"))); + + getClient(authToken).perform(get("/api/eperson/profiles/{id}", id)) + .andExpect(status().isOk()); + + getClient(authToken).perform(get("/api/eperson/profiles/{id}/item", id)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("item"))) + .andExpect(jsonPath("$.metadata", matchMetadata("dspace.object.owner", name, id.toString(), 0))) +// .andExpect(jsonPath("$.metadata", matchMetadata("cris.policy.group", administrators.getName(), +// UUIDUtils.toString(administrators.getID()), 0))) + .andExpect(jsonPath("$.metadata", matchMetadata("dspace.entity.type", "Person", 0))); + + getClient(authToken).perform(get("/api/eperson/profiles/{id}/eperson", id)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("eperson"))) + .andExpect(jsonPath("$.name", is(name))); + + authToken = getAuthToken(user.getEmail(), password); + + getClient(authToken).perform(get("/api/eperson/profiles/{id}", id)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(id.toString()))) + .andExpect(jsonPath("$.visible", is(false))) + .andExpect(jsonPath("$.type", is("profile"))) + .andExpect(jsonPath("$", matchLinks("http://localhost/api/eperson/profiles/" + id, "item", "eperson"))); + } + + /** + * Verify that a standard user can't call the createAndReturn endpoint to store + * a new researcher profile related to another user. + * + * @throws Exception + */ + @Test + public void testCreateAndReturnWithoutOwnUser() throws Exception { + + String authToken = getAuthToken(anotherUser.getEmail(), password); + + getClient(authToken).perform(post("/api/eperson/profiles/") + .param("eperson", user.getID().toString()) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isForbidden()); + + } + + /** + * Verify that a conflict occurs if an user that have already a profile call the + * createAndReturn endpoint. + * + * @throws Exception + */ + @Test + public void testCreateAndReturnWithProfileAlreadyAssociated() throws Exception { + + String id = user.getID().toString(); + String authToken = getAuthToken(user.getEmail(), password); + + getClient(authToken).perform(post("/api/eperson/profiles/") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id", is(id.toString()))) + .andExpect(jsonPath("$.visible", is(false))) + .andExpect(jsonPath("$.type", is("profile"))); + + getClient(authToken).perform(post("/api/eperson/profiles/") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.id", is(id.toString()))) + .andExpect(jsonPath("$.visible", is(false))) + .andExpect(jsonPath("$.type", is("profile"))); + + } + + /** + * Verify that an unprocessable entity status is back when the createAndReturn + * is called to create a profile for an unknown user. + * + * @throws Exception + */ + @Test + public void testCreateAndReturnWithUnknownEPerson() throws Exception { + + String unknownId = UUID.randomUUID().toString(); + String authToken = getAuthToken(admin.getEmail(), password); + + getClient(authToken).perform(post("/api/eperson/profiles/") + .param("eperson", unknownId) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isUnprocessableEntity()); + } + + /** + * Verify that a user can delete his profile using the delete endpoint. + * + * @throws Exception + */ + @Test + public void testDelete() throws Exception { + + configurationService.setProperty("researcher-profile.hard-delete.enabled", false); + + String id = user.getID().toString(); + String authToken = getAuthToken(user.getEmail(), password); + AtomicReference itemIdRef = new AtomicReference(); + + getClient(authToken).perform(post("/api/eperson/profiles/") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isCreated()); + + getClient(authToken).perform(get("/api/eperson/profiles/{id}", id)) + .andExpect(status().isOk()); + + getClient(authToken).perform(get("/api/eperson/profiles/{id}/item", id)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasJsonPath("$.metadata", matchMetadataNotEmpty("dspace.object.owner")))) + .andDo(result -> itemIdRef.set(fromString(read(result.getResponse().getContentAsString(), "$.id")))); + + getClient(authToken).perform(delete("/api/eperson/profiles/{id}", id)) + .andExpect(status().isNoContent()); + + getClient(authToken).perform(get("/api/eperson/profiles/{id}", id)) + .andExpect(status().isNotFound()); + + getClient(authToken).perform(get("/api/core/items/{id}", itemIdRef.get())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasJsonPath("$.metadata", matchMetadataDoesNotExist("dspace.object.owner")))); + + } + + /** + * Verify that a user can hard delete his profile using the delete endpoint. + * + * @throws Exception + */ + @Test + public void testHardDelete() throws Exception { + + configurationService.setProperty("researcher-profile.hard-delete.enabled", true); + + String id = user.getID().toString(); + String authToken = getAuthToken(user.getEmail(), password); + AtomicReference itemIdRef = new AtomicReference(); + + getClient(authToken).perform(post("/api/eperson/profiles/") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isCreated()); + + getClient(authToken).perform(get("/api/eperson/profiles/{id}", id)) + .andExpect(status().isOk()); + + getClient(authToken).perform(get("/api/eperson/profiles/{id}/item", id)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasJsonPath("$.metadata", matchMetadataNotEmpty("dspace.object.owner")))) + .andDo(result -> itemIdRef.set(fromString(read(result.getResponse().getContentAsString(), "$.id")))); + + getClient(authToken).perform(delete("/api/eperson/profiles/{id}", id)) + .andExpect(status().isNoContent()); + + getClient(authToken).perform(get("/api/eperson/profiles/{id}", id)) + .andExpect(status().isNotFound()); + + getClient(authToken).perform(get("/api/core/items/{id}", itemIdRef.get())) + .andExpect(status().isNotFound()); + + } + + /** + * Verify that an admin can delete a profile of another user using the delete + * endpoint. + * + * @throws Exception + */ + @Test + public void testDeleteWithAdmin() throws Exception { + + String id = user.getID().toString(); + + String adminToken = getAuthToken(admin.getEmail(), password); + String userToken = getAuthToken(user.getEmail(), password); + + getClient(userToken).perform(post("/api/eperson/profiles/") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isCreated()); + + getClient(userToken).perform(get("/api/eperson/profiles/{id}", id)) + .andExpect(status().isOk()); + + getClient(adminToken).perform(delete("/api/eperson/profiles/{id}", id)) + .andExpect(status().isNoContent()); + + getClient(adminToken).perform(get("/api/eperson/profiles/{id}", id)) + .andExpect(status().isNotFound()); + + getClient(userToken).perform(get("/api/eperson/profiles/{id}", id)) + .andExpect(status().isNotFound()); + } + + /** + * Verify that an user can delete his profile using the delete endpoint even if + * was created by an admin. + * + * @throws Exception + */ + @Test + public void testDeleteProfileCreatedByAnAdmin() throws Exception { + + String id = user.getID().toString(); + + String adminToken = getAuthToken(admin.getEmail(), password); + String userToken = getAuthToken(user.getEmail(), password); + + getClient(adminToken).perform(post("/api/eperson/profiles/") + .param("eperson", id) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isCreated()); + + getClient(adminToken).perform(get("/api/eperson/profiles/{id}", id)) + .andExpect(status().isOk()); + + getClient(userToken).perform(delete("/api/eperson/profiles/{id}", id)) + .andExpect(status().isNoContent()); + + getClient(userToken).perform(get("/api/eperson/profiles/{id}", id)) + .andExpect(status().isNotFound()); + + getClient(adminToken).perform(get("/api/eperson/profiles/{id}", id)) + .andExpect(status().isNotFound()); + + } + + /** + * Verify that a standard user can't call the delete endpoint to delete a + * researcher profile related to another user. + * + * @throws Exception + */ + @Test + public void testDeleteWithoutOwnUser() throws Exception { + + String id = user.getID().toString(); + + String userToken = getAuthToken(user.getEmail(), password); + String anotherUserToken = getAuthToken(anotherUser.getEmail(), password); + + getClient(userToken).perform(post("/api/eperson/profiles/") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isCreated()); + + getClient(userToken).perform(get("/api/eperson/profiles/{id}", id)) + .andExpect(status().isOk()); + + getClient(anotherUserToken).perform(delete("/api/eperson/profiles/{id}", id)) + .andExpect(status().isForbidden()); + + getClient(userToken).perform(get("/api/eperson/profiles/{id}", id)) + .andExpect(status().isOk()); + + } + + /** + * Verify that an user can change the profile visibility using the patch endpoint. + * + * @throws Exception + */ + @Test + public void testPatchToChangeVisibleAttribute() throws Exception { + + String id = user.getID().toString(); + String authToken = getAuthToken(user.getEmail(), password); + + getClient(authToken).perform(post("/api/eperson/profiles/") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.visible", is(false))); + + getClient(authToken).perform(get("/api/eperson/profiles/{id}", id)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.visible", is(false))); + + // change the visibility to true + List operations = asList(new ReplaceOperation("/visible", true)); + + getClient(authToken).perform(patch("/api/eperson/profiles/{id}", id) + .content(getPatchContent(operations)) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.visible", is(true))); + + getClient(authToken).perform(get("/api/eperson/profiles/{id}", id)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.visible", is(true))); + + // change the visibility to false + operations = asList(new ReplaceOperation("/visible", false)); + + getClient(authToken).perform(patch("/api/eperson/profiles/{id}", id) + .content(getPatchContent(operations)) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.visible", is(false))); + + getClient(authToken).perform(get("/api/eperson/profiles/{id}", id)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.visible", is(false))); + + } + + /** + * Verify that an user can not change the profile visibility of another user + * using the patch endpoint. + * + * @throws Exception + */ + @Test + public void testPatchToChangeVisibleAttributeWithoutOwnUser() throws Exception { + + String id = user.getID().toString(); + + String userToken = getAuthToken(user.getEmail(), password); + String anotherUserToken = getAuthToken(anotherUser.getEmail(), password); + + getClient(userToken).perform(post("/api/eperson/profiles/") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.visible", is(false))); + + getClient(userToken).perform(get("/api/eperson/profiles/{id}", id)) + .andExpect(status().isOk()); + + // try to change the visibility to true + List operations = asList(new ReplaceOperation("/visible", true)); + + getClient(anotherUserToken).perform(patch("/api/eperson/profiles/{id}", id) + .content(getPatchContent(operations)) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isForbidden()); + + getClient(userToken).perform(get("/api/eperson/profiles/{id}", id)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.visible", is(false))); + } + + /** + * Verify that an admin can change the profile visibility of another user using + * the patch endpoint. + * + * @throws Exception + */ + @Test + public void testPatchToChangeVisibleAttributeWithAdmin() throws Exception { + + String id = user.getID().toString(); + + String adminToken = getAuthToken(admin.getEmail(), password); + String userToken = getAuthToken(user.getEmail(), password); + + getClient(userToken).perform(post("/api/eperson/profiles/") + .param("eperson", id) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isCreated()); + + getClient(userToken).perform(get("/api/eperson/profiles/{id}", id)) + .andExpect(status().isOk()); + + // change the visibility to true + List operations = asList(new ReplaceOperation("/visible", true)); + + getClient(adminToken).perform(patch("/api/eperson/profiles/{id}", id) + .content(getPatchContent(operations)) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.visible", is(true))); + + getClient(userToken).perform(get("/api/eperson/profiles/{id}", id)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.visible", is(true))); + } + + /** + * Verify that an user can change the visibility of his profile using the patch + * endpoint even if was created by an admin. + * + * @throws Exception + */ + @Test + public void testPatchToChangeVisibilityOfProfileCreatedByAnAdmin() throws Exception { + + String id = user.getID().toString(); + + String adminToken = getAuthToken(admin.getEmail(), password); + String userToken = getAuthToken(user.getEmail(), password); + + getClient(adminToken).perform(post("/api/eperson/profiles/") + .param("eperson", id) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isCreated()); + + getClient(adminToken).perform(get("/api/eperson/profiles/{id}", id)) + .andExpect(status().isOk()); + + // change the visibility to true + List operations = asList(new ReplaceOperation("/visible", true)); + + getClient(userToken).perform(patch("/api/eperson/profiles/{id}", id) + .content(getPatchContent(operations)) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.visible", is(true))); + + getClient(userToken).perform(get("/api/eperson/profiles/{id}", id)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.visible", is(true))); + } + + /** + * Verify that after an user login an automatic claim between the logged eperson + * and possible profiles without eperson is done. + * + * @throws Exception + */ + @Test + public void testAutomaticProfileClaimByEmail() throws Exception { + + String id = user.getID().toString(); + + String adminToken = getAuthToken(admin.getEmail(), password); + + // create and delete a profile + getClient(adminToken).perform(post("/api/eperson/profiles/") + .param("eperson", id) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isCreated()); + + String firstItemId = getItemIdByProfileId(adminToken, id); + + MetadataValueRest valueToAdd = new MetadataValueRest(user.getEmail()); + List operations = asList(new AddOperation("/metadata/person.email", valueToAdd)); + + getClient(adminToken).perform(patch(BASE_REST_SERVER_URL + "/api/core/items/{id}", firstItemId) + .contentType(MediaType.APPLICATION_JSON) + .content(getPatchContent(operations))) + .andExpect(status().isOk()); + + getClient(adminToken).perform(delete("/api/eperson/profiles/{id}", id)) + .andExpect(status().isNoContent()); + + getClient(adminToken).perform(get("/api/eperson/profiles/{id}", id)) + .andExpect(status().isNotFound()); + + // the automatic claim is done after the user login + String userToken = getAuthToken(user.getEmail(), password); + + getClient(userToken).perform(get("/api/eperson/profiles/{id}", id)) + .andExpect(status().isOk()); + + // the profile item should be the same + String secondItemId = getItemIdByProfileId(adminToken, id); + assertEquals("The item should be the same", firstItemId, secondItemId); + + } + + @Test + public void testNoAutomaticProfileClaimOccursIfManyClaimableItemsAreFound() throws Exception { + + context.turnOffAuthorisationSystem(); + + EPerson ePerson = EPersonBuilder.createEPerson(context) + .withCanLogin(true) + .withNameInMetadata("Test", "User") + .withPassword(password) + .withEmail("test@email.it") + .build(); + + ItemBuilder.createItem(context, personCollection) + .withTitle("Test User") + .build(); + + ItemBuilder.createItem(context, personCollection) + .withTitle("Test User 2") + .build(); + + context.restoreAuthSystemState(); + + String epersonId = ePerson.getID().toString(); + + getClient(getAuthToken(ePerson.getEmail(), password)) + .perform(get("/api/eperson/profiles/{id}", epersonId)) + .andExpect(status().isNotFound()); + + } + + @Test + public void testNoAutomaticProfileClaimOccursIfTheUserHasAlreadyAProfile() throws Exception { + + context.turnOffAuthorisationSystem(); + + EPerson ePerson = EPersonBuilder.createEPerson(context) + .withCanLogin(true) + .withNameInMetadata("Test", "User") + .withPassword(password) + .withEmail("test@email.it") + .build(); + + context.restoreAuthSystemState(); + + String epersonId = ePerson.getID().toString(); + + String token = getAuthToken(ePerson.getEmail(), password); + + getClient(token).perform(post("/api/eperson/profiles/") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isCreated()); + + getClient(token).perform(get("/api/eperson/profiles/{id}", epersonId)) + .andExpect(status().isOk()); + + String profileItemId = getItemIdByProfileId(token, epersonId); + + context.turnOffAuthorisationSystem(); + + ItemBuilder.createItem(context, personCollection) + .withTitle("Test User") + .build(); + + context.restoreAuthSystemState(); + + token = getAuthToken(ePerson.getEmail(), password); + + String newProfileItemId = getItemIdByProfileId(token, epersonId); + assertEquals("The item should be the same", newProfileItemId, profileItemId); + + } + + @Test + public void testNoAutomaticProfileClaimOccursIfTheFoundProfileIsAlreadyClaimed() throws Exception { + + context.turnOffAuthorisationSystem(); + + EPerson ePerson = EPersonBuilder.createEPerson(context) + .withCanLogin(true) + .withNameInMetadata("Test", "User") + .withPassword(password) + .withEmail("test@email.it") + .build(); + + ItemBuilder.createItem(context, personCollection) + .withTitle("Admin User") + .withPersonEmail("test@email.it") + .withDspaceObjectOwner("Admin User", admin.getID().toString()) + .build(); + + context.restoreAuthSystemState(); + + String epersonId = ePerson.getID().toString(); + + String token = getAuthToken(ePerson.getEmail(), password); + + getClient(token).perform(get("/api/eperson/profiles/{id}", epersonId)) + .andExpect(status().isNotFound()); + + } + + @Test + public void researcherProfileClaim() throws Exception { + String id = user.getID().toString(); + String name = user.getName(); + + context.turnOffAuthorisationSystem(); + + final Item person = ItemBuilder.createItem(context, personCollection) + .withTitle("dc.title") + .build(); + + final Item otherPerson = ItemBuilder.createItem(context, personCollection) + .withTitle("dc.title") + .build(); + + context.restoreAuthSystemState(); + + String authToken = getAuthToken(user.getEmail(), password); + + getClient(authToken).perform(post("/api/eperson/profiles/") + .contentType(TEXT_URI_LIST) + .content("http://localhost:8080/server/api/core/items/" + person.getID().toString())) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id", is(id))) + .andExpect(jsonPath("$.type", is("profile"))) + .andExpect(jsonPath("$", + matchLinks("http://localhost/api/eperson/profiles/" + user.getID(), "item", "eperson"))); + + getClient(authToken).perform(get("/api/eperson/profiles/{id}", id)) + .andExpect(status().isOk()); + + getClient(authToken).perform(get("/api/eperson/profiles/{id}/item", id)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("item"))) + .andExpect(jsonPath("$.metadata", matchMetadata("dspace.object.owner", name, id, 0))) + .andExpect(jsonPath("$.metadata", matchMetadata("dspace.entity.type", "Person", 0))); + + getClient(authToken).perform(get("/api/eperson/profiles/{id}/eperson", id)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("eperson"))) + .andExpect(jsonPath("$.name", is(name))); + + // trying to claim another profile + getClient(authToken).perform(post("/api/eperson/profiles/") + .contentType(TEXT_URI_LIST) + .content("http://localhost:8080/server/api/core/items/" + otherPerson.getID().toString())) + .andExpect(status().isConflict()); + + // other person trying to claim same profile + context.turnOffAuthorisationSystem(); + EPerson ePerson = EPersonBuilder.createEPerson(context) + .withCanLogin(true) + .withEmail("foo@bar.baz") + .withPassword(password) + .withNameInMetadata("Test", "User") + .build(); + + context.restoreAuthSystemState(); + + final String ePersonToken = getAuthToken(ePerson.getEmail(), password); + + getClient(ePersonToken).perform(post("/api/eperson/profiles/") + .contentType(TEXT_URI_LIST) + .content("http://localhost:8080/server/api/core/items/" + person.getID().toString())) + .andExpect(status().isBadRequest()); + + getClient(authToken).perform(delete("/api/eperson/profiles/{id}", id)) + .andExpect(status().isNoContent()); + } + + @Test + public void claimForNotAllowedEntityType() throws Exception { + String id = user.getID().toString(); + String name = user.getName(); + + context.turnOffAuthorisationSystem(); + + final Collection publications = CollectionBuilder.createCollection(context, parentCommunity) + .withEntityType("Publication") + .build(); + + final Item publication = ItemBuilder.createItem(context, publications) + .withTitle("title") + .build(); + + context.restoreAuthSystemState(); + + String authToken = getAuthToken(user.getEmail(), password); + + getClient(authToken).perform(post("/api/eperson/profiles/") + .contentType(TEXT_URI_LIST) + .content("http://localhost:8080/server/api/core/items/" + publication.getID().toString())) + .andExpect(status().isBadRequest()); + } + + @Test + public void testCloneFromExternalSourceRecordNotFound() throws Exception { + + String authToken = getAuthToken(user.getEmail(), password); + + getClient(authToken) + .perform(post("/api/eperson/profiles/").contentType(TEXT_URI_LIST) + .content("http://localhost:8080/server/api/integration/externalsources/orcid/entryValues/FAKE")) + .andExpect(status().isBadRequest()); + } + + @Test + public void testCloneFromExternalSourceMultipleUri() throws Exception { + + String authToken = getAuthToken(user.getEmail(), password); + + getClient(authToken) + .perform(post("/api/eperson/profiles/").contentType(TEXT_URI_LIST) + .content("http://localhost:8080/server/api/integration/externalsources/orcid/entryValues/id \n " + + "http://localhost:8080/server/api/integration/externalsources/dspace/entryValues/id")) + .andExpect(status().isBadRequest()); + + } + + @Test + public void testCloneFromExternalProfileAlreadyAssociated() throws Exception { + + String id = user.getID().toString(); + String authToken = getAuthToken(user.getEmail(), password); + + getClient(authToken).perform(post("/api/eperson/profiles/").contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isCreated()).andExpect(jsonPath("$.id", is(id.toString()))) + .andExpect(jsonPath("$.visible", is(false))).andExpect(jsonPath("$.type", is("profile"))); + + getClient(authToken) + .perform(post("/api/eperson/profiles/").contentType(TEXT_URI_LIST) + .content("http://localhost:8080/server/api/integration/externalsources/orcid/entryValues/id")) + .andExpect(status().isConflict()); + } + + @Test + public void testCloneFromExternalCollectionNotSet() throws Exception { + + configurationService.setProperty("researcher-profile.collection.uuid", "not-existing"); + String id = user.getID().toString(); + String authToken = getAuthToken(user.getEmail(), password); + + getClient(authToken).perform(post("/api/eperson/profiles/").contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isCreated()).andExpect(jsonPath("$.id", is(id.toString()))) + .andExpect(jsonPath("$.visible", is(false))).andExpect(jsonPath("$.type", is("profile"))); + + getClient(authToken) + .perform(post("/api/eperson/profiles/").contentType(TEXT_URI_LIST) + .content("http://localhost:8080/server/api/integration/externalsources/orcid/entryValues/id \n " + + "http://localhost:8080/server/api/integration/externalsources/dspace/entryValues/id")) + .andExpect(status().isBadRequest()); + } + + private String getItemIdByProfileId(String token, String id) throws SQLException, Exception { + MvcResult result = getClient(token).perform(get("/api/eperson/profiles/{id}/item", id)) + .andExpect(status().isOk()) + .andReturn(); + + return readAttributeFromResponse(result, "$.id"); + } + + private T readAttributeFromResponse(MvcResult result, String attribute) throws UnsupportedEncodingException { + return JsonPath.read(result.getResponse().getContentAsString(), attribute); + } +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/MetadataMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/MetadataMatcher.java index 0b6c8c972a45..4a6ca96dfe3e 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/MetadataMatcher.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/MetadataMatcher.java @@ -15,6 +15,7 @@ import static org.hamcrest.Matchers.not; import org.hamcrest.Matcher; +import org.hamcrest.Matchers; import org.hamcrest.core.StringEndsWith; /** @@ -67,6 +68,22 @@ public static Matcher matchMetadata(String key, String value, in return hasJsonPath("$.['" + key + "'][" + position + "].value", is(value)); } + /** + * Gets a matcher to ensure a given value is present at a specific position in + * the list of values for a given key. + * + * @param key the metadata key. + * @param value the value that must be present. + * @param authority the authority that must be present. + * @param position the position it must be present at. + * @return the matcher. + */ + public static Matcher matchMetadata(String key, String value, String authority, int position) { + Matcher hasValue = hasJsonPath("$.['" + key + "'][" + position + "].value", is(value)); + Matcher hasAuthority = hasJsonPath("$.['" + key + "'][" + position + "].authority", is(authority)); + return Matchers.allOf(hasValue, hasAuthority); + } + /** * Gets a matcher to ensure a given key is not present. * diff --git a/dspace/config/modules/authority.cfg b/dspace/config/modules/authority.cfg new file mode 100644 index 000000000000..664730a8bf96 --- /dev/null +++ b/dspace/config/modules/authority.cfg @@ -0,0 +1,15 @@ +#---------------------------------------------------------------# +#----------------- AUTHORITY CONFIGURATIONS --------------------# +#---------------------------------------------------------------# +# These configs are used by the authority framework # +#---------------------------------------------------------------# + +##### Authority Control Settings ##### + +plugin.named.org.dspace.content.authority.ChoiceAuthority = \ +org.dspace.content.authority.EPersonAuthority = EPersonAuthority + + +choices.plugin.dspace.object.owner = EPersonAuthority +choices.presentation.dspace.object.owner = suggest +authority.controlled.dspace.object.owner = true \ No newline at end of file diff --git a/dspace/config/registries/dspace-types.xml b/dspace/config/registries/dspace-types.xml index ab0c1bc40fc4..a252fbced309 100644 --- a/dspace/config/registries/dspace-types.xml +++ b/dspace/config/registries/dspace-types.xml @@ -43,4 +43,12 @@ enabled Stores a boolean text value (true or false) to indicate if the iiif feature is enabled or not for the dspace object. If absent the value is derived from the parent dspace object + + + dspace + object + owner + Stores the reference to the eperson that own the item + + diff --git a/dspace/config/spring/api/core-services.xml b/dspace/config/spring/api/core-services.xml index 591a4ef3f4e7..5d5157273ba1 100644 --- a/dspace/config/spring/api/core-services.xml +++ b/dspace/config/spring/api/core-services.xml @@ -63,6 +63,8 @@ + + + + + + + + + + + + \ No newline at end of file From c25bdd12222e3957d2e4a1c798c21018656dcc58 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Tue, 29 Mar 2022 17:14:15 +0200 Subject: [PATCH 0077/1846] [CST-5306] Migrate Researcher Profile (REST) . --- dspace/config/dspace.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index d5c28da0964c..269955fe8bb3 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -1628,3 +1628,4 @@ include = ${module_dir}/translator.cfg include = ${module_dir}/usage-statistics.cfg include = ${module_dir}/versioning.cfg include = ${module_dir}/workflow.cfg +include = ${module_dir}/authority.cfg \ No newline at end of file From 3210d9ce38176c6007eeab05f5a9d369548a3647 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Wed, 30 Mar 2022 13:19:07 +0200 Subject: [PATCH 0078/1846] [CST-5288] Add CorsMapping for actuator endpoint --- .../java/org/dspace/app/rest/Application.java | 44 +++++++++++-------- .../configuration/ActuatorConfiguration.java | 4 ++ 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/Application.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/Application.java index e0ea0cccb0af..29d6853f4eb0 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/Application.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/Application.java @@ -12,6 +12,7 @@ import java.util.List; import javax.servlet.Filter; +import org.dspace.app.rest.configuration.ActuatorConfiguration; import org.dspace.app.rest.filter.DSpaceRequestContextFilter; import org.dspace.app.rest.model.hateoas.DSpaceLinkRelationProvider; import org.dspace.app.rest.parameter.resolver.SearchFilterResolver; @@ -65,6 +66,9 @@ public class Application extends SpringBootServletInitializer { @Autowired private ApplicationConfig configuration; + @Autowired + private ActuatorConfiguration actuatorConfiguration; + @Scheduled(cron = "${sitemap.cron:-}") public void generateSitemap() throws IOException, SQLException { GenerateSitemaps.generateSitemapsScheduled(); @@ -167,29 +171,31 @@ public void addCorsMappings(@NonNull CorsRegistry registry) { boolean corsAllowCredentials = configuration.getCorsAllowCredentials(); boolean iiifAllowCredentials = configuration.getIiifAllowCredentials(); + if (corsAllowedOrigins != null) { - registry.addMapping("/api/**").allowedMethods(CorsConfiguration.ALL) - // Set Access-Control-Allow-Credentials to "true" and specify which origins are valid - // for our Access-Control-Allow-Origin header - // for our Access-Control-Allow-Origin header - .allowCredentials(corsAllowCredentials).allowedOrigins(corsAllowedOrigins) - // Allow list of request preflight headers allowed to be sent to us from the client - .allowedHeaders("Accept", "Authorization", "Content-Type", "Origin", "X-On-Behalf-Of", - "X-Requested-With", "X-XSRF-TOKEN", "X-CORRELATION-ID", "X-REFERRER") - // Allow list of response headers allowed to be sent by us (the server) to the client - .exposedHeaders("Authorization", "DSPACE-XSRF-TOKEN", "Location", "WWW-Authenticate"); + addCorsMapping(registry, "/api/**", corsAllowedOrigins, corsAllowCredentials); + addCorsMapping(registry, actuatorConfiguration.getActuatorBasePath() + "/**", + corsAllowedOrigins, corsAllowCredentials); } + if (iiifAllowedOrigins != null) { - registry.addMapping("/iiif/**").allowedMethods(CorsConfiguration.ALL) - // Set Access-Control-Allow-Credentials to "true" and specify which origins are valid - // for our Access-Control-Allow-Origin header - .allowCredentials(iiifAllowCredentials).allowedOrigins(iiifAllowedOrigins) - // Allow list of request preflight headers allowed to be sent to us from the client - .allowedHeaders("Accept", "Authorization", "Content-Type", "Origin", "X-On-Behalf-Of", - "X-Requested-With", "X-XSRF-TOKEN", "X-CORRELATION-ID", "X-REFERRER") - // Allow list of response headers allowed to be sent by us (the server) to the client - .exposedHeaders("Authorization", "DSPACE-XSRF-TOKEN", "Location", "WWW-Authenticate"); + addCorsMapping(registry, "/iiif/**", iiifAllowedOrigins, iiifAllowCredentials); } + + } + + private void addCorsMapping(CorsRegistry registry, String pathPattern, + String[] allowedOrigins, boolean allowCredentials) { + + registry.addMapping(pathPattern).allowedMethods(CorsConfiguration.ALL) + // Set Access-Control-Allow-Credentials to "true" and specify which origins are valid + // for our Access-Control-Allow-Origin header + .allowCredentials(allowCredentials).allowedOrigins(allowedOrigins) + // Allow list of request preflight headers allowed to be sent to us from the client + .allowedHeaders("Accept", "Authorization", "Content-Type", "Origin", "X-On-Behalf-Of", + "X-Requested-With", "X-XSRF-TOKEN", "X-CORRELATION-ID", "X-REFERRER") + // Allow list of response headers allowed to be sent by us (the server) to the client + .exposedHeaders("Authorization", "DSPACE-XSRF-TOKEN", "Location", "WWW-Authenticate"); } /** diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/configuration/ActuatorConfiguration.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/configuration/ActuatorConfiguration.java index f2054a71019e..42a7bfab1570 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/configuration/ActuatorConfiguration.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/configuration/ActuatorConfiguration.java @@ -88,4 +88,8 @@ public GeoIpHealthIndicator geoIpHealthIndicator() { return new GeoIpHealthIndicator(); } + public String getActuatorBasePath() { + return actuatorBasePath; + } + } From 95ba7d805bb2f97e97956e51f81d6f13677e134f Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Wed, 30 Mar 2022 16:29:52 +0200 Subject: [PATCH 0079/1846] [CST-5288] Fixed CORS configuration for actuator endpoints --- .../java/org/dspace/app/rest/Application.java | 45 +++++++++---------- dspace/config/dspace.cfg | 6 +++ 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/Application.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/Application.java index 29d6853f4eb0..71f15d39de50 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/Application.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/Application.java @@ -12,7 +12,6 @@ import java.util.List; import javax.servlet.Filter; -import org.dspace.app.rest.configuration.ActuatorConfiguration; import org.dspace.app.rest.filter.DSpaceRequestContextFilter; import org.dspace.app.rest.model.hateoas.DSpaceLinkRelationProvider; import org.dspace.app.rest.parameter.resolver.SearchFilterResolver; @@ -66,9 +65,6 @@ public class Application extends SpringBootServletInitializer { @Autowired private ApplicationConfig configuration; - @Autowired - private ActuatorConfiguration actuatorConfiguration; - @Scheduled(cron = "${sitemap.cron:-}") public void generateSitemap() throws IOException, SQLException { GenerateSitemaps.generateSitemapsScheduled(); @@ -164,6 +160,7 @@ public WebMvcConfigurer webMvcConfigurer() { @Override public void addCorsMappings(@NonNull CorsRegistry registry) { // Get allowed origins for api and iiif endpoints. + // The actuator endpoints are configured using management.endpoints.web.cors.* properties String[] corsAllowedOrigins = configuration .getCorsAllowedOrigins(configuration.getCorsAllowedOriginsConfig()); String[] iiifAllowedOrigins = configuration @@ -171,31 +168,29 @@ public void addCorsMappings(@NonNull CorsRegistry registry) { boolean corsAllowCredentials = configuration.getCorsAllowCredentials(); boolean iiifAllowCredentials = configuration.getIiifAllowCredentials(); - if (corsAllowedOrigins != null) { - addCorsMapping(registry, "/api/**", corsAllowedOrigins, corsAllowCredentials); - addCorsMapping(registry, actuatorConfiguration.getActuatorBasePath() + "/**", - corsAllowedOrigins, corsAllowCredentials); + registry.addMapping("/api/**").allowedMethods(CorsConfiguration.ALL) + // Set Access-Control-Allow-Credentials to "true" and specify which origins are valid + // for our Access-Control-Allow-Origin header + // for our Access-Control-Allow-Origin header + .allowCredentials(corsAllowCredentials).allowedOrigins(corsAllowedOrigins) + // Allow list of request preflight headers allowed to be sent to us from the client + .allowedHeaders("Accept", "Authorization", "Content-Type", "Origin", "X-On-Behalf-Of", + "X-Requested-With", "X-XSRF-TOKEN", "X-CORRELATION-ID", "X-REFERRER") + // Allow list of response headers allowed to be sent by us (the server) to the client + .exposedHeaders("Authorization", "DSPACE-XSRF-TOKEN", "Location", "WWW-Authenticate"); } - if (iiifAllowedOrigins != null) { - addCorsMapping(registry, "/iiif/**", iiifAllowedOrigins, iiifAllowCredentials); + registry.addMapping("/iiif/**").allowedMethods(CorsConfiguration.ALL) + // Set Access-Control-Allow-Credentials to "true" and specify which origins are valid + // for our Access-Control-Allow-Origin header + .allowCredentials(iiifAllowCredentials).allowedOrigins(iiifAllowedOrigins) + // Allow list of request preflight headers allowed to be sent to us from the client + .allowedHeaders("Accept", "Authorization", "Content-Type", "Origin", "X-On-Behalf-Of", + "X-Requested-With", "X-XSRF-TOKEN", "X-CORRELATION-ID", "X-REFERRER") + // Allow list of response headers allowed to be sent by us (the server) to the client + .exposedHeaders("Authorization", "DSPACE-XSRF-TOKEN", "Location", "WWW-Authenticate"); } - - } - - private void addCorsMapping(CorsRegistry registry, String pathPattern, - String[] allowedOrigins, boolean allowCredentials) { - - registry.addMapping(pathPattern).allowedMethods(CorsConfiguration.ALL) - // Set Access-Control-Allow-Credentials to "true" and specify which origins are valid - // for our Access-Control-Allow-Origin header - .allowCredentials(allowCredentials).allowedOrigins(allowedOrigins) - // Allow list of request preflight headers allowed to be sent to us from the client - .allowedHeaders("Accept", "Authorization", "Content-Type", "Origin", "X-On-Behalf-Of", - "X-Requested-With", "X-XSRF-TOKEN", "X-CORRELATION-ID", "X-REFERRER") - // Allow list of response headers allowed to be sent by us (the server) to the client - .exposedHeaders("Authorization", "DSPACE-XSRF-TOKEN", "Location", "WWW-Authenticate"); } /** diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index 4c0d04dbff9d..e1e3dec830c0 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -1583,6 +1583,12 @@ management.endpoint.health.status.order= down, out-of-service, up-with-issues, u management.health.ping.enabled = false management.health.diskSpace.enabled = false +management.endpoints.web.cors.allowed-origins = ${rest.cors.allowed-origins} +management.endpoints.web.cors.allowed-methods = * +management.endpoints.web.cors.allowed-headers = Accept, Authorization, Content-Type, Origin, X-On-Behalf-Of, X-Requested-With, X-XSRF-TOKEN, X-CORRELATION-ID, X-REFERRER +management.endpoints.web.cors.exposed-headers = Authorization, DSPACE-XSRF-TOKEN, Location, WWW-Authenticate +management.endpoints.web.cors.allow-credentials = true + #------------------------------------------------------------------# #-------------------MODULE CONFIGURATIONS--------------------------# #------------------------------------------------------------------# From d4f95a31bde4f45c75f196d66961436dcec85bee Mon Sep 17 00:00:00 2001 From: eskander Date: Wed, 30 Mar 2022 17:02:28 +0200 Subject: [PATCH 0080/1846] [CST-5306] Migrate Researcher Profile (REST). added ClaimItemFeature. --- .../dspace/app/profile/ResearcherProfile.java | 5 - .../profile/ResearcherProfileServiceImpl.java | 17 -- .../AfterResearcherProfileCreationAction.java | 35 --- .../impl/CanClaimItemFeature.java | 71 +++++++ .../impl/ShowClaimItemFeature.java | 93 ++++++++ .../converter/ResearcherProfileConverter.java | 50 ----- .../rest/login/impl/LoginEventFireAction.java | 46 ---- .../app/rest/model/ResearcherProfileRest.java | 74 ------- .../ResearcherProfileRestRepositoryIT.java | 7 - .../authorization/CanClaimItemFeatureIT.java | 198 +++++++++++++++++ .../authorization/ShowClaimItemFeatureIT.java | 199 ++++++++++++++++++ .../spring/rest/post-logged-in-actions.xml | 3 - 12 files changed, 561 insertions(+), 237 deletions(-) delete mode 100644 dspace-api/src/main/java/org/dspace/app/profile/service/AfterResearcherProfileCreationAction.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanClaimItemFeature.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/ShowClaimItemFeature.java delete mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/login/impl/LoginEventFireAction.java create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanClaimItemFeatureIT.java create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/ShowClaimItemFeatureIT.java diff --git a/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfile.java b/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfile.java index 59f4a0cfff8b..d282f0cb4778 100644 --- a/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfile.java +++ b/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfile.java @@ -62,11 +62,6 @@ public Item getItem() { return item; } -// public Optional getOrcid() { -// return getMetadataValue(item, "person.identifier.orcid") -// .map(metadataValue -> metadataValue.getValue()); -// } - private MetadataValue getDspaceObjectOwnerMetadata(Item item) { return getMetadataValue(item, "dspace.object.owner") .filter(metadata -> UUIDUtils.fromString(metadata.getAuthority()) != null) diff --git a/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfileServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfileServiceImpl.java index 8b9964972ce0..1e81636ff68d 100644 --- a/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfileServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfileServiceImpl.java @@ -25,7 +25,6 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang.StringUtils; import org.dspace.app.exception.ResourceConflictException; -import org.dspace.app.profile.service.AfterResearcherProfileCreationAction; import org.dspace.app.profile.service.ResearcherProfileService; import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.service.AuthorizeService; @@ -88,18 +87,6 @@ public class ResearcherProfileServiceImpl implements ResearcherProfileService { @Autowired private AuthorizeService authorizeService; - @Autowired(required = false) - private List afterCreationActions; - - @PostConstruct - public void postConstruct() { - - if (afterCreationActions == null) { - afterCreationActions = Collections.emptyList(); - } - - } - @Override public ResearcherProfile findById(Context context, UUID id) throws SQLException, AuthorizeException { Assert.notNull(id, "An id must be provided to find a researcher profile"); @@ -133,10 +120,6 @@ public ResearcherProfile createAndReturn(Context context, EPerson ePerson) ResearcherProfile researcherProfile = new ResearcherProfile(item); - for (AfterResearcherProfileCreationAction afterCreationAction : afterCreationActions) { - afterCreationAction.perform(context, researcherProfile, ePerson); - } - return researcherProfile; } diff --git a/dspace-api/src/main/java/org/dspace/app/profile/service/AfterResearcherProfileCreationAction.java b/dspace-api/src/main/java/org/dspace/app/profile/service/AfterResearcherProfileCreationAction.java deleted file mode 100644 index 7bf5c97aa869..000000000000 --- a/dspace-api/src/main/java/org/dspace/app/profile/service/AfterResearcherProfileCreationAction.java +++ /dev/null @@ -1,35 +0,0 @@ -/** - * 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.profile.service; - -import org.dspace.app.profile.ResearcherProfile; -import org.dspace.core.Context; -import org.dspace.eperson.EPerson; - -import java.sql.SQLException; - -/** - * Interface to mark classes that allow to perform additional logic on created - * researcher profile. - * - * @author Luca Giamminonni (luca.giamminonni at 4science.it) - * - */ -public interface AfterResearcherProfileCreationAction { - - /** - * Perform some actions on the given researcher profile and returns the updated - * profile. - * - * @param context the DSpace context - * @param researcherProfile the created researcher profile - * @param owner the EPerson that is owner of the given profile - * @throws SQLException if a SQL error occurs - */ - void perform(Context context, ResearcherProfile researcherProfile, EPerson owner) throws SQLException; -} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanClaimItemFeature.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanClaimItemFeature.java new file mode 100644 index 000000000000..f203247c3671 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanClaimItemFeature.java @@ -0,0 +1,71 @@ +/** + * 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.rest.authorization.impl; + +import java.sql.SQLException; +import java.util.Objects; +import java.util.UUID; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.app.rest.authorization.AuthorizationFeature; +import org.dspace.app.rest.authorization.AuthorizationFeatureDocumentation; +import org.dspace.app.rest.model.BaseObjectRest; +import org.dspace.app.rest.model.ItemRest; +import org.dspace.content.Item; +import org.dspace.content.service.ItemService; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Checks if the given user can claim the given item. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +@Component +@AuthorizationFeatureDocumentation(name = CanClaimItemFeature.NAME, + description = "Used to verify if the given user can request the claim of an item") +public class CanClaimItemFeature implements AuthorizationFeature { + + public static final String NAME = "canClaimItem"; + + @Autowired + private ItemService itemService; + + @Autowired + private ShowClaimItemFeature showClaimItemFeature; + + @Override + @SuppressWarnings("rawtypes") + public boolean isAuthorized(Context context, BaseObjectRest object) throws SQLException { + + if (!showClaimItemFeature.isAuthorized(context, object)) { + return false; + } + + if (!(object instanceof ItemRest) || Objects.isNull(context.getCurrentUser())) { + return false; + } + + String id = ((ItemRest) object).getId(); + Item item = itemService.find(context, UUID.fromString(id)); + + return hasNotOwner(item); + } + + private boolean hasNotOwner(Item item) { + return StringUtils.isBlank(itemService.getMetadata(item, "dspace.object.owner")); + } + + @Override + public String[] getSupportedTypes() { + return new String[] { ItemRest.CATEGORY + "." + ItemRest.NAME }; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/ShowClaimItemFeature.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/ShowClaimItemFeature.java new file mode 100644 index 000000000000..1de02ce331fb --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/ShowClaimItemFeature.java @@ -0,0 +1,93 @@ +/** + * 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.rest.authorization.impl; + +import java.sql.SQLException; +import java.util.Objects; +import java.util.UUID; + +import org.apache.commons.lang3.ArrayUtils; +import org.dspace.app.profile.service.ResearcherProfileService; +import org.dspace.app.rest.authorization.AuthorizationFeature; +import org.dspace.app.rest.authorization.AuthorizationFeatureDocumentation; +import org.dspace.app.rest.model.BaseObjectRest; +import org.dspace.app.rest.model.ItemRest; +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.Item; +import org.dspace.content.service.ItemService; +import org.dspace.core.Context; +import org.dspace.services.ConfigurationService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Checks if the given user can request the claim of an item. Whether or not the + * user can then make the claim is determined by the feature + * {@link CanClaimItemFeature}. + * + * @author Corrado Lombardi (corrado.lombardi at 4science.it) + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + */ +@Component +@AuthorizationFeatureDocumentation(name = ShowClaimItemFeature.NAME, + description = "Used to verify if the given user can request the claim of an item") +public class ShowClaimItemFeature implements AuthorizationFeature { + + public static final String NAME = "showClaimItem"; + private static final Logger LOG = LoggerFactory.getLogger(ShowClaimItemFeature.class); + + private final ItemService itemService; + private final ResearcherProfileService researcherProfileService; + private final ConfigurationService configurationService; + + @Autowired + public ShowClaimItemFeature(ItemService itemService, + ResearcherProfileService researcherProfileService, + ConfigurationService configurationService) { + this.itemService = itemService; + this.researcherProfileService = researcherProfileService; + this.configurationService = configurationService; + } + + @Override + @SuppressWarnings("rawtypes") + public boolean isAuthorized(Context context, BaseObjectRest object) throws SQLException { + + if (!(object instanceof ItemRest) || Objects.isNull(context.getCurrentUser())) { + return false; + } + + String id = ((ItemRest) object).getId(); + Item item = itemService.find(context, UUID.fromString(id)); + + return claimableEntityType(item) && hasNotAlreadyAProfile(context); + } + + private boolean hasNotAlreadyAProfile(Context context) { + try { + return researcherProfileService.findById(context, context.getCurrentUser().getID()) == null; + } catch (SQLException | AuthorizeException e) { + LOG.warn("Error while checking if eperson has a ResearcherProfileAssociated: {}", + e.getMessage(), e); + return false; + } + } + + private boolean claimableEntityType(Item item) { + String[] claimableEntityTypes = configurationService.getArrayProperty("claimable.entityType"); + String entityType = itemService.getEntityType(item); + return ArrayUtils.contains(claimableEntityTypes, entityType); + } + + @Override + public String[] getSupportedTypes() { + return new String[] {ItemRest.CATEGORY + "." + ItemRest.NAME}; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ResearcherProfileConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ResearcherProfileConverter.java index 4eb291d8d18d..70740651059e 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ResearcherProfileConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ResearcherProfileConverter.java @@ -7,22 +7,10 @@ */ package org.dspace.app.rest.converter; -//import static org.dspace.app.orcid.model.OrcidEntityType.FUNDING; -//import static org.dspace.app.orcid.model.OrcidEntityType.PUBLICATION; - -import java.util.List; -import java.util.stream.Collectors; - -//import org.dspace.app.orcid.service.OrcidSynchronizationService; -//import org.dspace.app.profile.OrcidEntitySyncPreference; -//import org.dspace.app.profile.OrcidProfileSyncPreference; -//import org.dspace.app.profile.OrcidSynchronizationMode; import org.dspace.app.profile.ResearcherProfile; import org.dspace.app.rest.model.ResearcherProfileRest; -//import org.dspace.app.rest.model.ResearcherProfileRest.OrcidSynchronizationRest; import org.dspace.app.rest.projection.Projection; import org.dspace.content.Item; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** @@ -35,9 +23,6 @@ @Component public class ResearcherProfileConverter implements DSpaceConverter { -// @Autowired -// private OrcidSynchronizationService orcidSynchronizationService; - @Override public ResearcherProfileRest convert(ResearcherProfile profile, Projection projection) { ResearcherProfileRest researcherProfileRest = new ResearcherProfileRest(); @@ -48,44 +33,9 @@ public ResearcherProfileRest convert(ResearcherProfile profile, Projection proje Item item = profile.getItem(); -// if (orcidSynchronizationService.isLinkedToOrcid(item)) { -// profile.getOrcid().ifPresent(researcherProfileRest::setOrcid); -// -// OrcidSynchronizationRest orcidSynchronization = new OrcidSynchronizationRest(); -// orcidSynchronization.setMode(getMode(item)); -// orcidSynchronization.setProfilePreferences(getProfilePreferences(item)); -// orcidSynchronization.setFundingsPreference(getFundingsPreference(item)); -// orcidSynchronization.setPublicationsPreference(getPublicationsPreference(item)); -// researcherProfileRest.setOrcidSynchronization(orcidSynchronization); -// } - return researcherProfileRest; } -// private String getPublicationsPreference(Item item) { -// return orcidSynchronizationService.getEntityPreference(item, PUBLICATION) -// .map(OrcidEntitySyncPreference::name) -// .orElse(OrcidEntitySyncPreference.DISABLED.name()); -// } -// -// private String getFundingsPreference(Item item) { -// return orcidSynchronizationService.getEntityPreference(item, FUNDING) -// .map(OrcidEntitySyncPreference::name) -// .orElse(OrcidEntitySyncPreference.DISABLED.name()); -// } -// -// private List getProfilePreferences(Item item) { -// return orcidSynchronizationService.getProfilePreferences(item).stream() -// .map(OrcidProfileSyncPreference::name) -// .collect(Collectors.toList()); -// } -// -// private String getMode(Item item) { -// return orcidSynchronizationService.getSynchronizationMode(item) -// .map(OrcidSynchronizationMode::name) -// .orElse(OrcidSynchronizationMode.MANUAL.name()); -// } - @Override public Class getModelClass() { return ResearcherProfile.class; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/login/impl/LoginEventFireAction.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/login/impl/LoginEventFireAction.java deleted file mode 100644 index b5df891d1025..000000000000 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/login/impl/LoginEventFireAction.java +++ /dev/null @@ -1,46 +0,0 @@ -/** - * 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.rest.login.impl; - -import org.dspace.app.rest.login.PostLoggedInAction; -import org.dspace.core.Context; -import org.dspace.eperson.EPerson; -import org.dspace.services.EventService; -import org.dspace.usage.UsageEvent; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletRequestAttributes; - -import javax.servlet.http.HttpServletRequest; - -/** - * Implementation of {@link PostLoggedInAction} that fire an LOGIN event. - * - * @author Luca Giamminonni (luca.giamminonni at 4science.it) - * - */ -public class LoginEventFireAction implements PostLoggedInAction { - - @Autowired - private EventService eventService; - - @Override - public void loggedIn(Context context) { - - HttpServletRequest request = getCurrentRequest(); - EPerson currentUser = context.getCurrentUser(); - - eventService.fireEvent(new UsageEvent(UsageEvent.Action.LOGIN, request, context, currentUser)); - - } - - private HttpServletRequest getCurrentRequest() { - return ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); - } - -} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ResearcherProfileRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ResearcherProfileRest.java index c1d8f692545d..ff789f93237f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ResearcherProfileRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ResearcherProfileRest.java @@ -27,7 +27,6 @@ public class ResearcherProfileRest extends BaseObjectRest { private static final long serialVersionUID = 1L; - // changed from RestModel.CRIS to RestModel.EPERSON public static final String CATEGORY = RestModel.EPERSON; public static final String NAME = "profile"; @@ -36,12 +35,6 @@ public class ResearcherProfileRest extends BaseObjectRest { private boolean visible; -// @JsonInclude(Include.NON_NULL) -// private String orcid; - -// @JsonInclude(Include.NON_NULL) -// private OrcidSynchronizationRest orcidSynchronization; - public boolean isVisible() { return visible; } @@ -50,22 +43,6 @@ public void setVisible(boolean visible) { this.visible = visible; } -// public OrcidSynchronizationRest getOrcidSynchronization() { -// return orcidSynchronization; -// } - -// public void setOrcidSynchronization(OrcidSynchronizationRest orcidSynchronization) { -// this.orcidSynchronization = orcidSynchronization; -// } - -// public String getOrcid() { -// return orcid; -// } -// -// public void setOrcid(String orcid) { -// this.orcid = orcid; -// } - @Override public String getType() { return NAME; @@ -80,55 +57,4 @@ public String getCategory() { public Class getController() { return RestResourceController.class; } - - /** - * Inner class to model ORCID synchronization preferences and mode. - * - * @author Luca Giamminonni (luca.giamminonni at 4science.it) - * - */ -// public static class OrcidSynchronizationRest { -// -// private String mode; -// -// private String publicationsPreference; -// -// private String fundingsPreference; -// -// private List profilePreferences; -// -// public String getMode() { -// return mode; -// } -// -// public void setMode(String mode) { -// this.mode = mode; -// } -// -// public List getProfilePreferences() { -// return profilePreferences; -// } -// -// public void setProfilePreferences(List profilePreferences) { -// this.profilePreferences = profilePreferences; -// } -// -// public String getPublicationsPreference() { -// return publicationsPreference; -// } -// -// public void setPublicationsPreference(String publicationsPreference) { -// this.publicationsPreference = publicationsPreference; -// } -// -// public String getFundingsPreference() { -// return fundingsPreference; -// } -// -// public void setFundingsPreference(String fundingsPreference) { -// this.fundingsPreference = fundingsPreference; -// } -// -// } - } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResearcherProfileRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResearcherProfileRestRepositoryIT.java index f5a460b29892..b316ea3acdd6 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResearcherProfileRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResearcherProfileRestRepositoryIT.java @@ -114,9 +114,6 @@ public void setUp() throws Exception { administrators = groupService.findByName(context, Group.ADMIN); -// itemService.addMetadata(context, personCollection.getTemplateItem(), "cris", "policy", -// "group", null, administrators.getName()); - configurationService.setProperty("researcher-profile.collection.uuid", personCollection.getID().toString()); configurationService.setProperty("claimable.entityType", "Person"); @@ -269,8 +266,6 @@ public void testCreateAndReturn() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$.type", is("item"))) .andExpect(jsonPath("$.metadata", matchMetadata("dspace.object.owner", name, id.toString(), 0))) - //.andExpect(jsonPath("$.metadata", matchMetadata("cris.policy.group", administrators.getName(), -// UUIDUtils.toString(administrators.getID()), 0))) .andExpect(jsonPath("$.metadata", matchMetadata("dspace.entity.type", "Person", 0))); getClient(authToken).perform(get("/api/eperson/profiles/{id}/eperson", id)) @@ -311,8 +306,6 @@ public void testCreateAndReturnWithAdmin() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$.type", is("item"))) .andExpect(jsonPath("$.metadata", matchMetadata("dspace.object.owner", name, id.toString(), 0))) -// .andExpect(jsonPath("$.metadata", matchMetadata("cris.policy.group", administrators.getName(), -// UUIDUtils.toString(administrators.getID()), 0))) .andExpect(jsonPath("$.metadata", matchMetadata("dspace.entity.type", "Person", 0))); getClient(authToken).perform(get("/api/eperson/profiles/{id}/eperson", id)) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanClaimItemFeatureIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanClaimItemFeatureIT.java new file mode 100644 index 000000000000..368d0f1a441c --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanClaimItemFeatureIT.java @@ -0,0 +1,198 @@ +/** + * 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.rest.authorization; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.dspace.app.rest.authorization.impl.CanClaimItemFeature; +import org.dspace.app.rest.converter.ItemConverter; +import org.dspace.app.rest.model.ItemRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.app.rest.utils.Utils; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.content.Collection; +import org.dspace.content.Item; +import org.dspace.services.ConfigurationService; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Test of Profile Claim Authorization Feature implementation. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + */ +public class CanClaimItemFeatureIT extends AbstractControllerIntegrationTest { + + private Item collectionAProfile; + private Item collectionBProfile; + + @Autowired + private ConfigurationService configurationService; + + @Autowired + private ItemConverter itemConverter; + + @Autowired + private Utils utils; + + @Autowired + private AuthorizationFeatureService authorizationFeatureService; + + private AuthorizationFeature canClaimProfileFeature; + + private Collection personCollection; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context).withName("Community").build(); + personCollection = + CollectionBuilder.createCollection(context, parentCommunity).withEntityType("Person") + .withName("claimableA").build(); + final Collection claimableCollectionB = + CollectionBuilder.createCollection(context, parentCommunity).withEntityType("Person") + .withName("claimableB").build(); + + collectionAProfile = ItemBuilder.createItem(context, personCollection).build(); + collectionBProfile = ItemBuilder.createItem(context, claimableCollectionB).build(); + + configurationService.addPropertyValue("claimable.entityType", "Person"); + + context.restoreAuthSystemState(); + + canClaimProfileFeature = authorizationFeatureService.find(CanClaimItemFeature.NAME); + + } + + @Test + public void testCanClaimAProfile() throws Exception { + + String token = getAuthToken(context.getCurrentUser().getEmail(), password); + getClient(token).perform(get("/api/authz/authorizations/search/object") + .param("uri", uri(collectionAProfile)) + .param("eperson", context.getCurrentUser().getID().toString()) + .param("feature", canClaimProfileFeature.getName())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded").exists()) + .andExpect(jsonPath("$.page.totalElements", greaterThanOrEqualTo(1))); + + getClient(token).perform(get("/api/authz/authorizations/search/object") + .param("uri", uri(collectionBProfile)) + .param("eperson", context.getCurrentUser().getID().toString()) + .param("feature", canClaimProfileFeature.getName())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded").exists()) + .andExpect(jsonPath("$.page.totalElements", greaterThanOrEqualTo(1))); + + } + + @Test + public void testNotClaimableEntity() throws Exception { + + context.turnOffAuthorisationSystem(); + + Collection publicationCollection = CollectionBuilder + .createCollection(context, parentCommunity) + .withEntityType("Publication") + .withName("notClaimable") + .build(); + + context.turnOffAuthorisationSystem(); + + Item publication = ItemBuilder.createItem(context, publicationCollection).build(); + + String token = getAuthToken(context.getCurrentUser().getEmail(), password); + + getClient(token).perform(get("/api/authz/authorizations/search/object") + .param("uri", uri(publication)) + .param("eperson", context.getCurrentUser().getID().toString()) + .param("feature", canClaimProfileFeature.getName())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded").doesNotExist()) + .andExpect(jsonPath("$.page.totalElements", equalTo(0))); + + } + + @Test + public void testItemAlreadyInARelation() throws Exception { + + context.turnOffAuthorisationSystem(); + + Item ownedItem = ItemBuilder.createItem(context, personCollection) + .withDspaceObjectOwner("owner", "ownerAuthority").build(); + context.restoreAuthSystemState(); + + String token = getAuthToken(context.getCurrentUser().getEmail(), password); + + getClient(token).perform(get("/api/authz/authorizations/search/object") + .param("uri", uri(ownedItem)) + .param("eperson", context.getCurrentUser().getID().toString()) + .param("feature", canClaimProfileFeature.getName())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded").doesNotExist()) + .andExpect(jsonPath("$.page.totalElements", equalTo(0))); + + } + + @Test + public void testUserWithProfile() throws Exception { + + context.turnOffAuthorisationSystem(); + + ItemBuilder.createItem(context, personCollection) + .withTitle("User") + .withDspaceObjectOwner("User", context.getCurrentUser().getID().toString()) + .build(); + + context.restoreAuthSystemState(); + + getClient(getAuthToken(context.getCurrentUser().getEmail(), password)) + .perform(get("/api/authz/authorizations/search/object") + .param("uri", uri(collectionAProfile)) + .param("eperson", context.getCurrentUser().getID().toString()) + .param("feature", canClaimProfileFeature.getName())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded").doesNotExist()) + .andExpect(jsonPath("$.page.totalElements", equalTo(0))); + } + + @Test + public void testWithoutClaimableEntities() throws Exception { + + configurationService.setProperty("claimable.entityType", null); + + getClient(getAuthToken(context.getCurrentUser().getEmail(), password)) + .perform(get("/api/authz/authorizations/search/object") + .param("uri", uri(collectionAProfile)) + .param("eperson", context.getCurrentUser().getID().toString()) + .param("feature", canClaimProfileFeature.getName())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded").doesNotExist()) + .andExpect(jsonPath("$.page.totalElements", equalTo(0))); + + } + + private String uri(Item item) { + ItemRest itemRest = itemConverter.convert(item, Projection.DEFAULT); + String itemRestURI = utils.linkToSingleResource(itemRest, "self").getHref(); + return itemRestURI; + } + +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/ShowClaimItemFeatureIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/ShowClaimItemFeatureIT.java new file mode 100644 index 000000000000..563e443efe89 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/ShowClaimItemFeatureIT.java @@ -0,0 +1,199 @@ +/** + * 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.rest.authorization; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.dspace.app.rest.authorization.impl.ShowClaimItemFeature; +import org.dspace.app.rest.converter.ItemConverter; +import org.dspace.app.rest.model.ItemRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.app.rest.utils.Utils; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.content.Collection; +import org.dspace.content.Item; +import org.dspace.services.ConfigurationService; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Test of Show Claim Item Authorization Feature implementation. + * + * @author Corrado Lombardi (corrado.lombardi at 4science.it) + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + */ +public class ShowClaimItemFeatureIT extends AbstractControllerIntegrationTest { + + private Item collectionAProfile; + private Item collectionBProfile; + + @Autowired + private ConfigurationService configurationService; + + @Autowired + private ItemConverter itemConverter; + + @Autowired + private Utils utils; + + @Autowired + private AuthorizationFeatureService authorizationFeatureService; + + private AuthorizationFeature showClaimProfileFeature; + + private Collection personCollection; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context).withName("Community").build(); + personCollection = + CollectionBuilder.createCollection(context, parentCommunity).withEntityType("Person") + .withName("claimableA").build(); + final Collection claimableCollectionB = + CollectionBuilder.createCollection(context, parentCommunity).withEntityType("Person") + .withName("claimableB").build(); + + collectionAProfile = ItemBuilder.createItem(context, personCollection).build(); + collectionBProfile = ItemBuilder.createItem(context, claimableCollectionB).build(); + + configurationService.addPropertyValue("claimable.entityType", "Person"); + + context.restoreAuthSystemState(); + + showClaimProfileFeature = authorizationFeatureService.find(ShowClaimItemFeature.NAME); + + } + + @Test + public void testCanClaimAProfile() throws Exception { + + String token = getAuthToken(context.getCurrentUser().getEmail(), password); + getClient(token).perform(get("/api/authz/authorizations/search/object") + .param("uri", uri(collectionAProfile)) + .param("eperson", context.getCurrentUser().getID().toString()) + .param("feature", showClaimProfileFeature.getName())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded").exists()) + .andExpect(jsonPath("$.page.totalElements", greaterThanOrEqualTo(1))); + + getClient(token).perform(get("/api/authz/authorizations/search/object") + .param("uri", uri(collectionBProfile)) + .param("eperson", context.getCurrentUser().getID().toString()) + .param("feature", showClaimProfileFeature.getName())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded").exists()) + .andExpect(jsonPath("$.page.totalElements", greaterThanOrEqualTo(1))); + + } + + @Test + public void testNotClaimableEntity() throws Exception { + + context.turnOffAuthorisationSystem(); + + Collection publicationCollection = CollectionBuilder + .createCollection(context, parentCommunity) + .withEntityType("Publication") + .withName("notClaimable") + .build(); + + context.turnOffAuthorisationSystem(); + + Item publication = ItemBuilder.createItem(context, publicationCollection).build(); + + String token = getAuthToken(context.getCurrentUser().getEmail(), password); + + getClient(token).perform(get("/api/authz/authorizations/search/object") + .param("uri", uri(publication)) + .param("eperson", context.getCurrentUser().getID().toString()) + .param("feature", showClaimProfileFeature.getName())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded").doesNotExist()) + .andExpect(jsonPath("$.page.totalElements", equalTo(0))); + + } + + @Test + public void testItemAlreadyInARelation() throws Exception { + + context.turnOffAuthorisationSystem(); + + Item ownedItem = ItemBuilder.createItem(context, personCollection) + .withDspaceObjectOwner("owner", "ownerAuthority").build(); + context.restoreAuthSystemState(); + + String token = getAuthToken(context.getCurrentUser().getEmail(), password); + + getClient(token).perform(get("/api/authz/authorizations/search/object") + .param("uri", uri(ownedItem)) + .param("eperson", context.getCurrentUser().getID().toString()) + .param("feature", showClaimProfileFeature.getName())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded").exists()) + .andExpect(jsonPath("$.page.totalElements", greaterThanOrEqualTo(1))); + + } + + @Test + public void testUserWithProfile() throws Exception { + + context.turnOffAuthorisationSystem(); + + ItemBuilder.createItem(context, personCollection) + .withTitle("User") + .withDspaceObjectOwner("User", context.getCurrentUser().getID().toString()) + .build(); + + context.restoreAuthSystemState(); + + getClient(getAuthToken(context.getCurrentUser().getEmail(), password)) + .perform(get("/api/authz/authorizations/search/object") + .param("uri", uri(collectionAProfile)) + .param("eperson", context.getCurrentUser().getID().toString()) + .param("feature", showClaimProfileFeature.getName())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded").doesNotExist()) + .andExpect(jsonPath("$.page.totalElements", equalTo(0))); + } + + @Test + public void testWithoutClaimableEntities() throws Exception { + + configurationService.setProperty("claimable.entityType", null); + + getClient(getAuthToken(context.getCurrentUser().getEmail(), password)) + .perform(get("/api/authz/authorizations/search/object") + .param("uri", uri(collectionAProfile)) + .param("eperson", context.getCurrentUser().getID().toString()) + .param("feature", showClaimProfileFeature.getName())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded").doesNotExist()) + .andExpect(jsonPath("$.page.totalElements", equalTo(0))); + + } + + private String uri(Item item) { + ItemRest itemRest = itemConverter.convert(item, Projection.DEFAULT); + String itemRestURI = utils.linkToSingleResource(itemRest, "self").getHref(); + return itemRestURI; + } + +} diff --git a/dspace/config/spring/rest/post-logged-in-actions.xml b/dspace/config/spring/rest/post-logged-in-actions.xml index e486a205ac95..1e025696471c 100644 --- a/dspace/config/spring/rest/post-logged-in-actions.xml +++ b/dspace/config/spring/rest/post-logged-in-actions.xml @@ -9,7 +9,4 @@ - - - \ No newline at end of file From 70f85edf9ebfd8b85531a7f71eca12f222169e8b Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 5 Jan 2022 10:12:01 -0600 Subject: [PATCH 0081/1846] Upgrade to h2 v2.x. Requires also updating to Hibernate 5.6.x --- dspace-api/pom.xml | 2 +- .../rdbms/migration/MigrationUtils.java | 7 +-- .../h2/V1.4__Upgrade_to_DSpace_1.4_schema.sql | 6 +-- ...9.26__DS-1582_Metadata_For_All_Objects.sql | 44 +++++++++---------- .../h2/V5.6_2016.08.23__DS-3097.sql | 4 +- ...125-fix-bundle-bitstream-delete-rights.sql | 2 +- .../h2/V6.0_2016.08.23__DS-3097.sql | 4 +- ...DV_place_after_migrating_from_DSpace_5.sql | 3 +- dspace-iiif/pom.xml | 7 +++ dspace-server-webapp/pom.xml | 7 +++ pom.xml | 13 ++++-- 11 files changed, 61 insertions(+), 38 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 6a54a9637628..d3085ecdecdf 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -373,7 +373,7 @@ org.hibernate.javax.persistence hibernate-jpa-2.1-api - 1.0.0.Final + 1.0.2.Final diff --git a/dspace-api/src/main/java/org/dspace/storage/rdbms/migration/MigrationUtils.java b/dspace-api/src/main/java/org/dspace/storage/rdbms/migration/MigrationUtils.java index 624d0cb55a5a..842fc15e1657 100644 --- a/dspace-api/src/main/java/org/dspace/storage/rdbms/migration/MigrationUtils.java +++ b/dspace-api/src/main/java/org/dspace/storage/rdbms/migration/MigrationUtils.java @@ -86,10 +86,11 @@ protected static Integer dropDBConstraint(Connection connection, String tableNam cascade = true; break; case "h2": - // In H2, constraints are listed in the "information_schema.constraints" table + // In H2, column constraints are listed in the "INFORMATION_SCHEMA.KEY_COLUMN_USAGE" table constraintNameSQL = "SELECT DISTINCT CONSTRAINT_NAME " + - "FROM information_schema.constraints " + - "WHERE table_name = ? AND column_list = ?"; + "FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE " + + "WHERE TABLE_NAME = ? AND COLUMN_NAME = ?"; + cascade = true; break; default: throw new SQLException("DBMS " + dbtype + " is unsupported in this migration."); diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V1.4__Upgrade_to_DSpace_1.4_schema.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V1.4__Upgrade_to_DSpace_1.4_schema.sql index e00a6516261c..62d12fe5ce25 100644 --- a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V1.4__Upgrade_to_DSpace_1.4_schema.sql +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V1.4__Upgrade_to_DSpace_1.4_schema.sql @@ -245,13 +245,13 @@ insert into most_recent_checksum ) select bitstream.bitstream_id, - '1', + true, CASE WHEN bitstream.checksum IS NULL THEN '' ELSE bitstream.checksum END, CASE WHEN bitstream.checksum IS NULL THEN '' ELSE bitstream.checksum END, FORMATDATETIME(NOW(),'DD-MM-RRRR HH24:MI:SS'), FORMATDATETIME(NOW(),'DD-MM-RRRR HH24:MI:SS'), CASE WHEN bitstream.checksum_algorithm IS NULL THEN 'MD5' ELSE bitstream.checksum_algorithm END, - '1' + true from bitstream; -- Update all the deleted checksums @@ -263,7 +263,7 @@ update most_recent_checksum set to_be_processed = 0 where most_recent_checksum.bitstream_id in ( select bitstream_id -from bitstream where deleted = '1' ); +from bitstream where deleted = true ); -- this will insert into history table -- for the initial start diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V5.0_2014.09.26__DS-1582_Metadata_For_All_Objects.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V5.0_2014.09.26__DS-1582_Metadata_For_All_Objects.sql index 87551bdf4e9b..cd908279f158 100644 --- a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V5.0_2014.09.26__DS-1582_Metadata_For_All_Objects.sql +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V5.0_2014.09.26__DS-1582_Metadata_For_All_Objects.sql @@ -36,7 +36,7 @@ alter table metadatavalue alter column resource_id set not null; INSERT INTO metadatavalue (metadata_value_id, resource_id, resource_type_id, metadata_field_id, text_value, text_lang, place) SELECT -metadatavalue_seq.nextval as metadata_value_id, +NEXT VALUE FOR metadatavalue_seq as metadata_value_id, community_id AS resource_id, 4 AS resource_type_id, (select metadata_field_id from metadatafieldregistry where metadata_schema_id=(select metadata_schema_id from metadataschemaregistry where short_id='dc') and element = 'description' and qualifier is null) AS metadata_field_id, @@ -47,7 +47,7 @@ FROM community where not introductory_text is null; INSERT INTO metadatavalue (metadata_value_id, resource_id, resource_type_id, metadata_field_id, text_value, text_lang, place) SELECT -metadatavalue_seq.nextval as metadata_value_id, +NEXT VALUE FOR metadatavalue_seq as metadata_value_id, community_id AS resource_id, 4 AS resource_type_id, (select metadata_field_id from metadatafieldregistry where metadata_schema_id=(select metadata_schema_id from metadataschemaregistry where short_id='dc') and element = 'description' and qualifier = 'abstract') AS metadata_field_id, @@ -58,7 +58,7 @@ FROM community where not short_description is null; INSERT INTO metadatavalue (metadata_value_id, resource_id, resource_type_id, metadata_field_id, text_value, text_lang, place) SELECT -metadatavalue_seq.nextval as metadata_value_id, +NEXT VALUE FOR metadatavalue_seq as metadata_value_id, community_id AS resource_id, 4 AS resource_type_id, (select metadata_field_id from metadatafieldregistry where metadata_schema_id=(select metadata_schema_id from metadataschemaregistry where short_id='dc') and element = 'description' and qualifier = 'tableofcontents') AS metadata_field_id, @@ -69,7 +69,7 @@ FROM community where not side_bar_text is null; INSERT INTO metadatavalue (metadata_value_id, resource_id, resource_type_id, metadata_field_id, text_value, text_lang, place) SELECT -metadatavalue_seq.nextval as metadata_value_id, +NEXT VALUE FOR metadatavalue_seq as metadata_value_id, community_id AS resource_id, 4 AS resource_type_id, (select metadata_field_id from metadatafieldregistry where metadata_schema_id=(select metadata_schema_id from metadataschemaregistry where short_id='dc') and element = 'rights' and qualifier is null) AS metadata_field_id, @@ -80,7 +80,7 @@ FROM community where not copyright_text is null; INSERT INTO metadatavalue (metadata_value_id, resource_id, resource_type_id, metadata_field_id, text_value, text_lang, place) SELECT -metadatavalue_seq.nextval as metadata_value_id, +NEXT VALUE FOR metadatavalue_seq as metadata_value_id, community_id AS resource_id, 4 AS resource_type_id, (select metadata_field_id from metadatafieldregistry where metadata_schema_id=(select metadata_schema_id from metadataschemaregistry where short_id='dc') and element = 'title' and qualifier is null) AS metadata_field_id, @@ -104,7 +104,7 @@ alter table community drop column name; INSERT INTO metadatavalue (metadata_value_id, resource_id, resource_type_id, metadata_field_id, text_value, text_lang, place) SELECT -metadatavalue_seq.nextval as metadata_value_id, +NEXT VALUE FOR metadatavalue_seq as metadata_value_id, collection_id AS resource_id, 3 AS resource_type_id, (select metadata_field_id from metadatafieldregistry where metadata_schema_id=(select metadata_schema_id from metadataschemaregistry where short_id='dc') and element = 'description' and qualifier is null) AS metadata_field_id, @@ -115,7 +115,7 @@ FROM collection where not introductory_text is null; INSERT INTO metadatavalue (metadata_value_id, resource_id, resource_type_id, metadata_field_id, text_value, text_lang, place) SELECT -metadatavalue_seq.nextval as metadata_value_id, +NEXT VALUE FOR metadatavalue_seq as metadata_value_id, collection_id AS resource_id, 3 AS resource_type_id, (select metadata_field_id from metadatafieldregistry where metadata_schema_id=(select metadata_schema_id from metadataschemaregistry where short_id='dc') and element = 'description' and qualifier = 'abstract') AS metadata_field_id, @@ -126,7 +126,7 @@ FROM collection where not short_description is null; INSERT INTO metadatavalue (metadata_value_id, resource_id, resource_type_id, metadata_field_id, text_value, text_lang, place) SELECT -metadatavalue_seq.nextval as metadata_value_id, +NEXT VALUE FOR metadatavalue_seq as metadata_value_id, collection_id AS resource_id, 3 AS resource_type_id, (select metadata_field_id from metadatafieldregistry where metadata_schema_id=(select metadata_schema_id from metadataschemaregistry where short_id='dc') and element = 'description' and qualifier = 'tableofcontents') AS metadata_field_id, @@ -137,7 +137,7 @@ FROM collection where not side_bar_text is null; INSERT INTO metadatavalue (metadata_value_id, resource_id, resource_type_id, metadata_field_id, text_value, text_lang, place) SELECT -metadatavalue_seq.nextval as metadata_value_id, +NEXT VALUE FOR metadatavalue_seq as metadata_value_id, collection_id AS resource_id, 3 AS resource_type_id, (select metadata_field_id from metadatafieldregistry where metadata_schema_id=(select metadata_schema_id from metadataschemaregistry where short_id='dc') and element = 'rights' and qualifier is null) AS metadata_field_id, @@ -148,7 +148,7 @@ FROM collection where not copyright_text is null; INSERT INTO metadatavalue (metadata_value_id, resource_id, resource_type_id, metadata_field_id, text_value, text_lang, place) SELECT -metadatavalue_seq.nextval as metadata_value_id, +NEXT VALUE FOR metadatavalue_seq as metadata_value_id, collection_id AS resource_id, 3 AS resource_type_id, (select metadata_field_id from metadatafieldregistry where metadata_schema_id=(select metadata_schema_id from metadataschemaregistry where short_id='dc') and element = 'title' and qualifier is null) AS metadata_field_id, @@ -159,7 +159,7 @@ FROM collection where not name is null; INSERT INTO metadatavalue (metadata_value_id, resource_id, resource_type_id, metadata_field_id, text_value, text_lang, place) SELECT -metadatavalue_seq.nextval as metadata_value_id, +NEXT VALUE FOR metadatavalue_seq as metadata_value_id, collection_id AS resource_id, 3 AS resource_type_id, (select metadata_field_id from metadatafieldregistry where metadata_schema_id=(select metadata_schema_id from metadataschemaregistry where short_id='dc') and element = 'provenance' and qualifier is null) AS metadata_field_id, @@ -170,7 +170,7 @@ FROM collection where not provenance_description is null; INSERT INTO metadatavalue (metadata_value_id, resource_id, resource_type_id, metadata_field_id, text_value, text_lang, place) SELECT -metadatavalue_seq.nextval as metadata_value_id, +NEXT VALUE FOR metadatavalue_seq as metadata_value_id, collection_id AS resource_id, 3 AS resource_type_id, (select metadata_field_id from metadatafieldregistry where metadata_schema_id=(select metadata_schema_id from metadataschemaregistry where short_id='dc') and element = 'rights' and qualifier = 'license') AS metadata_field_id, @@ -194,7 +194,7 @@ alter table collection drop column provenance_description; INSERT INTO metadatavalue (metadata_value_id, resource_id, resource_type_id, metadata_field_id, text_value, text_lang, place) SELECT -metadatavalue_seq.nextval as metadata_value_id, +NEXT VALUE FOR metadatavalue_seq as metadata_value_id, bundle_id AS resource_id, 1 AS resource_type_id, (select metadata_field_id from metadatafieldregistry where metadata_schema_id=(select metadata_schema_id from metadataschemaregistry where short_id='dc') and element = 'title' and qualifier is null) AS metadata_field_id, @@ -214,7 +214,7 @@ alter table bundle drop column name; INSERT INTO metadatavalue (metadata_value_id, resource_id, resource_type_id, metadata_field_id, text_value, text_lang, place) SELECT -metadatavalue_seq.nextval as metadata_value_id, +NEXT VALUE FOR metadatavalue_seq as metadata_value_id, bitstream_id AS resource_id, 0 AS resource_type_id, (select metadata_field_id from metadatafieldregistry where metadata_schema_id=(select metadata_schema_id from metadataschemaregistry where short_id='dc') and element = 'title' and qualifier is null) AS metadata_field_id, @@ -225,7 +225,7 @@ FROM bitstream where not name is null; INSERT INTO metadatavalue (metadata_value_id, resource_id, resource_type_id, metadata_field_id, text_value, text_lang, place) SELECT -metadatavalue_seq.nextval as metadata_value_id, +NEXT VALUE FOR metadatavalue_seq as metadata_value_id, bitstream_id AS resource_id, 0 AS resource_type_id, (select metadata_field_id from metadatafieldregistry where metadata_schema_id=(select metadata_schema_id from metadataschemaregistry where short_id='dc') and element = 'description' and qualifier is null) AS metadata_field_id, @@ -236,7 +236,7 @@ FROM bitstream where not description is null; INSERT INTO metadatavalue (metadata_value_id, resource_id, resource_type_id, metadata_field_id, text_value, text_lang, place) SELECT -metadatavalue_seq.nextval as metadata_value_id, +NEXT VALUE FOR metadatavalue_seq as metadata_value_id, bitstream_id AS resource_id, 0 AS resource_type_id, (select metadata_field_id from metadatafieldregistry where metadata_schema_id=(select metadata_schema_id from metadataschemaregistry where short_id='dc') and element = 'format' and qualifier is null) AS metadata_field_id, @@ -247,7 +247,7 @@ FROM bitstream where not user_format_description is null; INSERT INTO metadatavalue (metadata_value_id, resource_id, resource_type_id, metadata_field_id, text_value, text_lang, place) SELECT -metadatavalue_seq.nextval as metadata_value_id, +NEXT VALUE FOR metadatavalue_seq as metadata_value_id, bitstream_id AS resource_id, 0 AS resource_type_id, (select metadata_field_id from metadatafieldregistry where metadata_schema_id=(select metadata_schema_id from metadataschemaregistry where short_id='dc') and element = 'source' and qualifier is null) AS metadata_field_id, @@ -269,7 +269,7 @@ alter table bitstream drop column source; INSERT INTO metadatavalue (metadata_value_id, resource_id, resource_type_id, metadata_field_id, text_value, text_lang, place) SELECT -metadatavalue_seq.nextval as metadata_value_id, +NEXT VALUE FOR metadatavalue_seq as metadata_value_id, eperson_group_id AS resource_id, 6 AS resource_type_id, (select metadata_field_id from metadatafieldregistry where metadata_schema_id=(select metadata_schema_id from metadataschemaregistry where short_id='dc') and element = 'title' and qualifier is null) AS metadata_field_id, @@ -288,7 +288,7 @@ alter table epersongroup drop column name; INSERT INTO metadatavalue (metadata_value_id, resource_id, resource_type_id, metadata_field_id, text_value, text_lang, place) SELECT -metadatavalue_seq.nextval as metadata_value_id, +NEXT VALUE FOR metadatavalue_seq as metadata_value_id, eperson_id AS resource_id, 7 AS resource_type_id, (select metadata_field_id from metadatafieldregistry where metadata_schema_id=(select metadata_schema_id from metadataschemaregistry where short_id='eperson') and element = 'firstname' and qualifier is null) AS metadata_field_id, @@ -299,7 +299,7 @@ FROM eperson where not firstname is null; INSERT INTO metadatavalue (metadata_value_id, resource_id, resource_type_id, metadata_field_id, text_value, text_lang, place) SELECT -metadatavalue_seq.nextval as metadata_value_id, +NEXT VALUE FOR metadatavalue_seq as metadata_value_id, eperson_id AS resource_id, 7 AS resource_type_id, (select metadata_field_id from metadatafieldregistry where metadata_schema_id=(select metadata_schema_id from metadataschemaregistry where short_id='eperson') and element = 'lastname' and qualifier is null) AS metadata_field_id, @@ -310,7 +310,7 @@ FROM eperson where not lastname is null; INSERT INTO metadatavalue (metadata_value_id, resource_id, resource_type_id, metadata_field_id, text_value, text_lang, place) SELECT -metadatavalue_seq.nextval as metadata_value_id, +NEXT VALUE FOR metadatavalue_seq as metadata_value_id, eperson_id AS resource_id, 7 AS resource_type_id, (select metadata_field_id from metadatafieldregistry where metadata_schema_id=(select metadata_schema_id from metadataschemaregistry where short_id='eperson') and element = 'phone' and qualifier is null) AS metadata_field_id, @@ -321,7 +321,7 @@ FROM eperson where not phone is null; INSERT INTO metadatavalue (metadata_value_id, resource_id, resource_type_id, metadata_field_id, text_value, text_lang, place) SELECT -metadatavalue_seq.nextval as metadata_value_id, +NEXT VALUE FOR metadatavalue_seq as metadata_value_id, eperson_id AS resource_id, 7 AS resource_type_id, (select metadata_field_id from metadatafieldregistry where metadata_schema_id=(select metadata_schema_id from metadataschemaregistry where short_id='eperson') and element = 'language' and qualifier is null) AS metadata_field_id, diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V5.6_2016.08.23__DS-3097.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V5.6_2016.08.23__DS-3097.sql index 2e09b807de3b..0bd68c520193 100644 --- a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V5.6_2016.08.23__DS-3097.sql +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V5.6_2016.08.23__DS-3097.sql @@ -14,11 +14,11 @@ UPDATE resourcepolicy SET action_id = 12 where action_id = 0 and resource_type_i SELECT bundle2bitstream.bitstream_id FROM bundle2bitstream LEFT JOIN item2bundle ON bundle2bitstream.bundle_id = item2bundle.bundle_id LEFT JOIN item ON item2bundle.item_id = item.item_id - WHERE item.withdrawn = 1 + WHERE item.withdrawn = true ); UPDATE resourcepolicy SET action_id = 12 where action_id = 0 and resource_type_id = 1 and resource_id in ( SELECT item2bundle.bundle_id FROM item2bundle LEFT JOIN item ON item2bundle.item_id = item.item_id - WHERE item.withdrawn = 1 + WHERE item.withdrawn = true ); \ No newline at end of file diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V6.0_2016.04.14__DS-3125-fix-bundle-bitstream-delete-rights.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V6.0_2016.04.14__DS-3125-fix-bundle-bitstream-delete-rights.sql index 1c98ceef2a97..1ee23246eaae 100644 --- a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V6.0_2016.04.14__DS-3125-fix-bundle-bitstream-delete-rights.sql +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V6.0_2016.04.14__DS-3125-fix-bundle-bitstream-delete-rights.sql @@ -17,7 +17,7 @@ INSERT INTO resourcepolicy (policy_id, resource_type_id, resource_id, action_id, start_date, end_date, rpname, rptype, rpdescription, eperson_id, epersongroup_id, dspace_object) SELECT -resourcepolicy_seq.nextval AS policy_id, +NEXT VALUE FOR resourcepolicy_seq AS policy_id, resource_type_id, resource_id, -- Insert the Constants.DELETE action diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V6.0_2016.08.23__DS-3097.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V6.0_2016.08.23__DS-3097.sql index e1220c8c7cce..5bb59970c55b 100644 --- a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V6.0_2016.08.23__DS-3097.sql +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V6.0_2016.08.23__DS-3097.sql @@ -14,11 +14,11 @@ UPDATE resourcepolicy SET action_id = 12 where action_id = 0 and dspace_object i SELECT bundle2bitstream.bitstream_id FROM bundle2bitstream LEFT JOIN item2bundle ON bundle2bitstream.bundle_id = item2bundle.bundle_id LEFT JOIN item ON item2bundle.item_id = item.uuid - WHERE item.withdrawn = 1 + WHERE item.withdrawn = true ); UPDATE resourcepolicy SET action_id = 12 where action_id = 0 and dspace_object in ( SELECT item2bundle.bundle_id FROM item2bundle LEFT JOIN item ON item2bundle.item_id = item.uuid - WHERE item.withdrawn = 1 + WHERE item.withdrawn = true ); \ No newline at end of file diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.1_2021.10.18__Fix_MDV_place_after_migrating_from_DSpace_5.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.1_2021.10.18__Fix_MDV_place_after_migrating_from_DSpace_5.sql index 3b649a321c9f..7506433cddbc 100644 --- a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.1_2021.10.18__Fix_MDV_place_after_migrating_from_DSpace_5.sql +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.1_2021.10.18__Fix_MDV_place_after_migrating_from_DSpace_5.sql @@ -9,10 +9,11 @@ ---------------------------------------------------- -- Make sure the metadatavalue.place column starts at 0 instead of 1 ---------------------------------------------------- + CREATE LOCAL TEMPORARY TABLE mdv_minplace ( dspace_object_id UUID NOT NULL, metadata_field_id INT NOT NULL, - minplace INT NOT NULL, + minplace INT NOT NULL ); INSERT INTO mdv_minplace diff --git a/dspace-iiif/pom.xml b/dspace-iiif/pom.xml index c8f64c6f0435..e60652318217 100644 --- a/dspace-iiif/pom.xml +++ b/dspace-iiif/pom.xml @@ -45,6 +45,13 @@ org.springframework.boot spring-boot-starter-web ${spring-boot.version} + + + + org.hibernate.validator + hibernate-validator + + org.springframework.boot diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 20e6602347c7..f90ea51b1585 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -260,6 +260,13 @@ org.springframework.boot spring-boot-starter-web ${spring-boot.version} + + + + org.hibernate.validator + hibernate-validator + + org.springframework.boot diff --git a/pom.xml b/pom.xml index e03dcb3b07ba..f050e1e36fa5 100644 --- a/pom.xml +++ b/pom.xml @@ -22,8 +22,8 @@ 5.2.5.RELEASE 2.2.6.RELEASE 5.2.2.RELEASE - 5.4.10.Final - 6.0.18.Final + 5.6.5.Final + 6.0.23.Final 42.3.3 8.8.1 @@ -1161,6 +1161,13 @@ ${hibernate-validator.version} + + + org.jboss.logging + jboss-logging + 3.4.3.Final + + com.rometools @@ -1691,7 +1698,7 @@ com.h2database h2 - 1.4.187 + 2.1.210 test From a35a6d4a28a6a494b5d9f2855109bc004bc943d8 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 5 Jan 2022 10:12:32 -0600 Subject: [PATCH 0082/1846] Upgrade to Flyway 8.2.x to support h2 v2 --- dspace-api/pom.xml | 2 +- .../storage/rdbms/EntityTypeServiceInitializer.java | 10 ++++++++++ .../dspace/storage/rdbms/GroupServiceInitializer.java | 10 ++++++++++ .../dspace/storage/rdbms/PostgreSQLCryptoChecker.java | 10 ++++++++++ .../java/org/dspace/storage/rdbms/RegistryUpdater.java | 10 ++++++++++ .../dspace/storage/rdbms/SiteServiceInitializer.java | 10 ++++++++++ 6 files changed, 51 insertions(+), 1 deletion(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index d3085ecdecdf..572b9f8cf3dd 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -762,7 +762,7 @@ org.flywaydb flyway-core - 6.5.7 + 8.2.3 diff --git a/dspace-api/src/main/java/org/dspace/storage/rdbms/EntityTypeServiceInitializer.java b/dspace-api/src/main/java/org/dspace/storage/rdbms/EntityTypeServiceInitializer.java index ebf790900bbd..e0e41516d01f 100644 --- a/dspace-api/src/main/java/org/dspace/storage/rdbms/EntityTypeServiceInitializer.java +++ b/dspace-api/src/main/java/org/dspace/storage/rdbms/EntityTypeServiceInitializer.java @@ -49,6 +49,16 @@ private void initEntityTypes() { } } + /** + * The callback name, Flyway will use this to sort the callbacks alphabetically before executing them + * @return The callback name + */ + @Override + public String getCallbackName() { + // Return class name only (not prepended by package) + return EntityTypeServiceInitializer.class.getSimpleName(); + } + @Override public boolean supports(Event event, org.flywaydb.core.api.callback.Context context) { // Must run AFTER all migrations complete, since it is dependent on Hibernate diff --git a/dspace-api/src/main/java/org/dspace/storage/rdbms/GroupServiceInitializer.java b/dspace-api/src/main/java/org/dspace/storage/rdbms/GroupServiceInitializer.java index 7338dd75bcb7..54498a1c644a 100644 --- a/dspace-api/src/main/java/org/dspace/storage/rdbms/GroupServiceInitializer.java +++ b/dspace-api/src/main/java/org/dspace/storage/rdbms/GroupServiceInitializer.java @@ -51,6 +51,16 @@ public void initGroups() { } + /** + * The callback name, Flyway will use this to sort the callbacks alphabetically before executing them + * @return The callback name + */ + @Override + public String getCallbackName() { + // Return class name only (not prepended by package) + return GroupServiceInitializer.class.getSimpleName(); + } + /** * Events supported by this callback. * @param event Flyway event diff --git a/dspace-api/src/main/java/org/dspace/storage/rdbms/PostgreSQLCryptoChecker.java b/dspace-api/src/main/java/org/dspace/storage/rdbms/PostgreSQLCryptoChecker.java index 5798f4254cdc..5459cc3cc35e 100644 --- a/dspace-api/src/main/java/org/dspace/storage/rdbms/PostgreSQLCryptoChecker.java +++ b/dspace-api/src/main/java/org/dspace/storage/rdbms/PostgreSQLCryptoChecker.java @@ -97,6 +97,16 @@ public void removePgCrypto(Connection connection) { } } + /** + * The callback name, Flyway will use this to sort the callbacks alphabetically before executing them + * @return The callback name + */ + @Override + public String getCallbackName() { + // Return class name only (not prepended by package) + return PostgreSQLCryptoChecker.class.getSimpleName(); + } + /** * Events supported by this callback. * @param event Flyway event diff --git a/dspace-api/src/main/java/org/dspace/storage/rdbms/RegistryUpdater.java b/dspace-api/src/main/java/org/dspace/storage/rdbms/RegistryUpdater.java index ae8be0988a12..c2a9b8a84869 100644 --- a/dspace-api/src/main/java/org/dspace/storage/rdbms/RegistryUpdater.java +++ b/dspace-api/src/main/java/org/dspace/storage/rdbms/RegistryUpdater.java @@ -101,6 +101,16 @@ private void updateRegistries() { } + /** + * The callback name, Flyway will use this to sort the callbacks alphabetically before executing them + * @return The callback name + */ + @Override + public String getCallbackName() { + // Return class name only (not prepended by package) + return RegistryUpdater.class.getSimpleName(); + } + /** * Events supported by this callback. * @param event Flyway event diff --git a/dspace-api/src/main/java/org/dspace/storage/rdbms/SiteServiceInitializer.java b/dspace-api/src/main/java/org/dspace/storage/rdbms/SiteServiceInitializer.java index 26e76804e1e5..872a633146af 100644 --- a/dspace-api/src/main/java/org/dspace/storage/rdbms/SiteServiceInitializer.java +++ b/dspace-api/src/main/java/org/dspace/storage/rdbms/SiteServiceInitializer.java @@ -73,6 +73,16 @@ public void initializeSiteObject() { } + /** + * The callback name, Flyway will use this to sort the callbacks alphabetically before executing them + * @return The callback name + */ + @Override + public String getCallbackName() { + // Return class name only (not prepended by package) + return SiteServiceInitializer.class.getSimpleName(); + } + /** * Events supported by this callback. * @param event Flyway event From 5a046dca13e8a4c60ad5e4deef27e0fd40adc0ad Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 5 Jan 2022 10:21:25 -0600 Subject: [PATCH 0083/1846] Fix incorrect StringUtils import --- .../org/dspace/app/rest/RelationshipTypeRestRepositoryIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RelationshipTypeRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RelationshipTypeRestRepositoryIT.java index a2ed672a61f6..3295e03593e7 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RelationshipTypeRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RelationshipTypeRestRepositoryIT.java @@ -22,13 +22,13 @@ import java.sql.SQLException; import java.util.List; +import org.apache.commons.lang3.StringUtils; import org.dspace.app.rest.matcher.EntityTypeMatcher; import org.dspace.app.rest.matcher.RelationshipTypeMatcher; import org.dspace.app.rest.test.AbstractEntityIntegrationTest; import org.dspace.content.RelationshipType; import org.dspace.content.service.EntityTypeService; import org.dspace.content.service.RelationshipTypeService; -import org.h2.util.StringUtils; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; From 531fc8acda9101e16fa7c327a152e2aa5798be49 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Thu, 24 Feb 2022 16:38:14 -0600 Subject: [PATCH 0084/1846] Upgrade to Flyway 8.4.4 --- dspace-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 572b9f8cf3dd..b8fae8d86c7b 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -762,7 +762,7 @@ org.flywaydb flyway-core - 8.2.3 + 8.4.4 From 0595eeddbb4a6d9fe2f008fa4fee7e27f4410008 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 16 Feb 2022 16:35:41 -0600 Subject: [PATCH 0085/1846] Tiny stability fix to test (fails on Windows). Remove newline from string comparison --- .../org/dspace/iiif/canvasdimension/CanvasDimensionsIT.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/test/java/org/dspace/iiif/canvasdimension/CanvasDimensionsIT.java b/dspace-api/src/test/java/org/dspace/iiif/canvasdimension/CanvasDimensionsIT.java index 502266da0686..2711f46c319d 100644 --- a/dspace-api/src/test/java/org/dspace/iiif/canvasdimension/CanvasDimensionsIT.java +++ b/dspace-api/src/test/java/org/dspace/iiif/canvasdimension/CanvasDimensionsIT.java @@ -15,6 +15,7 @@ import java.io.InputStream; import java.io.PrintStream; +import org.apache.commons.lang.StringUtils; import org.dspace.AbstractIntegrationTestWithDatabase; import org.dspace.builder.BitstreamBuilder; import org.dspace.builder.CollectionBuilder; @@ -408,7 +409,7 @@ public void processParentCommunityWithMaximum() throws Exception { execCanvasScriptWithMaxRecs(id); // check System.out for number of items processed. - assertEquals("2 IIIF items were processed.\n", outContent.toString()); + assertEquals("2 IIIF items were processed.", StringUtils.chomp(outContent.toString())); } @Test From c4ad834a8a8c3b14c5d8f533a14acc766a3c8b52 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 18 Feb 2022 14:04:04 -0600 Subject: [PATCH 0086/1846] Cleanup IT logs by loading beans by name. Add note to application.properties on how to enable debug logs --- .../org/dspace/AbstractIntegrationTestWithDatabase.java | 8 +++++--- .../src/main/resources/application.properties | 2 ++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/dspace-api/src/test/java/org/dspace/AbstractIntegrationTestWithDatabase.java b/dspace-api/src/test/java/org/dspace/AbstractIntegrationTestWithDatabase.java index cada4944e3b9..e27fb19a68eb 100644 --- a/dspace-api/src/test/java/org/dspace/AbstractIntegrationTestWithDatabase.java +++ b/dspace-api/src/test/java/org/dspace/AbstractIntegrationTestWithDatabase.java @@ -15,6 +15,7 @@ import org.apache.logging.log4j.Logger; import org.dspace.app.launcher.ScriptLauncher; import org.dspace.app.scripts.handler.impl.TestDSpaceRunnableHandler; +import org.dspace.authority.AuthoritySearchService; import org.dspace.authority.MockAuthoritySolrServiceImpl; import org.dspace.authorize.AuthorizeException; import org.dspace.builder.AbstractBuilder; @@ -31,6 +32,7 @@ import org.dspace.services.factory.DSpaceServicesFactory; import org.dspace.statistics.MockSolrLoggerServiceImpl; import org.dspace.statistics.MockSolrStatisticsCore; +import org.dspace.statistics.SolrStatisticsCore; import org.dspace.storage.rdbms.DatabaseUtils; import org.jdom2.Document; import org.junit.After; @@ -183,15 +185,15 @@ public void destroy() throws Exception { searchService.reset(); // Clear the statistics core. serviceManager - .getServiceByName(null, MockSolrStatisticsCore.class) + .getServiceByName(SolrStatisticsCore.class.getName(), MockSolrStatisticsCore.class) .reset(); MockSolrLoggerServiceImpl statisticsService = serviceManager - .getServiceByName(null, MockSolrLoggerServiceImpl.class); + .getServiceByName("solrLoggerService", MockSolrLoggerServiceImpl.class); statisticsService.reset(); MockAuthoritySolrServiceImpl authorityService = serviceManager - .getServiceByName(null, MockAuthoritySolrServiceImpl.class); + .getServiceByName(AuthoritySearchService.class.getName(), MockAuthoritySolrServiceImpl.class); authorityService.reset(); // Reload our ConfigurationService (to reset configs to defaults again) diff --git a/dspace-server-webapp/src/main/resources/application.properties b/dspace-server-webapp/src/main/resources/application.properties index c695fe2fbae6..91e2aa73ae9a 100644 --- a/dspace-server-webapp/src/main/resources/application.properties +++ b/dspace-server-webapp/src/main/resources/application.properties @@ -117,6 +117,8 @@ spring.main.allow-bean-definition-overriding = true ######################### # Spring Boot Logging levels # +# NOTE: The below settings can be uncommented to debug issues in Spring Boot/WebMVC. +# These "logging.level" settings will also override defaults in "logging.config" below. #logging.level.org.springframework.boot=DEBUG #logging.level.org.springframework.web=DEBUG #logging.level.org.hibernate=ERROR From 57b19fa71a728a53bce120c6c3c5a18882c191b3 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Tue, 21 Dec 2021 09:12:52 -0600 Subject: [PATCH 0087/1846] Bug fix, ensure Solr container is *shutdown* when destroyed --- dspace-api/src/test/java/org/dspace/solr/MockSolrServer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/dspace-api/src/test/java/org/dspace/solr/MockSolrServer.java b/dspace-api/src/test/java/org/dspace/solr/MockSolrServer.java index e80d5f8e1750..aed0c088c362 100644 --- a/dspace-api/src/test/java/org/dspace/solr/MockSolrServer.java +++ b/dspace-api/src/test/java/org/dspace/solr/MockSolrServer.java @@ -171,6 +171,7 @@ private static synchronized void initSolrContainer() { * Discard the embedded Solr container. */ private static synchronized void destroyContainer() { + container.shutdown(); container = null; log.info("SOLR CoreContainer destroyed"); } From 62c0e28f5451dddb7b8f0c34c5af0a8bdca6168b Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Tue, 1 Mar 2022 17:06:19 -0600 Subject: [PATCH 0088/1846] Remove custom Postgres Dialect. Replace with DatabaseAwareLobType --- .../org/dspace/authorize/ResourcePolicy.java | 2 +- .../org/dspace/content/MetadataValue.java | 2 +- .../rdbms/hibernate/DatabaseAwareLobType.java | 57 ++++++++++++++++ .../postgres/DSpacePostgreSQL82Dialect.java | 67 ------------------- dspace/config/dspace.cfg | 4 +- dspace/config/local.cfg.EXAMPLE | 4 +- 6 files changed, 63 insertions(+), 73 deletions(-) create mode 100644 dspace-api/src/main/java/org/dspace/storage/rdbms/hibernate/DatabaseAwareLobType.java delete mode 100644 dspace-api/src/main/java/org/dspace/storage/rdbms/hibernate/postgres/DSpacePostgreSQL82Dialect.java diff --git a/dspace-api/src/main/java/org/dspace/authorize/ResourcePolicy.java b/dspace-api/src/main/java/org/dspace/authorize/ResourcePolicy.java index a25a492a3af5..954bb9699038 100644 --- a/dspace-api/src/main/java/org/dspace/authorize/ResourcePolicy.java +++ b/dspace-api/src/main/java/org/dspace/authorize/ResourcePolicy.java @@ -93,7 +93,7 @@ public class ResourcePolicy implements ReloadableEntity { private String rptype; @Lob - @Type(type = "org.hibernate.type.MaterializedClobType") + @Type(type = "org.dspace.storage.rdbms.hibernate.DatabaseAwareLobType") @Column(name = "rpdescription") private String rpdescription; diff --git a/dspace-api/src/main/java/org/dspace/content/MetadataValue.java b/dspace-api/src/main/java/org/dspace/content/MetadataValue.java index d1b636cdff45..9ff3cb9ec2af 100644 --- a/dspace-api/src/main/java/org/dspace/content/MetadataValue.java +++ b/dspace-api/src/main/java/org/dspace/content/MetadataValue.java @@ -59,7 +59,7 @@ public class MetadataValue implements ReloadableEntity { * The value of the field */ @Lob - @Type(type = "org.hibernate.type.MaterializedClobType") + @Type(type = "org.dspace.storage.rdbms.hibernate.DatabaseAwareLobType") @Column(name = "text_value") private String value; diff --git a/dspace-api/src/main/java/org/dspace/storage/rdbms/hibernate/DatabaseAwareLobType.java b/dspace-api/src/main/java/org/dspace/storage/rdbms/hibernate/DatabaseAwareLobType.java new file mode 100644 index 000000000000..95939f9902aa --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/storage/rdbms/hibernate/DatabaseAwareLobType.java @@ -0,0 +1,57 @@ +/** + * 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.storage.rdbms.hibernate; + +import org.apache.commons.lang.StringUtils; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; +import org.hibernate.type.AbstractSingleColumnStandardBasicType; +import org.hibernate.type.descriptor.java.StringTypeDescriptor; +import org.hibernate.type.descriptor.sql.ClobTypeDescriptor; +import org.hibernate.type.descriptor.sql.LongVarcharTypeDescriptor; +import org.hibernate.type.descriptor.sql.SqlTypeDescriptor; + +/** + * A Hibernate @Type used to properly support the CLOB in both Postgres and Oracle. + * PostgreSQL doesn't have a CLOB type, instead it's a TEXT field. + * Normally, you'd use org.hibernate.type.TextType to support TEXT, but that won't work for Oracle. + * https://github.com/hibernate/hibernate-orm/blob/5.6/hibernate-core/src/main/java/org/hibernate/type/TextType.java + * + * This Type checks if we are using PostgreSQL. + * If so, it configures Hibernate to map CLOB to LongVarChar (same as org.hibernate.type.TextType) + * If not, it uses default CLOB (which works for other databases). + */ +public class DatabaseAwareLobType extends AbstractSingleColumnStandardBasicType { + + public static final DatabaseAwareLobType INSTANCE = new DatabaseAwareLobType(); + + public DatabaseAwareLobType() { + super( getDbDescriptor(), StringTypeDescriptor.INSTANCE ); + } + + public static SqlTypeDescriptor getDbDescriptor() { + if ( isPostgres() ) { + return LongVarcharTypeDescriptor.INSTANCE; + } else { + return ClobTypeDescriptor.DEFAULT; + } + } + + private static boolean isPostgres() { + ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + String dbDialect = configurationService.getProperty("db.dialect"); + + return StringUtils.containsIgnoreCase(dbDialect, "PostgreSQL"); + } + + @Override + public String getName() { + return "database_aware_lob"; + } +} + diff --git a/dspace-api/src/main/java/org/dspace/storage/rdbms/hibernate/postgres/DSpacePostgreSQL82Dialect.java b/dspace-api/src/main/java/org/dspace/storage/rdbms/hibernate/postgres/DSpacePostgreSQL82Dialect.java deleted file mode 100644 index 2701c22fd208..000000000000 --- a/dspace-api/src/main/java/org/dspace/storage/rdbms/hibernate/postgres/DSpacePostgreSQL82Dialect.java +++ /dev/null @@ -1,67 +0,0 @@ -/** - * 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.storage.rdbms.hibernate.postgres; - -import java.sql.Types; - -import org.hibernate.dialect.PostgreSQL82Dialect; -import org.hibernate.service.ServiceRegistry; -import org.hibernate.type.PostgresUUIDType; -import org.hibernate.type.descriptor.sql.LongVarcharTypeDescriptor; -import org.hibernate.type.descriptor.sql.SqlTypeDescriptor; - -/** - * UUID's are not supported by default in hibernate due to differences in the database in order to fix this a custom - * sql dialect is needed. - * Source: https://forum.hibernate.org/viewtopic.php?f=1&t=1014157 - * - * @author kevinvandevelde at atmire.com - */ -public class DSpacePostgreSQL82Dialect extends PostgreSQL82Dialect { - @Override - public void contributeTypes(final org.hibernate.boot.model.TypeContributions typeContributions, - final ServiceRegistry serviceRegistry) { - super.contributeTypes(typeContributions, serviceRegistry); - typeContributions.contributeType(new InternalPostgresUUIDType()); - } - - @Override - protected void registerHibernateType(int code, String name) { - super.registerHibernateType(code, name); - } - - protected static class InternalPostgresUUIDType extends PostgresUUIDType { - - @Override - protected boolean registerUnderJavaType() { - return true; - } - } - - /** - * Override is needed to properly support the CLOB on metadatavalue in Postgres and Oracle. - * - * @param sqlCode {@linkplain java.sql.Types JDBC type-code} for the column mapped by this type. - * @return Descriptor for the SQL/JDBC side of a value mapping. - */ - @Override - public SqlTypeDescriptor getSqlTypeDescriptorOverride(int sqlCode) { - SqlTypeDescriptor descriptor; - switch (sqlCode) { - case Types.CLOB: { - descriptor = LongVarcharTypeDescriptor.INSTANCE; - break; - } - default: { - descriptor = super.getSqlTypeDescriptorOverride(sqlCode); - break; - } - } - return descriptor; - } -} diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index d5c28da0964c..99e49e77e40c 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -80,9 +80,9 @@ db.url = jdbc:postgresql://localhost:5432/dspace db.driver = org.postgresql.Driver # Database Dialect (for Hibernate) -# * For Postgres: org.dspace.storage.rdbms.hibernate.postgres.DSpacePostgreSQL82Dialect +# * For Postgres: org.hibernate.dialect.PostgreSQL94Dialect # * For Oracle: org.hibernate.dialect.Oracle10gDialect -db.dialect = org.dspace.storage.rdbms.hibernate.postgres.DSpacePostgreSQL82Dialect +db.dialect = org.hibernate.dialect.PostgreSQL94Dialect # Database username and password db.username = dspace diff --git a/dspace/config/local.cfg.EXAMPLE b/dspace/config/local.cfg.EXAMPLE index 167d2d45da51..67c03808c48c 100644 --- a/dspace/config/local.cfg.EXAMPLE +++ b/dspace/config/local.cfg.EXAMPLE @@ -80,9 +80,9 @@ db.url = jdbc:postgresql://localhost:5432/dspace db.driver = org.postgresql.Driver # Database Dialect (for Hibernate) -# * For Postgres: org.dspace.storage.rdbms.hibernate.postgres.DSpacePostgreSQL82Dialect +# * For Postgres: org.hibernate.dialect.PostgreSQL94Dialect # * For Oracle: org.hibernate.dialect.Oracle10gDialect -db.dialect = org.dspace.storage.rdbms.hibernate.postgres.DSpacePostgreSQL82Dialect +db.dialect = org.hibernate.dialect.PostgreSQL94Dialect # Database username and password db.username = dspace From 967e36af7a9881e6c152347e594e760807f99c3f Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 2 Mar 2022 13:52:24 -0600 Subject: [PATCH 0089/1846] Fix Item & Bundle tests which check order of results. H2 2.x + Hibernate 5.x returns results ordered by UUID when unspecified. --- .../org/dspace/builder/BitstreamBuilder.java | 7 +-- .../app/rest/BitstreamControllerIT.java | 37 ++++++----- .../app/rest/BitstreamRestRepositoryIT.java | 29 ++++++--- .../app/rest/BundleRestRepositoryIT.java | 28 ++++----- .../app/rest/CollectionRestRepositoryIT.java | 8 ++- .../dspace/app/rest/ItemRestRepositoryIT.java | 58 ++++++++++------- .../app/rest/iiif/IIIFControllerIT.java | 63 +++++++++---------- 7 files changed, 130 insertions(+), 100 deletions(-) diff --git a/dspace-api/src/test/java/org/dspace/builder/BitstreamBuilder.java b/dspace-api/src/test/java/org/dspace/builder/BitstreamBuilder.java index 283091778e6a..424833e5cc65 100644 --- a/dspace-api/src/test/java/org/dspace/builder/BitstreamBuilder.java +++ b/dspace-api/src/test/java/org/dspace/builder/BitstreamBuilder.java @@ -18,6 +18,7 @@ import org.dspace.content.Bundle; import org.dspace.content.Item; import org.dspace.content.service.DSpaceObjectService; +import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.eperson.Group; @@ -26,8 +27,6 @@ */ public class BitstreamBuilder extends AbstractDSpaceObjectBuilder { - public static final String ORIGINAL = "ORIGINAL"; - private Bitstream bitstream; private Item item; private Group readerGroup; @@ -158,12 +157,12 @@ public BitstreamBuilder withIIIFToC(String toc) throws SQLException { } private Bundle getOriginalBundle(Item item) throws SQLException, AuthorizeException { - List bundles = itemService.getBundles(item, ORIGINAL); + List bundles = itemService.getBundles(item, Constants.CONTENT_BUNDLE_NAME); Bundle targetBundle = null; if (bundles.size() < 1) { // not found, create a new one - targetBundle = bundleService.create(context, item, ORIGINAL); + targetBundle = bundleService.create(context, item, Constants.CONTENT_BUNDLE_NAME); } else { // put bitstreams into first bundle targetBundle = bundles.iterator().next(); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamControllerIT.java index ca3c05ec30b9..4b1124071ad0 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamControllerIT.java @@ -16,6 +16,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.io.InputStream; +import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import org.apache.commons.codec.CharEncoding; @@ -144,47 +146,48 @@ public void getFirstBundleWhenMultipleBundles() throws Exception { String bitstreamContent = "ThisIsSomeDummyText"; - Bundle bundle1 = BundleBuilder.createBundle(context, publicItem1) + List bundles = new ArrayList(); + bundles.add(BundleBuilder.createBundle(context, publicItem1) .withName("TEST FIRST BUNDLE") - .build(); + .build()); Bitstream bitstream = null; try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) { bitstream = BitstreamBuilder. - createBitstream(context, bundle1, is) + createBitstream(context, bundles.get(0), is) .withName("Bitstream") .withDescription("description") .withMimeType("text/plain") .build(); } - Bundle bundle2 = BundleBuilder.createBundle(context, publicItem1) + bundles.add(BundleBuilder.createBundle(context, publicItem1) .withName("TEST SECOND BUNDLE") .withBitstream(bitstream) - .build(); + .build()); context.restoreAuthSystemState(); + // While in DSpace code, Bundles are *unordered*, in Hibernate v5 + H2 v2.x, they are returned sorted by UUID. + // So, we reorder this list of created Bundles by UUID to get their expected return order. + // NOTE: Once on Hibernate v6, this might need "toString()" removed as it may sort UUIDs based on RFC 4412. + Comparator compareByUUID = Comparator.comparing(b -> b.getID().toString()); + bundles.sort(compareByUUID); + String token = getAuthToken(admin.getEmail(), password); + // Expect only the first Bundle to be returned getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/bundle") .param("projection", "full")) .andExpect(status().isOk()) .andExpect(content().contentType(contentType)) .andExpect(jsonPath("$", Matchers.is( - BundleMatcher.matchBundle(bundle1.getName(), - bundle1.getID(), - bundle1.getHandle(), - bundle1.getType(), - bundle1.getBitstreams()) - - ))).andExpect(jsonPath("$", Matchers.not( - BundleMatcher.matchBundle(bundle2.getName(), - bundle2.getID(), - bundle2.getHandle(), - bundle2.getType(), - bundle2.getBitstreams()) + BundleMatcher.matchBundle(bundles.get(0).getName(), + bundles.get(0).getID(), + bundles.get(0).getHandle(), + bundles.get(0).getType(), + bundles.get(0).getBitstreams()) ))); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java index fd95e7f584d5..f9c1e469fcfe 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java @@ -20,6 +20,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.io.InputStream; +import java.util.Comparator; +import java.util.List; import java.util.UUID; import org.apache.commons.codec.CharEncoding; @@ -45,6 +47,7 @@ import org.dspace.content.Item; import org.dspace.content.service.BitstreamFormatService; import org.dspace.content.service.BitstreamService; +import org.dspace.content.service.ItemService; import org.dspace.core.Constants; import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; @@ -67,6 +70,10 @@ public class BitstreamRestRepositoryIT extends AbstractControllerIntegrationTest @Autowired private GroupService groupService; + + @Autowired + private ItemService itemService; + @Test public void findAllTest() throws Exception { //We turn off the authorization system in order to create the structure as defined below @@ -1480,11 +1487,19 @@ public void linksToFirstBundleWhenMultipleBundles() throws Exception { .build(); } - Bundle secondBundle = BundleBuilder.createBundle(context, publicItem1) + // Add default content bundle to list of bundles + List bundles = itemService.getBundles(publicItem1, Constants.CONTENT_BUNDLE_NAME); + + // Add this bitstream to a second bundle & append to list of bundles + bundles.add(BundleBuilder.createBundle(context, publicItem1) .withName("second bundle") - .withBitstream(bitstream).build(); + .withBitstream(bitstream).build()); - Bundle bundle = bitstream.getBundles().get(0); + // While in DSpace code, Bundles are *unordered*, in Hibernate v5 + H2 v2.x, they are returned sorted by UUID. + // So, we reorder this list of created Bundles by UUID to get their expected return order. + // NOTE: Once on Hibernate v6, this might need "toString()" removed as it may sort UUIDs based on RFC 4412. + Comparator compareByUUID = Comparator.comparing(b -> b.getID().toString()); + bundles.sort(compareByUUID); //Get bundle should contain the first bundle in the list getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/bundle")) @@ -1492,10 +1507,10 @@ public void linksToFirstBundleWhenMultipleBundles() throws Exception { .andExpect(content().contentType(contentType)) .andExpect(jsonPath("$", BundleMatcher.matchProperties( - bundle.getName(), - bundle.getID(), - bundle.getHandle(), - bundle.getType() + bundles.get(0).getName(), + bundles.get(0).getID(), + bundles.get(0).getHandle(), + bundles.get(0).getType() ) )); } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BundleRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BundleRestRepositoryIT.java index 884d55158291..96385095a200 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BundleRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BundleRestRepositoryIT.java @@ -397,26 +397,24 @@ public void createBundleOnNonExistingItem() throws Exception { public void getBitstreamsForBundle() throws Exception { context.turnOffAuthorisationSystem(); + bundle1 = BundleBuilder.createBundle(context, item) + .withName("testname") + .build(); + String bitstreamContent = "Dummy content"; try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) { - bitstream1 = BitstreamBuilder.createBitstream(context, item, is) + bitstream1 = BitstreamBuilder.createBitstream(context, item, is, bundle1.getName()) .withName("Bitstream") .withDescription("Description") .withMimeType("text/plain") .build(); - bitstream2 = BitstreamBuilder.createBitstream(context, item, is) + bitstream2 = BitstreamBuilder.createBitstream(context, item, is, bundle1.getName()) .withName("Bitstream2") .withDescription("Description2") .withMimeType("text/plain") .build(); } - bundle1 = BundleBuilder.createBundle(context, item) - .withName("testname") - .withBitstream(bitstream1) - .withBitstream(bitstream2) - .build(); - context.restoreAuthSystemState(); getClient().perform(get("/api/core/bundles/" + bundle1.getID() + "/bitstreams") @@ -465,26 +463,24 @@ public void getBitstreamsForBundleForbiddenTest() throws Exception { public void patchMoveBitstreams() throws Exception { context.turnOffAuthorisationSystem(); + bundle1 = BundleBuilder.createBundle(context, item) + .withName("testname") + .build(); + String bitstreamContent = "Dummy content"; try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) { - bitstream1 = BitstreamBuilder.createBitstream(context, item, is) + bitstream1 = BitstreamBuilder.createBitstream(context, item, is, bundle1.getName()) .withName("Bitstream") .withDescription("Description") .withMimeType("text/plain") .build(); - bitstream2 = BitstreamBuilder.createBitstream(context, item, is) + bitstream2 = BitstreamBuilder.createBitstream(context, item, is, bundle1.getName()) .withName("Bitstream2") .withDescription("Description2") .withMimeType("text/plain") .build(); } - bundle1 = BundleBuilder.createBundle(context, item) - .withName("testname") - .withBitstream(bitstream1) - .withBitstream(bitstream2) - .build(); - context.restoreAuthSystemState(); getClient().perform(get("/api/core/bundles/" + bundle1.getID() + "/bitstreams") diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CollectionRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CollectionRestRepositoryIT.java index 92fdecf8249c..6ae33cb2801b 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CollectionRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CollectionRestRepositoryIT.java @@ -2421,9 +2421,11 @@ public void findOneTestWithEmbedsWithPageSize() throws Exception { .build(); List items = new ArrayList(); - // This comparator is used to sort our test Items by java.util.UUID (which sorts them based on the RFC - // and not based on String comparison, see also https://stackoverflow.com/a/51031298/3750035 ) - Comparator compareByUUID = Comparator.comparing(i -> i.getID()); + // Hibernate 5.x's org.hibernate.dialect.H2Dialect sorts UUIDs as if they are Strings. + // So, we must compare UUIDs as if they are strings. + // In Hibernate 6, the H2Dialect has been updated with native UUID type support, at which point + // we'd need to update the below comparator to compare them as java.util.UUID (which sorts based on RFC 4412). + Comparator compareByUUID = Comparator.comparing(i -> i.getID().toString()); Item item0 = ItemBuilder.createItem(context, collection).withTitle("Item 0").build(); items.add(item0); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java index 372ab084dd08..95ec53772711 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java @@ -123,9 +123,11 @@ public void findAllTest() throws Exception { Collection col2 = CollectionBuilder.createCollection(context, child1).withName("Collection 2").build(); List items = new ArrayList(); - // This comparator is used to sort our test Items by java.util.UUID (which sorts them based on the RFC - // and not based on String comparison, see also https://stackoverflow.com/a/51031298/3750035 ) - Comparator compareByUUID = Comparator.comparing(i -> i.getID()); + // Hibernate 5.x's org.hibernate.dialect.H2Dialect sorts UUIDs as if they are Strings. + // So, we must compare UUIDs as if they are strings. + // In Hibernate 6, the H2Dialect has been updated with native UUID type support, at which point + // we'd need to update the below comparator to compare them as java.util.UUID (which sorts based on RFC 4412). + Comparator compareByUUID = Comparator.comparing(i -> i.getID().toString()); //2. Three public items that are readable by Anonymous with different subjects Item publicItem1 = ItemBuilder.createItem(context, col1) @@ -204,9 +206,11 @@ public void findAllWithPaginationTest() throws Exception { .build(); List items = new ArrayList(); - // This comparator is used to sort our test Items by java.util.UUID (which sorts them based on the RFC - // and not based on String comparison, see also https://stackoverflow.com/a/51031298/3750035 ) - Comparator compareByUUID = Comparator.comparing(i -> i.getID()); + // Hibernate 5.x's org.hibernate.dialect.H2Dialect sorts UUIDs as if they are Strings. + // So, we must compare UUIDs as if they are strings. + // In Hibernate 6, the H2Dialect has been updated with native UUID type support, at which point + // we'd need to update the below comparator to compare them as java.util.UUID (which sorts based on RFC 4412). + Comparator compareByUUID = Comparator.comparing(i -> i.getID().toString()); //2. Three public items that are readable by Anonymous with different subjects Item publicItem1 = ItemBuilder.createItem(context, col1) @@ -3213,16 +3217,23 @@ public void findOneTestWithEmbedsWithPageSize() throws Exception { Item item = ItemBuilder.createItem(context, collection).withTitle("Item").build(); - Bundle bundle0 = BundleBuilder.createBundle(context, item).withName("Bundle 0").build(); - Bundle bundle1 = BundleBuilder.createBundle(context, item).withName("Bundle 1").build(); - Bundle bundle2 = BundleBuilder.createBundle(context, item).withName("Bundle 2").build(); - Bundle bundle3 = BundleBuilder.createBundle(context, item).withName("Bundle 3").build(); - Bundle bundle4 = BundleBuilder.createBundle(context, item).withName("Bundle 4").build(); - Bundle bundle5 = BundleBuilder.createBundle(context, item).withName("Bundle 5").build(); - Bundle bundle6 = BundleBuilder.createBundle(context, item).withName("Bundle 6").build(); - Bundle bundle7 = BundleBuilder.createBundle(context, item).withName("Bundle 7").build(); - Bundle bundle8 = BundleBuilder.createBundle(context, item).withName("Bundle 8").build(); - Bundle bundle9 = BundleBuilder.createBundle(context, item).withName("Bundle 9").build(); + List bundles = new ArrayList(); + bundles.add(BundleBuilder.createBundle(context, item).withName("Bundle 0").build()); + bundles.add(BundleBuilder.createBundle(context, item).withName("Bundle 1").build()); + bundles.add(BundleBuilder.createBundle(context, item).withName("Bundle 2").build()); + bundles.add(BundleBuilder.createBundle(context, item).withName("Bundle 3").build()); + bundles.add(BundleBuilder.createBundle(context, item).withName("Bundle 4").build()); + bundles.add(BundleBuilder.createBundle(context, item).withName("Bundle 5").build()); + bundles.add(BundleBuilder.createBundle(context, item).withName("Bundle 6").build()); + bundles.add(BundleBuilder.createBundle(context, item).withName("Bundle 7").build()); + bundles.add(BundleBuilder.createBundle(context, item).withName("Bundle 8").build()); + bundles.add(BundleBuilder.createBundle(context, item).withName("Bundle 9").build()); + + // While in DSpace code, Bundles are *unordered*, in Hibernate v5 + H2 v2.x, they are returned sorted by UUID. + // So, we reorder this list of created Bundles by UUID to get their expected pagination ordering. + // NOTE: Once on Hibernate v6, this might need "toString()" removed as it may sort UUIDs based on RFC 4412. + Comparator compareByUUID = Comparator.comparing(b -> b.getID().toString()); + bundles.sort(compareByUUID); context.restoreAuthSystemState(); @@ -3232,11 +3243,16 @@ public void findOneTestWithEmbedsWithPageSize() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$", ItemMatcher.matchItemProperties(item))) .andExpect(jsonPath("$._embedded.bundles._embedded.bundles",Matchers.containsInAnyOrder( - BundleMatcher.matchProperties(bundle0.getName(), bundle0.getID(), bundle0.getHandle(), bundle0.getType()), - BundleMatcher.matchProperties(bundle1.getName(), bundle1.getID(), bundle1.getHandle(), bundle1.getType()), - BundleMatcher.matchProperties(bundle2.getName(), bundle2.getID(), bundle2.getHandle(), bundle2.getType()), - BundleMatcher.matchProperties(bundle3.getName(), bundle3.getID(), bundle3.getHandle(), bundle3.getType()), - BundleMatcher.matchProperties(bundle4.getName(), bundle4.getID(), bundle4.getHandle(), bundle4.getType()) + BundleMatcher.matchProperties(bundles.get(0).getName(), bundles.get(0).getID(), bundles.get(0).getHandle(), + bundles.get(0).getType()), + BundleMatcher.matchProperties(bundles.get(1).getName(), bundles.get(1).getID(), bundles.get(1).getHandle(), + bundles.get(1).getType()), + BundleMatcher.matchProperties(bundles.get(2).getName(), bundles.get(2).getID(), bundles.get(2).getHandle(), + bundles.get(2).getType()), + BundleMatcher.matchProperties(bundles.get(3).getName(), bundles.get(3).getID(), bundles.get(3).getHandle(), + bundles.get(3).getType()), + BundleMatcher.matchProperties(bundles.get(4).getName(), bundles.get(4).getID(), bundles.get(4).getHandle(), + bundles.get(4).getType()) ))) .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/core/items/" + item.getID()))) .andExpect(jsonPath("$._embedded.bundles.page.size", is(5))) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/iiif/IIIFControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/iiif/IIIFControllerIT.java index 71090cd70f1a..beafcb9622de 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/iiif/IIIFControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/iiif/IIIFControllerIT.java @@ -469,40 +469,39 @@ public void findOneWithBundleStructures() throws Exception { .withMimeType("image/tiff") .build(); } - context.restoreAuthSystemState(); - // expect structures elements with label and canvas id. + + // Expected structures elements based on the above test content + // NOTE: we cannot guarantee the order of Bundles in the Manifest, therefore this test has to simply check + // that each Bundle exists in the manifest with Canvases corresponding to each bitstream. getClient().perform(get("/iiif/" + publicItem1.getID() + "/manifest")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.@context", is("http://iiif.io/api/presentation/2/context.json"))) - .andExpect(jsonPath("$.sequences[0].canvases[0].@id", - Matchers.containsString("/iiif/" + publicItem1.getID() + "/canvas/c0"))) - .andExpect(jsonPath("$.sequences[0].canvases[0].label", is("Global 1"))) - .andExpect(jsonPath("$.sequences[0].canvases[0].width", is(2000))) - .andExpect(jsonPath("$.sequences[0].canvases[0].height", is(3000))) - .andExpect(jsonPath("$.sequences[0].canvases[1].label", is("Global 2"))) - .andExpect(jsonPath("$.sequences[0].canvases[2].label", is("Global 3"))) - .andExpect(jsonPath("$.structures[0].@id", - Matchers.endsWith("/iiif/" + publicItem1.getID() + "/manifest/range/r0"))) - .andExpect(jsonPath("$.structures[0].label", is("Table of Contents"))) - .andExpect(jsonPath("$.structures[0].viewingHint", is("top"))) - .andExpect(jsonPath("$.structures[0].ranges[0]", - Matchers.endsWith("/iiif/" + publicItem1.getID() + "/manifest/range/r0-0"))) - .andExpect(jsonPath("$.structures[0].ranges[1]", - Matchers.endsWith("/iiif/" + publicItem1.getID() + "/manifest/range/r0-1"))) - .andExpect(jsonPath("$.structures[1].@id", - Matchers.endsWith("/iiif/" + publicItem1.getID() + "/manifest/range/r0-0"))) - .andExpect(jsonPath("$.structures[1].label", is("ORIGINAL"))) - .andExpect(jsonPath("$.structures[1].canvases[0]", - Matchers.containsString("/iiif/" + publicItem1.getID() + "/canvas/c0"))) - .andExpect(jsonPath("$.structures[2].@id", - Matchers.endsWith("/iiif/" + publicItem1.getID() + "/manifest/range/r0-1"))) - .andExpect(jsonPath("$.structures[2].label", is("IIIF"))) - .andExpect(jsonPath("$.structures[2].canvases[0]", - Matchers.containsString("/iiif/" + publicItem1.getID() + "/canvas/c1"))) - .andExpect(jsonPath("$.structures[2].canvases[1]", - Matchers.containsString("/iiif/" + publicItem1.getID() + "/canvas/c2"))) - .andExpect(jsonPath("$.service").exists()); + .andExpect(status().isOk()) + .andExpect(jsonPath("$.@context", is("http://iiif.io/api/presentation/2/context.json"))) + // should contain 3 canvases, corresponding to each bitstream + .andExpect(jsonPath("$.sequences[0].canvases[*].label", + Matchers.contains("Global 1", "Global 2", "Global 3"))) + + // First structure should be a Table of Contents + .andExpect(jsonPath("$.structures[0].@id", + Matchers.endsWith("/iiif/" + publicItem1.getID() + "/manifest/range/r0"))) + .andExpect(jsonPath("$.structures[0].label", is("Table of Contents"))) + .andExpect(jsonPath("$.structures[0].viewingHint", is("top"))) + .andExpect(jsonPath("$.structures[0].ranges[0]", + Matchers.endsWith("/iiif/" + publicItem1.getID() + "/manifest/range/r0-0"))) + .andExpect(jsonPath("$.structures[0].ranges[1]", + Matchers.endsWith("/iiif/" + publicItem1.getID() + "/manifest/range/r0-1"))) + + // Should contain a structure with label=IIIF, corresponding to IIIF bundle + // It should have exactly 2 canvases (corresponding to 2 bitstreams) + .andExpect(jsonPath("$.structures[?(@.label=='IIIF')].canvases[0]").exists()) + .andExpect(jsonPath("$.structures[?(@.label=='IIIF')].canvases[1]").exists()) + .andExpect(jsonPath("$.structures[?(@.label=='IIIF')].canvases[2]").doesNotExist()) + + // Should contain a structure with label=ORIGINAL, corresponding to ORIGINAL bundle + // It should have exactly 1 canvas (corresponding to 1 bitstream) + .andExpect(jsonPath("$.structures[?(@.label=='ORIGINAL')].canvases[0]").exists()) + .andExpect(jsonPath("$.structures[?(@.label=='ORIGINAL')].canvases[1]").doesNotExist()) + .andExpect(jsonPath("$.service").exists()); } @Test From b4915868ddd09e87c975445b898b6c19d4b76a3d Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 7 Mar 2022 13:06:00 -0600 Subject: [PATCH 0090/1846] Entity Types now come back in a different order. Updating pagination tests based on new ordering --- .../app/rest/EntityTypeRestRepositoryIT.java | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EntityTypeRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EntityTypeRestRepositoryIT.java index 93aacb60e4e2..740a2c0dc388 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EntityTypeRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EntityTypeRestRepositoryIT.java @@ -86,13 +86,13 @@ public void getAllEntityTypeEndpointWithPaging() throws Exception { .andExpect(jsonPath("$._links.self.href", containsString("api/core/entitytypes"))) //We have 4 facets in the default configuration, they need to all be present in the embedded section .andExpect(jsonPath("$._embedded.entitytypes", containsInAnyOrder( - EntityTypeMatcher.matchEntityTypeEntry(entityTypeService.findByEntityType(context, - Constants.ENTITY_TYPE_NONE)), + EntityTypeMatcher.matchEntityTypeEntry(entityTypeService.findByEntityType(context, "Journal")), EntityTypeMatcher - .matchEntityTypeEntry(entityTypeService.findByEntityType(context, "Publication")), - EntityTypeMatcher.matchEntityTypeEntry(entityTypeService.findByEntityType(context, "Person")), - EntityTypeMatcher.matchEntityTypeEntry(entityTypeService.findByEntityType(context, "Project")), - EntityTypeMatcher.matchEntityTypeEntry(entityTypeService.findByEntityType(context, "OrgUnit")) + .matchEntityTypeEntry(entityTypeService.findByEntityType(context, "JournalIssue")), + EntityTypeMatcher + .matchEntityTypeEntry(entityTypeService.findByEntityType(context, "JournalVolume")), + EntityTypeMatcher.matchEntityTypeEntry(entityTypeService.findByEntityType(context, "OrgUnit")), + EntityTypeMatcher.matchEntityTypeEntry(entityTypeService.findByEntityType(context, "Person")) ))); getClient().perform(get("/api/core/entitytypes").param("size", "5").param("page", "1")) @@ -108,11 +108,12 @@ public void getAllEntityTypeEndpointWithPaging() throws Exception { .andExpect(jsonPath("$._links.self.href", containsString("api/core/entitytypes"))) //We have 4 facets in the default configuration, they need to all be present in the embedded section .andExpect(jsonPath("$._embedded.entitytypes", containsInAnyOrder( - EntityTypeMatcher.matchEntityTypeEntry(entityTypeService.findByEntityType(context, "Journal")), + EntityTypeMatcher.matchEntityTypeEntry(entityTypeService.findByEntityType(context, "Project")), EntityTypeMatcher - .matchEntityTypeEntry(entityTypeService.findByEntityType(context, "JournalVolume")), + .matchEntityTypeEntry(entityTypeService.findByEntityType(context, "Publication")), EntityTypeMatcher - .matchEntityTypeEntry(entityTypeService.findByEntityType(context, "JournalIssue")) + .matchEntityTypeEntry(entityTypeService.findByEntityType(context, + Constants.ENTITY_TYPE_NONE)) ))); } @@ -137,10 +138,11 @@ public void findAllPaginationTest() throws Exception { .param("size", "3")) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.entitytypes", containsInAnyOrder( - EntityTypeMatcher.matchEntityTypeEntry(entityTypeService.findByEntityType(context, "Publication")), - EntityTypeMatcher.matchEntityTypeEntry(entityTypeService.findByEntityType(context, "Person")), + EntityTypeMatcher.matchEntityTypeEntry(entityTypeService.findByEntityType(context, "Journal")), + EntityTypeMatcher.matchEntityTypeEntry(entityTypeService.findByEntityType(context, + "JournalIssue")), EntityTypeMatcher.matchEntityTypeEntry(entityTypeService.findByEntityType(context, - Constants.ENTITY_TYPE_NONE)) + "JournalVolume")) ))) .andExpect(jsonPath("$._links.first.href", Matchers.allOf( Matchers.containsString("/api/core/entitytypes?"), @@ -165,7 +167,7 @@ public void findAllPaginationTest() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.entitytypes", containsInAnyOrder( EntityTypeMatcher.matchEntityTypeEntry(entityTypeService.findByEntityType(context, "OrgUnit")), - EntityTypeMatcher.matchEntityTypeEntry(entityTypeService.findByEntityType(context, "Journal")), + EntityTypeMatcher.matchEntityTypeEntry(entityTypeService.findByEntityType(context, "Person")), EntityTypeMatcher.matchEntityTypeEntry(entityTypeService.findByEntityType(context, "Project")) ))) .andExpect(jsonPath("$._links.first.href", Matchers.allOf( From f8e8866f7563336dc0997dc9431b3f150b7c4e7a Mon Sep 17 00:00:00 2001 From: eskander Date: Thu, 31 Mar 2022 18:19:32 +0200 Subject: [PATCH 0091/1846] [CST-5306] Migrate Researcher Profile (REST). - added new four test functions for claim profile. - formatting the code. - removed unused classes. --- .../dspace/app/profile/ResearcherProfile.java | 16 +- .../profile/ResearcherProfileServiceImpl.java | 12 +- .../service/ResearcherProfileService.java | 8 +- .../authorize/AuthorizeServiceImpl.java | 4 - .../authorize/service/AuthorizeService.java | 1 - .../dspace/content/service/ItemService.java | 5 +- .../test/data/dspaceFolder/config/local.cfg | 4 +- .../org/dspace/app/matcher/LambdaMatcher.java | 55 ----- .../app/matcher/MetadataValueMatcher.java | 96 -------- .../java/org/dspace/builder/ItemBuilder.java | 11 +- .../impl/ResearcherProfileAutomaticClaim.java | 19 +- .../app/rest/model/ResearcherProfileRest.java | 7 +- .../ResearcherProfileRestRepository.java | 15 +- ...rProfileRestPermissionEvaluatorPlugin.java | 14 +- .../ResearcherProfileRestRepositoryIT.java | 222 ++++++++++-------- 15 files changed, 179 insertions(+), 310 deletions(-) delete mode 100644 dspace-api/src/test/java/org/dspace/app/matcher/LambdaMatcher.java delete mode 100644 dspace-api/src/test/java/org/dspace/app/matcher/MetadataValueMatcher.java diff --git a/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfile.java b/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfile.java index d282f0cb4778..21dbe36da6a9 100644 --- a/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfile.java +++ b/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfile.java @@ -7,17 +7,17 @@ */ package org.dspace.app.profile; -import org.dspace.content.Item; -import org.dspace.content.MetadataValue; -import org.dspace.util.UUIDUtils; -import org.springframework.util.Assert; +import static org.dspace.core.Constants.READ; +import static org.dspace.eperson.Group.ANONYMOUS; import java.util.Optional; import java.util.UUID; import java.util.stream.Stream; -import static org.dspace.core.Constants.READ; -import static org.dspace.eperson.Group.ANONYMOUS; +import org.dspace.content.Item; +import org.dspace.content.MetadataValue; +import org.dspace.util.UUIDUtils; +import org.springframework.util.Assert; /** * Object representing a Researcher Profile. @@ -65,7 +65,9 @@ public Item getItem() { private MetadataValue getDspaceObjectOwnerMetadata(Item item) { return getMetadataValue(item, "dspace.object.owner") .filter(metadata -> UUIDUtils.fromString(metadata.getAuthority()) != null) - .orElseThrow(() -> new IllegalArgumentException("A profile item must have a valid dspace.object.owner metadata")); + .orElseThrow( + () -> new IllegalArgumentException("A profile item must have a valid dspace.object.owner metadata") + ); } private Optional getMetadataValue(Item item, String metadataField) { diff --git a/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfileServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfileServiceImpl.java index 1e81636ff68d..75c1db45b018 100644 --- a/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfileServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfileServiceImpl.java @@ -15,12 +15,10 @@ import java.net.URI; import java.sql.SQLException; import java.util.Arrays; -import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.UUID; -import javax.annotation.PostConstruct; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang.StringUtils; @@ -187,8 +185,8 @@ public ResearcherProfile claim(final Context context, final EPerson ePerson, fin } context.turnOffAuthorisationSystem(); - itemService.addMetadata(context, item, "dspace", "object", "owner", null, ePerson.getName(), - ePerson.getID().toString(), CF_ACCEPTED); + itemService.addMetadata(context, item, "dspace", "object", "owner", null, + ePerson.getName(), ePerson.getID().toString(), CF_ACCEPTED); context.restoreAuthSystemState(); return new ResearcherProfile(item); @@ -249,8 +247,10 @@ private Item createProfileItem(Context context, EPerson ePerson, Collection coll WorkspaceItem workspaceItem = workspaceItemService.create(context, collection, true); Item item = workspaceItem.getItem(); - itemService.addMetadata(context, item, "dc", "title", null, null, ePerson.getFullName()); - itemService.addMetadata(context, item, "dspace", "object", "owner", null, ePerson.getFullName(), id, CF_ACCEPTED); + itemService.addMetadata(context, item, "dc", "title", null, null, + ePerson.getFullName()); + itemService.addMetadata(context, item, "dspace", "object", "owner", null, + ePerson.getFullName(), id, CF_ACCEPTED); item = installItemService.installItem(context, workspaceItem); diff --git a/dspace-api/src/main/java/org/dspace/app/profile/service/ResearcherProfileService.java b/dspace-api/src/main/java/org/dspace/app/profile/service/ResearcherProfileService.java index 0d565f07549e..674b440b0069 100644 --- a/dspace-api/src/main/java/org/dspace/app/profile/service/ResearcherProfileService.java +++ b/dspace-api/src/main/java/org/dspace/app/profile/service/ResearcherProfileService.java @@ -7,16 +7,16 @@ */ package org.dspace.app.profile.service; +import java.net.URI; +import java.sql.SQLException; +import java.util.UUID; + import org.dspace.app.profile.ResearcherProfile; import org.dspace.authorize.AuthorizeException; import org.dspace.core.Context; import org.dspace.discovery.SearchServiceException; import org.dspace.eperson.EPerson; -import java.net.URI; -import java.sql.SQLException; -import java.util.UUID; - /** * Service interface class for the {@link ResearcherProfile} object. The * implementation of this class is responsible for all business logic calls for diff --git a/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java b/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java index 57300e8e1864..919e82f14f58 100644 --- a/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java @@ -959,8 +959,4 @@ private String formatCustomQuery(String query) { return query + " AND "; } } - @Override - public boolean isPartOfTheGroup(Context c, String egroup) throws SQLException { - return false; - } } diff --git a/dspace-api/src/main/java/org/dspace/authorize/service/AuthorizeService.java b/dspace-api/src/main/java/org/dspace/authorize/service/AuthorizeService.java index 65bf0f31f9a7..9f6171a22030 100644 --- a/dspace-api/src/main/java/org/dspace/authorize/service/AuthorizeService.java +++ b/dspace-api/src/main/java/org/dspace/authorize/service/AuthorizeService.java @@ -592,5 +592,4 @@ List findAdminAuthorizedCollection(Context context, String query, in */ long countAdminAuthorizedCollection(Context context, String query) throws SearchServiceException, SQLException; - public boolean isPartOfTheGroup(Context c, String egroup) throws SQLException; } diff --git a/dspace-api/src/main/java/org/dspace/content/service/ItemService.java b/dspace-api/src/main/java/org/dspace/content/service/ItemService.java index 2d5468029925..401c8fdbec1d 100644 --- a/dspace-api/src/main/java/org/dspace/content/service/ItemService.java +++ b/dspace-api/src/main/java/org/dspace/content/service/ItemService.java @@ -592,8 +592,9 @@ public void move(Context context, Item item, Collection from, Collection to, boo * @throws SQLException if database error * @throws AuthorizeException if authorization error */ - public Iterator findArchivedByMetadataField(Context context, String schema, String element, - String qualifier, String value) throws SQLException, AuthorizeException; + public Iterator findArchivedByMetadataField(Context context, String schema, + String element, String qualifier, + String value) throws SQLException, AuthorizeException; /** * Returns an iterator of in archive items possessing the passed metadata field, or only diff --git a/dspace-api/src/test/data/dspaceFolder/config/local.cfg b/dspace-api/src/test/data/dspaceFolder/config/local.cfg index 11a344fa0dc4..34acade122e6 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/local.cfg +++ b/dspace-api/src/test/data/dspaceFolder/config/local.cfg @@ -20,7 +20,6 @@ # For example, including "dspace.dir" in this local.cfg will override the # default value of "dspace.dir" in the dspace.cfg file. # -researcher-profile.type = Person ########################## # SERVER CONFIGURATION # ########################## @@ -144,3 +143,6 @@ authentication-ip.Student = 6.6.6.6 useProxies = true proxies.trusted.ipranges = 7.7.7.7 proxies.trusted.include_ui_ip = true + +# researcher-profile.type +researcher-profile.type = Person \ No newline at end of file diff --git a/dspace-api/src/test/java/org/dspace/app/matcher/LambdaMatcher.java b/dspace-api/src/test/java/org/dspace/app/matcher/LambdaMatcher.java deleted file mode 100644 index 641aee57040a..000000000000 --- a/dspace-api/src/test/java/org/dspace/app/matcher/LambdaMatcher.java +++ /dev/null @@ -1,55 +0,0 @@ -/** - * 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.matcher; - -import org.hamcrest.BaseMatcher; -import org.hamcrest.Description; -import org.hamcrest.Matcher; -import org.hamcrest.Matchers; - -import java.util.function.Predicate; - -/** - * Matcher based on an {@link Predicate}. - * - * @author Luca Giamminonni (luca.giamminonni at 4science.it) - * @param the type of the instance to match - */ -public class LambdaMatcher extends BaseMatcher { - - private final Predicate matcher; - private final String description; - - public static LambdaMatcher matches(Predicate matcher) { - return new LambdaMatcher(matcher, "Matches the given predicate"); - } - - public static LambdaMatcher matches(Predicate matcher, String description) { - return new LambdaMatcher(matcher, description); - } - - public static Matcher> has(Predicate matcher) { - return Matchers.hasItem(matches(matcher)); - } - - private LambdaMatcher(Predicate matcher, String description) { - this.matcher = matcher; - this.description = description; - } - - @Override - @SuppressWarnings("unchecked") - public boolean matches(Object argument) { - return matcher.test((T) argument); - } - - @Override - public void describeTo(Description description) { - description.appendText(this.description); - } -} diff --git a/dspace-api/src/test/java/org/dspace/app/matcher/MetadataValueMatcher.java b/dspace-api/src/test/java/org/dspace/app/matcher/MetadataValueMatcher.java deleted file mode 100644 index eb2158a9ce14..000000000000 --- a/dspace-api/src/test/java/org/dspace/app/matcher/MetadataValueMatcher.java +++ /dev/null @@ -1,96 +0,0 @@ -/** - * 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.matcher; - -import org.dspace.content.MetadataValue; -import org.hamcrest.Description; -import org.hamcrest.TypeSafeMatcher; - -import java.util.Objects; - -/** - * Implementation of {@link org.hamcrest.Matcher} to match a MetadataValue by - * all its attributes. - * - * @author Luca Giamminonni (luca.giamminonni at 4science.it) - * - */ -public class MetadataValueMatcher extends TypeSafeMatcher { - - private String field; - - private String value; - - private String language; - - private String authority; - - private Integer place; - - private Integer confidence; - - private MetadataValueMatcher(String field, String value, String language, String authority, Integer place, - Integer confidence) { - - this.field = field; - this.value = value; - this.language = language; - this.authority = authority; - this.place = place; - this.confidence = confidence; - - } - - @Override - public void describeTo(Description description) { - description.appendText("MetadataValue with the following attributes [field=" + field + ", value=" - + value + ", language=" + language + ", authority=" + authority + ", place=" + place + ", confidence=" - + confidence + "]"); - } - - @Override - protected void describeMismatchSafely(MetadataValue item, Description mismatchDescription) { - mismatchDescription.appendText("was ") - .appendValue("MetadataValue [metadataField=").appendValue(item.getMetadataField().toString('.')) - .appendValue(", value=").appendValue(item.getValue()).appendValue(", language=").appendValue(language) - .appendValue(", place=").appendValue(item.getPlace()).appendValue(", authority=") - .appendValue(item.getAuthority()).appendValue(", confidence=").appendValue(item.getConfidence() + "]"); - } - - @Override - protected boolean matchesSafely(MetadataValue metadataValue) { - return Objects.equals(metadataValue.getValue(), value) && - Objects.equals(metadataValue.getMetadataField().toString('.'), field) && - Objects.equals(metadataValue.getLanguage(), language) && - Objects.equals(metadataValue.getAuthority(), authority) && - Objects.equals(metadataValue.getPlace(), place) && - Objects.equals(metadataValue.getConfidence(), confidence); - } - - public static MetadataValueMatcher with(String field, String value, String language, - String authority, Integer place, Integer confidence) { - return new MetadataValueMatcher(field, value, language, authority, place, confidence); - } - - public static MetadataValueMatcher with(String field, String value) { - return with(field, value, null, null, 0, -1); - } - - public static MetadataValueMatcher with(String field, String value, String authority, int place, int confidence) { - return with(field, value, null, authority, place, confidence); - } - - public static MetadataValueMatcher with(String field, String value, String authority, int confidence) { - return with(field, value, null, authority, 0, confidence); - } - - public static MetadataValueMatcher with(String field, String value, int place) { - return with(field, value, null, null, place, -1); - } - -} diff --git a/dspace-api/src/test/java/org/dspace/builder/ItemBuilder.java b/dspace-api/src/test/java/org/dspace/builder/ItemBuilder.java index ac32e9a2ae84..23371df12722 100644 --- a/dspace-api/src/test/java/org/dspace/builder/ItemBuilder.java +++ b/dspace-api/src/test/java/org/dspace/builder/ItemBuilder.java @@ -7,6 +7,9 @@ */ package org.dspace.builder; +import static org.dspace.content.MetadataSchemaEnum.DC; +import static org.dspace.content.authority.Choices.CF_ACCEPTED; + import java.io.IOException; import java.sql.SQLException; import java.util.UUID; @@ -22,9 +25,6 @@ import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; -import static org.dspace.content.MetadataSchemaEnum.DC; -import static org.dspace.content.authority.Choices.CF_ACCEPTED; - /** * Builder to construct Item objects * @@ -156,9 +156,6 @@ public ItemBuilder withDspaceObjectOwner(String value, String authority) { return addMetadataValue(item, "dspace", "object", "owner", null, value, authority, CF_ACCEPTED); } - public ItemBuilder withDspaceObjectOwner(EPerson ePerson) { - return withDspaceObjectOwner(ePerson.getFullName(), ePerson.getID().toString()); - } public ItemBuilder makeUnDiscoverable() { item.setDiscoverable(false); return this; @@ -187,7 +184,7 @@ public ItemBuilder withReaderGroup(Group group) { /** * Create an admin group for the collection with the specified members * - * @param members epersons to add to the admin group + * @param ePerson epersons to add to the admin group * @return this builder * @throws SQLException * @throws AuthorizeException diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/login/impl/ResearcherProfileAutomaticClaim.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/login/impl/ResearcherProfileAutomaticClaim.java index bcc10781e98d..3c3f459fd1cf 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/login/impl/ResearcherProfileAutomaticClaim.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/login/impl/ResearcherProfileAutomaticClaim.java @@ -7,6 +7,14 @@ */ package org.dspace.app.rest.login.impl; +import static org.apache.commons.collections4.IteratorUtils.toList; +import static org.dspace.content.authority.Choices.CF_ACCEPTED; + +import java.sql.SQLException; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.dspace.app.profile.service.ResearcherProfileService; @@ -23,14 +31,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.Assert; -import java.sql.SQLException; -import java.util.List; -import java.util.UUID; -import java.util.stream.Collectors; - -import static org.apache.commons.collections4.IteratorUtils.toList; -import static org.dspace.content.authority.Choices.CF_ACCEPTED; - /** * Implementation of {@link PostLoggedInAction} that perform an automatic claim * between the logged eperson and possible profiles without eperson present in @@ -91,7 +91,8 @@ private void claimProfile(Context context, EPerson currentUser) throws SQLExcept Item item = findClaimableItem(context, currentUser); if (item != null) { - itemService.addMetadata(context, item, "dspace", "object", "owner", null, fullName, id.toString(), CF_ACCEPTED); + itemService.addMetadata(context, item, "dspace", "object", "owner", + null, fullName, id.toString(), CF_ACCEPTED); } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ResearcherProfileRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ResearcherProfileRest.java index ff789f93237f..ef145d5e7175 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ResearcherProfileRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ResearcherProfileRest.java @@ -7,13 +7,10 @@ */ package org.dspace.app.rest.model; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonInclude.Include; -import org.dspace.app.rest.RestResourceController; - -import java.util.List; import java.util.UUID; +import org.dspace.app.rest.RestResourceController; + /** * The Researcher Profile REST resource. * diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResearcherProfileRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResearcherProfileRestRepository.java index 8be3720f52d8..25eb3d938af3 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResearcherProfileRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResearcherProfileRestRepository.java @@ -7,6 +7,12 @@ */ package org.dspace.app.rest.repository; +import java.net.URI; +import java.sql.SQLException; +import java.util.List; +import java.util.UUID; +import javax.servlet.http.HttpServletRequest; + import org.apache.commons.collections.CollectionUtils; import org.dspace.app.profile.ResearcherProfile; import org.dspace.app.profile.service.ResearcherProfileService; @@ -25,7 +31,6 @@ import org.dspace.util.UUIDUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Conditional; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.rest.webmvc.ResourceNotFoundException; @@ -34,12 +39,6 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; -import javax.servlet.http.HttpServletRequest; -import java.net.URI; -import java.sql.SQLException; -import java.util.List; -import java.util.UUID; - /** * This is the repository responsible of exposing researcher profiles. * @@ -48,7 +47,7 @@ */ @Component(ResearcherProfileRest.CATEGORY + "." + ResearcherProfileRest.NAME) @ConditionalOnProperty( - value="researcher-profile.type" + value = "researcher-profile.type" ) public class ResearcherProfileRestRepository extends DSpaceRestRepository { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResearcherProfileRestPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResearcherProfileRestPermissionEvaluatorPlugin.java index 7545e9ad9c21..66cc873db25a 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResearcherProfileRestPermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResearcherProfileRestPermissionEvaluatorPlugin.java @@ -7,6 +7,14 @@ */ package org.dspace.app.rest.security; +import static org.dspace.app.rest.security.DSpaceRestPermission.DELETE; +import static org.dspace.app.rest.security.DSpaceRestPermission.READ; +import static org.dspace.app.rest.security.DSpaceRestPermission.WRITE; + +import java.io.Serializable; +import java.util.UUID; +import javax.servlet.http.HttpServletRequest; + import org.apache.commons.lang3.StringUtils; import org.dspace.app.rest.model.ResearcherProfileRest; import org.dspace.app.rest.utils.ContextUtil; @@ -19,12 +27,6 @@ import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; -import javax.servlet.http.HttpServletRequest; -import java.io.Serializable; -import java.util.UUID; - -import static org.dspace.app.rest.security.DSpaceRestPermission.*; - /** * * An authenticated user is allowed to view, update or delete his or her own diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResearcherProfileRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResearcherProfileRestRepositoryIT.java index b316ea3acdd6..3d03a0991262 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResearcherProfileRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResearcherProfileRestRepositoryIT.java @@ -7,56 +7,49 @@ */ package org.dspace.app.rest; +import static com.jayway.jsonpath.JsonPath.read; +import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; +import static java.util.Arrays.asList; +import static java.util.UUID.fromString; +import static org.dspace.app.rest.matcher.HalMatcher.matchLinks; +import static org.dspace.app.rest.matcher.MetadataMatcher.matchMetadata; +import static org.dspace.app.rest.matcher.MetadataMatcher.matchMetadataDoesNotExist; +import static org.dspace.app.rest.matcher.MetadataMatcher.matchMetadataNotEmpty; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertEquals; +import static org.springframework.data.rest.webmvc.RestMediaTypes.TEXT_URI_LIST; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.io.UnsupportedEncodingException; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; + import com.jayway.jsonpath.JsonPath; import org.dspace.app.rest.model.MetadataValueRest; import org.dspace.app.rest.model.patch.AddOperation; import org.dspace.app.rest.model.patch.Operation; -import org.dspace.app.rest.model.patch.RemoveOperation; import org.dspace.app.rest.model.patch.ReplaceOperation; +import org.dspace.app.rest.repository.ResearcherProfileRestRepository; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; -import org.dspace.authorize.AuthorizeException; -import org.dspace.builder.*; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.EPersonBuilder; +import org.dspace.builder.ItemBuilder; import org.dspace.content.Collection; import org.dspace.content.Item; -import org.dspace.content.MetadataValue; -import org.dspace.content.service.ItemService; import org.dspace.eperson.EPerson; -import org.dspace.eperson.Group; -import org.dspace.eperson.service.GroupService; import org.dspace.services.ConfigurationService; -import org.dspace.util.UUIDUtils; -import org.junit.After; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MvcResult; -import java.io.UnsupportedEncodingException; -import java.sql.SQLException; -import java.util.List; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Predicate; - -import static com.jayway.jsonpath.JsonPath.read; -import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; -import static java.util.Arrays.asList; -import static java.util.UUID.fromString; -import static org.dspace.app.matcher.LambdaMatcher.has; -import static org.dspace.app.matcher.MetadataValueMatcher.with; -import static org.dspace.app.rest.matcher.HalMatcher.matchLinks; -import static org.dspace.app.rest.matcher.MetadataMatcher.*; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; -import static org.springframework.data.rest.webmvc.RestMediaTypes.TEXT_URI_LIST; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - /** * Integration tests for {@link ResearcherProfileRestRepository}. * @@ -68,20 +61,12 @@ public class ResearcherProfileRestRepositoryIT extends AbstractControllerIntegra @Autowired private ConfigurationService configurationService; - @Autowired - private ItemService itemService; - - @Autowired - private GroupService groupService; - private EPerson user; private EPerson anotherUser; private Collection personCollection; - private Group administrators; - /** * Tests setup. */ @@ -112,8 +97,6 @@ public void setUp() throws Exception { .withTemplateItem() .build(); - administrators = groupService.findByName(context, Group.ADMIN); - configurationService.setProperty("researcher-profile.collection.uuid", personCollection.getID().toString()); configurationService.setProperty("claimable.entityType", "Person"); @@ -254,7 +237,7 @@ public void testCreateAndReturn() throws Exception { getClient(authToken).perform(post("/api/eperson/profiles/") .contentType(MediaType.APPLICATION_JSON_VALUE)) .andExpect(status().isCreated()) - .andExpect(jsonPath("$.id", is(id.toString()))) + .andExpect(jsonPath("$.id", is(id))) .andExpect(jsonPath("$.visible", is(false))) .andExpect(jsonPath("$.type", is("profile"))) .andExpect(jsonPath("$", matchLinks("http://localhost/api/eperson/profiles/" + id, "item", "eperson"))); @@ -265,7 +248,7 @@ public void testCreateAndReturn() throws Exception { getClient(authToken).perform(get("/api/eperson/profiles/{id}/item", id)) .andExpect(status().isOk()) .andExpect(jsonPath("$.type", is("item"))) - .andExpect(jsonPath("$.metadata", matchMetadata("dspace.object.owner", name, id.toString(), 0))) + .andExpect(jsonPath("$.metadata", matchMetadata("dspace.object.owner", name, id, 0))) .andExpect(jsonPath("$.metadata", matchMetadata("dspace.entity.type", "Person", 0))); getClient(authToken).perform(get("/api/eperson/profiles/{id}/eperson", id)) @@ -294,7 +277,7 @@ public void testCreateAndReturnWithAdmin() throws Exception { .param("eperson", id) .contentType(MediaType.APPLICATION_JSON_VALUE)) .andExpect(status().isCreated()) - .andExpect(jsonPath("$.id", is(id.toString()))) + .andExpect(jsonPath("$.id", is(id))) .andExpect(jsonPath("$.visible", is(false))) .andExpect(jsonPath("$.type", is("profile"))) .andExpect(jsonPath("$", matchLinks("http://localhost/api/eperson/profiles/" + id, "item", "eperson"))); @@ -305,7 +288,7 @@ public void testCreateAndReturnWithAdmin() throws Exception { getClient(authToken).perform(get("/api/eperson/profiles/{id}/item", id)) .andExpect(status().isOk()) .andExpect(jsonPath("$.type", is("item"))) - .andExpect(jsonPath("$.metadata", matchMetadata("dspace.object.owner", name, id.toString(), 0))) + .andExpect(jsonPath("$.metadata", matchMetadata("dspace.object.owner", name, id, 0))) .andExpect(jsonPath("$.metadata", matchMetadata("dspace.entity.type", "Person", 0))); getClient(authToken).perform(get("/api/eperson/profiles/{id}/eperson", id)) @@ -317,7 +300,7 @@ public void testCreateAndReturnWithAdmin() throws Exception { getClient(authToken).perform(get("/api/eperson/profiles/{id}", id)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.id", is(id.toString()))) + .andExpect(jsonPath("$.id", is(id))) .andExpect(jsonPath("$.visible", is(false))) .andExpect(jsonPath("$.type", is("profile"))) .andExpect(jsonPath("$", matchLinks("http://localhost/api/eperson/profiles/" + id, "item", "eperson"))); @@ -356,14 +339,14 @@ public void testCreateAndReturnWithProfileAlreadyAssociated() throws Exception { getClient(authToken).perform(post("/api/eperson/profiles/") .contentType(MediaType.APPLICATION_JSON_VALUE)) .andExpect(status().isCreated()) - .andExpect(jsonPath("$.id", is(id.toString()))) + .andExpect(jsonPath("$.id", is(id))) .andExpect(jsonPath("$.visible", is(false))) .andExpect(jsonPath("$.type", is("profile"))); getClient(authToken).perform(post("/api/eperson/profiles/") .contentType(MediaType.APPLICATION_JSON_VALUE)) .andExpect(status().isConflict()) - .andExpect(jsonPath("$.id", is(id.toString()))) + .andExpect(jsonPath("$.id", is(id))) .andExpect(jsonPath("$.visible", is(false))) .andExpect(jsonPath("$.type", is("profile"))); @@ -399,7 +382,7 @@ public void testDelete() throws Exception { String id = user.getID().toString(); String authToken = getAuthToken(user.getEmail(), password); - AtomicReference itemIdRef = new AtomicReference(); + AtomicReference itemIdRef = new AtomicReference<>(); getClient(authToken).perform(post("/api/eperson/profiles/") .contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -437,7 +420,7 @@ public void testHardDelete() throws Exception { String id = user.getID().toString(); String authToken = getAuthToken(user.getEmail(), password); - AtomicReference itemIdRef = new AtomicReference(); + AtomicReference itemIdRef = new AtomicReference<>(); getClient(authToken).perform(post("/api/eperson/profiles/") .contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -710,6 +693,28 @@ public void testPatchToChangeVisibilityOfProfileCreatedByAnAdmin() throws Except .andExpect(jsonPath("$.visible", is(true))); } + @Test + public void testPatchToChangeVisibleAttributeOfNotExistProfile() throws Exception { + + String id = user.getID().toString(); + String authToken = getAuthToken(user.getEmail(), password); + + getClient(authToken).perform(post("/api/eperson/profiles/") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.visible", is(false))); + + getClient(authToken).perform(delete("/api/eperson/profiles/{id}", id)) + .andExpect(status().isNoContent()); + + List operations = asList(new ReplaceOperation("/visible", true)); + + getClient(authToken).perform(patch("/api/eperson/profiles/{id}", id) + .content(getPatchContent(operations)) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isNotFound()); + } + /** * Verify that after an user login an automatic claim between the logged eperson * and possible profiles without eperson is done. @@ -866,11 +871,11 @@ public void researcherProfileClaim() throws Exception { context.turnOffAuthorisationSystem(); final Item person = ItemBuilder.createItem(context, personCollection) - .withTitle("dc.title") + .withTitle("Test User 1") .build(); final Item otherPerson = ItemBuilder.createItem(context, personCollection) - .withTitle("dc.title") + .withTitle("Test User 2") .build(); context.restoreAuthSystemState(); @@ -883,8 +888,8 @@ public void researcherProfileClaim() throws Exception { .andExpect(status().isCreated()) .andExpect(jsonPath("$.id", is(id))) .andExpect(jsonPath("$.type", is("profile"))) - .andExpect(jsonPath("$", - matchLinks("http://localhost/api/eperson/profiles/" + user.getID(), "item", "eperson"))); + .andExpect(jsonPath("$", matchLinks("http://localhost/api/eperson/profiles/" + user.getID(), + "item", "eperson"))); getClient(authToken).perform(get("/api/eperson/profiles/{id}", id)) .andExpect(status().isOk()); @@ -929,18 +934,12 @@ public void researcherProfileClaim() throws Exception { } @Test - public void claimForNotAllowedEntityType() throws Exception { - String id = user.getID().toString(); - String name = user.getName(); + public void testNotAdminUserClaimProfileOfAnotherUser() throws Exception { context.turnOffAuthorisationSystem(); - final Collection publications = CollectionBuilder.createCollection(context, parentCommunity) - .withEntityType("Publication") - .build(); - - final Item publication = ItemBuilder.createItem(context, publications) - .withTitle("title") + final Item person = ItemBuilder.createItem(context, personCollection) + .withTitle("Test User 1") .build(); context.restoreAuthSystemState(); @@ -948,70 +947,95 @@ public void claimForNotAllowedEntityType() throws Exception { String authToken = getAuthToken(user.getEmail(), password); getClient(authToken).perform(post("/api/eperson/profiles/") + .param("eperson" , anotherUser.getID().toString()) .contentType(TEXT_URI_LIST) - .content("http://localhost:8080/server/api/core/items/" + publication.getID().toString())) - .andExpect(status().isBadRequest()); + .content("http://localhost:8080/server/api/core/items/" + person.getID().toString())) + .andExpect(status().isForbidden()); } @Test - public void testCloneFromExternalSourceRecordNotFound() throws Exception { + public void testAdminUserClaimProfileOfNotExistingPersonId() throws Exception { - String authToken = getAuthToken(user.getEmail(), password); + String id = "bef23ba3-9aeb-4f7b-b153-77b0f1fc3612"; - getClient(authToken) - .perform(post("/api/eperson/profiles/").contentType(TEXT_URI_LIST) - .content("http://localhost:8080/server/api/integration/externalsources/orcid/entryValues/FAKE")) - .andExpect(status().isBadRequest()); + context.turnOffAuthorisationSystem(); + + final Item person = ItemBuilder.createItem(context, personCollection) + .withTitle("Test User 1") + .build(); + + context.restoreAuthSystemState(); + + String authToken = getAuthToken(admin.getEmail(), password); + + getClient(authToken).perform(post("/api/eperson/profiles/") + .param("eperson" , id) + .contentType(TEXT_URI_LIST) + .content("http://localhost:8080/server/api/core/items/" + person.getID().toString())) + .andExpect(status().isUnprocessableEntity()); } @Test - public void testCloneFromExternalSourceMultipleUri() throws Exception { + public void testAdminUserClaimProfileOfWrongPersonId() throws Exception { - String authToken = getAuthToken(user.getEmail(), password); + String id = "invalid_id"; - getClient(authToken) - .perform(post("/api/eperson/profiles/").contentType(TEXT_URI_LIST) - .content("http://localhost:8080/server/api/integration/externalsources/orcid/entryValues/id \n " - + "http://localhost:8080/server/api/integration/externalsources/dspace/entryValues/id")) - .andExpect(status().isBadRequest()); + context.turnOffAuthorisationSystem(); + + final Item person = ItemBuilder.createItem(context, personCollection) + .withTitle("Test User 1") + .build(); + + context.restoreAuthSystemState(); + String authToken = getAuthToken(admin.getEmail(), password); + + getClient(authToken).perform(post("/api/eperson/profiles/") + .param("eperson" , id) + .contentType(TEXT_URI_LIST) + .content("http://localhost:8080/server/api/core/items/" + person.getID().toString())) + .andExpect(status().isBadRequest()); } @Test - public void testCloneFromExternalProfileAlreadyAssociated() throws Exception { + public void claimForNotAllowedEntityType() throws Exception { + context.turnOffAuthorisationSystem(); - String id = user.getID().toString(); - String authToken = getAuthToken(user.getEmail(), password); + final Collection publications = CollectionBuilder.createCollection(context, parentCommunity) + .withEntityType("Publication") + .build(); - getClient(authToken).perform(post("/api/eperson/profiles/").contentType(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(status().isCreated()).andExpect(jsonPath("$.id", is(id.toString()))) - .andExpect(jsonPath("$.visible", is(false))).andExpect(jsonPath("$.type", is("profile"))); + final Item publication = ItemBuilder.createItem(context, publications) + .withTitle("title") + .build(); - getClient(authToken) - .perform(post("/api/eperson/profiles/").contentType(TEXT_URI_LIST) - .content("http://localhost:8080/server/api/integration/externalsources/orcid/entryValues/id")) - .andExpect(status().isConflict()); + context.restoreAuthSystemState(); + + String authToken = getAuthToken(user.getEmail(), password); + + getClient(authToken).perform(post("/api/eperson/profiles/") + .contentType(TEXT_URI_LIST) + .content("http://localhost:8080/server/api/core/items/" + publication.getID().toString())) + .andExpect(status().isBadRequest()); } @Test - public void testCloneFromExternalCollectionNotSet() throws Exception { + public void testCloneFromExternalProfileAlreadyAssociated() throws Exception { - configurationService.setProperty("researcher-profile.collection.uuid", "not-existing"); String id = user.getID().toString(); String authToken = getAuthToken(user.getEmail(), password); getClient(authToken).perform(post("/api/eperson/profiles/").contentType(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(status().isCreated()).andExpect(jsonPath("$.id", is(id.toString()))) + .andExpect(status().isCreated()).andExpect(jsonPath("$.id", is(id))) .andExpect(jsonPath("$.visible", is(false))).andExpect(jsonPath("$.type", is("profile"))); getClient(authToken) .perform(post("/api/eperson/profiles/").contentType(TEXT_URI_LIST) - .content("http://localhost:8080/server/api/integration/externalsources/orcid/entryValues/id \n " - + "http://localhost:8080/server/api/integration/externalsources/dspace/entryValues/id")) - .andExpect(status().isBadRequest()); + .content("http://localhost:8080/server/api/core/items/" + id)) + .andExpect(status().isConflict()); } - private String getItemIdByProfileId(String token, String id) throws SQLException, Exception { + private String getItemIdByProfileId(String token, String id) throws Exception { MvcResult result = getClient(token).perform(get("/api/eperson/profiles/{id}/item", id)) .andExpect(status().isOk()) .andReturn(); From f863866183f58148508dbdb89c5818bd914dbf3c Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Thu, 31 Mar 2022 13:20:28 -0500 Subject: [PATCH 0092/1846] Update to Spring v5.2.20. Requires also explicitly specifying version of spring-context-support to avoid dependency convergence issues. --- pom.xml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 1f12c0c9f281..d66e81c9328a 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ 11 - 5.2.5.RELEASE + 5.2.20.RELEASE 2.2.6.RELEASE 5.2.2.RELEASE 5.4.10.Final @@ -1218,6 +1218,12 @@ ${spring.version} + + org.springframework + spring-context-support + ${spring.version} + + org.springframework spring-test From fe9d22a4f6cc43de97fbcec060a09e484b90012b Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 1 Apr 2022 09:07:33 -0500 Subject: [PATCH 0093/1846] Change to snapshot version --- dspace-api/pom.xml | 2 +- dspace-iiif/pom.xml | 2 +- dspace-oai/pom.xml | 2 +- dspace-rdf/pom.xml | 2 +- dspace-rest/pom.xml | 4 ++-- dspace-server-webapp/pom.xml | 2 +- dspace-services/pom.xml | 2 +- dspace-sword/pom.xml | 2 +- dspace-swordv2/pom.xml | 2 +- dspace/modules/additions/pom.xml | 2 +- dspace/modules/pom.xml | 2 +- dspace/modules/rest/pom.xml | 2 +- dspace/modules/server/pom.xml | 2 +- dspace/pom.xml | 2 +- pom.xml | 30 +++++++++++++++--------------- 15 files changed, 30 insertions(+), 30 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index f000f2e1babf..8d6b7333087f 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -12,7 +12,7 @@ org.dspace dspace-parent - 7.2 + 7.2.1-SNAPSHOT .. diff --git a/dspace-iiif/pom.xml b/dspace-iiif/pom.xml index ce1dc8a3263f..244cbae25601 100644 --- a/dspace-iiif/pom.xml +++ b/dspace-iiif/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 7.2 + 7.2.1-SNAPSHOT .. diff --git a/dspace-oai/pom.xml b/dspace-oai/pom.xml index f4acb0d29752..c29ae237705d 100644 --- a/dspace-oai/pom.xml +++ b/dspace-oai/pom.xml @@ -8,7 +8,7 @@ dspace-parent org.dspace - 7.2 + 7.2.1-SNAPSHOT .. diff --git a/dspace-rdf/pom.xml b/dspace-rdf/pom.xml index 8a25dece7e8e..635fff3de7ab 100644 --- a/dspace-rdf/pom.xml +++ b/dspace-rdf/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 7.2 + 7.2.1-SNAPSHOT .. diff --git a/dspace-rest/pom.xml b/dspace-rest/pom.xml index c60383384fe2..15550950a159 100644 --- a/dspace-rest/pom.xml +++ b/dspace-rest/pom.xml @@ -3,7 +3,7 @@ org.dspace dspace-rest war - 7.2 + 7.2.1-SNAPSHOT DSpace (Deprecated) REST Webapp DSpace RESTful Web Services API. NOTE: this REST API is DEPRECATED. Please consider using the REST API in the dspace-server-webapp instead! @@ -12,7 +12,7 @@ org.dspace dspace-parent - 7.2 + 7.2.1-SNAPSHOT .. diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 20395013ceee..1c56bdc1b6b6 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 7.2 + 7.2.1-SNAPSHOT .. diff --git a/dspace-services/pom.xml b/dspace-services/pom.xml index 7f08f304d772..74868fd757d6 100644 --- a/dspace-services/pom.xml +++ b/dspace-services/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 7.2 + 7.2.1-SNAPSHOT diff --git a/dspace-sword/pom.xml b/dspace-sword/pom.xml index a55e0457a970..80f5bac9c32b 100644 --- a/dspace-sword/pom.xml +++ b/dspace-sword/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 7.2 + 7.2.1-SNAPSHOT .. diff --git a/dspace-swordv2/pom.xml b/dspace-swordv2/pom.xml index a9a58cff06d1..2c5da3ff452a 100644 --- a/dspace-swordv2/pom.xml +++ b/dspace-swordv2/pom.xml @@ -13,7 +13,7 @@ org.dspace dspace-parent - 7.2 + 7.2.1-SNAPSHOT .. diff --git a/dspace/modules/additions/pom.xml b/dspace/modules/additions/pom.xml index 88ddce8b3219..d0471ebfa7cf 100644 --- a/dspace/modules/additions/pom.xml +++ b/dspace/modules/additions/pom.xml @@ -17,7 +17,7 @@ org.dspace modules - 7.2 + 7.2.1-SNAPSHOT .. diff --git a/dspace/modules/pom.xml b/dspace/modules/pom.xml index 7deb4bd0d097..f5c3813f303e 100644 --- a/dspace/modules/pom.xml +++ b/dspace/modules/pom.xml @@ -11,7 +11,7 @@ org.dspace dspace-parent - 7.2 + 7.2.1-SNAPSHOT ../../pom.xml diff --git a/dspace/modules/rest/pom.xml b/dspace/modules/rest/pom.xml index 6f4097f3d715..f604809754f8 100644 --- a/dspace/modules/rest/pom.xml +++ b/dspace/modules/rest/pom.xml @@ -13,7 +13,7 @@ org.dspace modules - 7.2 + 7.2.1-SNAPSHOT .. diff --git a/dspace/modules/server/pom.xml b/dspace/modules/server/pom.xml index fadd08b01455..6f9924675024 100644 --- a/dspace/modules/server/pom.xml +++ b/dspace/modules/server/pom.xml @@ -13,7 +13,7 @@ just adding new jar in the classloader modules org.dspace - 7.2 + 7.2.1-SNAPSHOT .. diff --git a/dspace/pom.xml b/dspace/pom.xml index 54819ee041e8..23afe092c0e9 100644 --- a/dspace/pom.xml +++ b/dspace/pom.xml @@ -16,7 +16,7 @@ org.dspace dspace-parent - 7.2 + 7.2.1-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index d66e81c9328a..681606eaf0fd 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.dspace dspace-parent pom - 7.2 + 7.2.1-SNAPSHOT DSpace Parent Project DSpace open source software is a turnkey institutional repository application. @@ -868,14 +868,14 @@ org.dspace dspace-rest - 7.2 + 7.2.1-SNAPSHOT jar classes org.dspace dspace-rest - 7.2 + 7.2.1-SNAPSHOT war @@ -1026,69 +1026,69 @@ org.dspace dspace-api - 7.2 + 7.2.1-SNAPSHOT org.dspace dspace-api test-jar - 7.2 + 7.2.1-SNAPSHOT test org.dspace.modules additions - 7.2 + 7.2.1-SNAPSHOT org.dspace dspace-sword - 7.2 + 7.2.1-SNAPSHOT org.dspace dspace-swordv2 - 7.2 + 7.2.1-SNAPSHOT org.dspace dspace-oai - 7.2 + 7.2.1-SNAPSHOT org.dspace dspace-services - 7.2 + 7.2.1-SNAPSHOT org.dspace dspace-server-webapp test-jar - 7.2 + 7.2.1-SNAPSHOT test org.dspace dspace-rdf - 7.2 + 7.2.1-SNAPSHOT org.dspace dspace-iiif - 7.2 + 7.2.1-SNAPSHOT org.dspace dspace-server-webapp - 7.2 + 7.2.1-SNAPSHOT jar classes org.dspace dspace-server-webapp - 7.2 + 7.2.1-SNAPSHOT war From e8b92088f128bcb7b091cf59e048693ec00fb134 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 1 Apr 2022 09:21:54 -0500 Subject: [PATCH 0094/1846] [maven-release-plugin] prepare release dspace-7.2.1 --- dspace-api/pom.xml | 2 +- dspace-iiif/pom.xml | 2 +- dspace-oai/pom.xml | 2 +- dspace-rdf/pom.xml | 2 +- dspace-rest/pom.xml | 4 ++-- dspace-server-webapp/pom.xml | 2 +- dspace-services/pom.xml | 2 +- dspace-sword/pom.xml | 2 +- dspace-swordv2/pom.xml | 2 +- dspace/modules/additions/pom.xml | 2 +- dspace/modules/pom.xml | 2 +- dspace/modules/rest/pom.xml | 2 +- dspace/modules/server/pom.xml | 2 +- dspace/pom.xml | 2 +- pom.xml | 32 ++++++++++++++++---------------- 15 files changed, 31 insertions(+), 31 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 8d6b7333087f..f0f73d9ca306 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -12,7 +12,7 @@ org.dspace dspace-parent - 7.2.1-SNAPSHOT + 7.2.1 .. diff --git a/dspace-iiif/pom.xml b/dspace-iiif/pom.xml index 244cbae25601..303be411e4ca 100644 --- a/dspace-iiif/pom.xml +++ b/dspace-iiif/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 7.2.1-SNAPSHOT + 7.2.1 .. diff --git a/dspace-oai/pom.xml b/dspace-oai/pom.xml index c29ae237705d..1a6b87b051c1 100644 --- a/dspace-oai/pom.xml +++ b/dspace-oai/pom.xml @@ -8,7 +8,7 @@ dspace-parent org.dspace - 7.2.1-SNAPSHOT + 7.2.1 .. diff --git a/dspace-rdf/pom.xml b/dspace-rdf/pom.xml index 635fff3de7ab..b68f727ea781 100644 --- a/dspace-rdf/pom.xml +++ b/dspace-rdf/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 7.2.1-SNAPSHOT + 7.2.1 .. diff --git a/dspace-rest/pom.xml b/dspace-rest/pom.xml index 15550950a159..e4f46a0b9581 100644 --- a/dspace-rest/pom.xml +++ b/dspace-rest/pom.xml @@ -3,7 +3,7 @@ org.dspace dspace-rest war - 7.2.1-SNAPSHOT + 7.2.1 DSpace (Deprecated) REST Webapp DSpace RESTful Web Services API. NOTE: this REST API is DEPRECATED. Please consider using the REST API in the dspace-server-webapp instead! @@ -12,7 +12,7 @@ org.dspace dspace-parent - 7.2.1-SNAPSHOT + 7.2.1 .. diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 1c56bdc1b6b6..8ac636239c7e 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 7.2.1-SNAPSHOT + 7.2.1 .. diff --git a/dspace-services/pom.xml b/dspace-services/pom.xml index 74868fd757d6..75d2f3855c61 100644 --- a/dspace-services/pom.xml +++ b/dspace-services/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 7.2.1-SNAPSHOT + 7.2.1 diff --git a/dspace-sword/pom.xml b/dspace-sword/pom.xml index 80f5bac9c32b..25eb7c8ccb15 100644 --- a/dspace-sword/pom.xml +++ b/dspace-sword/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 7.2.1-SNAPSHOT + 7.2.1 .. diff --git a/dspace-swordv2/pom.xml b/dspace-swordv2/pom.xml index 2c5da3ff452a..5a507edd60d4 100644 --- a/dspace-swordv2/pom.xml +++ b/dspace-swordv2/pom.xml @@ -13,7 +13,7 @@ org.dspace dspace-parent - 7.2.1-SNAPSHOT + 7.2.1 .. diff --git a/dspace/modules/additions/pom.xml b/dspace/modules/additions/pom.xml index d0471ebfa7cf..5b494781affb 100644 --- a/dspace/modules/additions/pom.xml +++ b/dspace/modules/additions/pom.xml @@ -17,7 +17,7 @@ org.dspace modules - 7.2.1-SNAPSHOT + 7.2.1 .. diff --git a/dspace/modules/pom.xml b/dspace/modules/pom.xml index f5c3813f303e..57daef684715 100644 --- a/dspace/modules/pom.xml +++ b/dspace/modules/pom.xml @@ -11,7 +11,7 @@ org.dspace dspace-parent - 7.2.1-SNAPSHOT + 7.2.1 ../../pom.xml diff --git a/dspace/modules/rest/pom.xml b/dspace/modules/rest/pom.xml index f604809754f8..4d97118d362d 100644 --- a/dspace/modules/rest/pom.xml +++ b/dspace/modules/rest/pom.xml @@ -13,7 +13,7 @@ org.dspace modules - 7.2.1-SNAPSHOT + 7.2.1 .. diff --git a/dspace/modules/server/pom.xml b/dspace/modules/server/pom.xml index 6f9924675024..b01cef062e78 100644 --- a/dspace/modules/server/pom.xml +++ b/dspace/modules/server/pom.xml @@ -13,7 +13,7 @@ just adding new jar in the classloader modules org.dspace - 7.2.1-SNAPSHOT + 7.2.1 .. diff --git a/dspace/pom.xml b/dspace/pom.xml index 23afe092c0e9..a1bdbfca2e28 100644 --- a/dspace/pom.xml +++ b/dspace/pom.xml @@ -16,7 +16,7 @@ org.dspace dspace-parent - 7.2.1-SNAPSHOT + 7.2.1 ../pom.xml diff --git a/pom.xml b/pom.xml index 681606eaf0fd..e92dbdcc8bf2 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.dspace dspace-parent pom - 7.2.1-SNAPSHOT + 7.2.1 DSpace Parent Project DSpace open source software is a turnkey institutional repository application. @@ -868,14 +868,14 @@ org.dspace dspace-rest - 7.2.1-SNAPSHOT + 7.2.1 jar classes org.dspace dspace-rest - 7.2.1-SNAPSHOT + 7.2.1 war @@ -1026,69 +1026,69 @@ org.dspace dspace-api - 7.2.1-SNAPSHOT + 7.2.1 org.dspace dspace-api test-jar - 7.2.1-SNAPSHOT + 7.2.1 test org.dspace.modules additions - 7.2.1-SNAPSHOT + 7.2.1 org.dspace dspace-sword - 7.2.1-SNAPSHOT + 7.2.1 org.dspace dspace-swordv2 - 7.2.1-SNAPSHOT + 7.2.1 org.dspace dspace-oai - 7.2.1-SNAPSHOT + 7.2.1 org.dspace dspace-services - 7.2.1-SNAPSHOT + 7.2.1 org.dspace dspace-server-webapp test-jar - 7.2.1-SNAPSHOT + 7.2.1 test org.dspace dspace-rdf - 7.2.1-SNAPSHOT + 7.2.1 org.dspace dspace-iiif - 7.2.1-SNAPSHOT + 7.2.1 org.dspace dspace-server-webapp - 7.2.1-SNAPSHOT + 7.2.1 jar classes org.dspace dspace-server-webapp - 7.2.1-SNAPSHOT + 7.2.1 war @@ -1883,7 +1883,7 @@ scm:git:git@github.com:DSpace/DSpace.git scm:git:git@github.com:DSpace/DSpace.git git@github.com:DSpace/DSpace.git - dspace-7.2 + dspace-7.2.1 From 8ee4d9f9ab1223c7be5d913d99e8ef33a487c70b Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 1 Apr 2022 09:21:58 -0500 Subject: [PATCH 0095/1846] [maven-release-plugin] prepare for next development iteration --- dspace-api/pom.xml | 2 +- dspace-iiif/pom.xml | 2 +- dspace-oai/pom.xml | 2 +- dspace-rdf/pom.xml | 2 +- dspace-rest/pom.xml | 4 ++-- dspace-server-webapp/pom.xml | 2 +- dspace-services/pom.xml | 2 +- dspace-sword/pom.xml | 2 +- dspace-swordv2/pom.xml | 2 +- dspace/modules/additions/pom.xml | 2 +- dspace/modules/pom.xml | 2 +- dspace/modules/rest/pom.xml | 2 +- dspace/modules/server/pom.xml | 2 +- dspace/pom.xml | 2 +- pom.xml | 32 ++++++++++++++++---------------- 15 files changed, 31 insertions(+), 31 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index f0f73d9ca306..c649d06002e0 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -12,7 +12,7 @@ org.dspace dspace-parent - 7.2.1 + 7.2.2-SNAPSHOT .. diff --git a/dspace-iiif/pom.xml b/dspace-iiif/pom.xml index 303be411e4ca..19d4fbe1ab14 100644 --- a/dspace-iiif/pom.xml +++ b/dspace-iiif/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 7.2.1 + 7.2.2-SNAPSHOT .. diff --git a/dspace-oai/pom.xml b/dspace-oai/pom.xml index 1a6b87b051c1..11faf5f9211f 100644 --- a/dspace-oai/pom.xml +++ b/dspace-oai/pom.xml @@ -8,7 +8,7 @@ dspace-parent org.dspace - 7.2.1 + 7.2.2-SNAPSHOT .. diff --git a/dspace-rdf/pom.xml b/dspace-rdf/pom.xml index b68f727ea781..52cc15742ef9 100644 --- a/dspace-rdf/pom.xml +++ b/dspace-rdf/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 7.2.1 + 7.2.2-SNAPSHOT .. diff --git a/dspace-rest/pom.xml b/dspace-rest/pom.xml index e4f46a0b9581..21f5692026ca 100644 --- a/dspace-rest/pom.xml +++ b/dspace-rest/pom.xml @@ -3,7 +3,7 @@ org.dspace dspace-rest war - 7.2.1 + 7.2.2-SNAPSHOT DSpace (Deprecated) REST Webapp DSpace RESTful Web Services API. NOTE: this REST API is DEPRECATED. Please consider using the REST API in the dspace-server-webapp instead! @@ -12,7 +12,7 @@ org.dspace dspace-parent - 7.2.1 + 7.2.2-SNAPSHOT .. diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 8ac636239c7e..a76d8daa966a 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 7.2.1 + 7.2.2-SNAPSHOT .. diff --git a/dspace-services/pom.xml b/dspace-services/pom.xml index 75d2f3855c61..c46e79c6cd64 100644 --- a/dspace-services/pom.xml +++ b/dspace-services/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 7.2.1 + 7.2.2-SNAPSHOT diff --git a/dspace-sword/pom.xml b/dspace-sword/pom.xml index 25eb7c8ccb15..5491d85ee18c 100644 --- a/dspace-sword/pom.xml +++ b/dspace-sword/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 7.2.1 + 7.2.2-SNAPSHOT .. diff --git a/dspace-swordv2/pom.xml b/dspace-swordv2/pom.xml index 5a507edd60d4..cc8dd3b24bbc 100644 --- a/dspace-swordv2/pom.xml +++ b/dspace-swordv2/pom.xml @@ -13,7 +13,7 @@ org.dspace dspace-parent - 7.2.1 + 7.2.2-SNAPSHOT .. diff --git a/dspace/modules/additions/pom.xml b/dspace/modules/additions/pom.xml index 5b494781affb..68b9e940c63b 100644 --- a/dspace/modules/additions/pom.xml +++ b/dspace/modules/additions/pom.xml @@ -17,7 +17,7 @@ org.dspace modules - 7.2.1 + 7.2.2-SNAPSHOT .. diff --git a/dspace/modules/pom.xml b/dspace/modules/pom.xml index 57daef684715..0580e234abfd 100644 --- a/dspace/modules/pom.xml +++ b/dspace/modules/pom.xml @@ -11,7 +11,7 @@ org.dspace dspace-parent - 7.2.1 + 7.2.2-SNAPSHOT ../../pom.xml diff --git a/dspace/modules/rest/pom.xml b/dspace/modules/rest/pom.xml index 4d97118d362d..d7c92507308b 100644 --- a/dspace/modules/rest/pom.xml +++ b/dspace/modules/rest/pom.xml @@ -13,7 +13,7 @@ org.dspace modules - 7.2.1 + 7.2.2-SNAPSHOT .. diff --git a/dspace/modules/server/pom.xml b/dspace/modules/server/pom.xml index b01cef062e78..58f832ab3532 100644 --- a/dspace/modules/server/pom.xml +++ b/dspace/modules/server/pom.xml @@ -13,7 +13,7 @@ just adding new jar in the classloader modules org.dspace - 7.2.1 + 7.2.2-SNAPSHOT .. diff --git a/dspace/pom.xml b/dspace/pom.xml index a1bdbfca2e28..4e5c6d4b61de 100644 --- a/dspace/pom.xml +++ b/dspace/pom.xml @@ -16,7 +16,7 @@ org.dspace dspace-parent - 7.2.1 + 7.2.2-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index e92dbdcc8bf2..8dc63ca4234b 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.dspace dspace-parent pom - 7.2.1 + 7.2.2-SNAPSHOT DSpace Parent Project DSpace open source software is a turnkey institutional repository application. @@ -868,14 +868,14 @@ org.dspace dspace-rest - 7.2.1 + 7.2.2-SNAPSHOT jar classes org.dspace dspace-rest - 7.2.1 + 7.2.2-SNAPSHOT war @@ -1026,69 +1026,69 @@ org.dspace dspace-api - 7.2.1 + 7.2.2-SNAPSHOT org.dspace dspace-api test-jar - 7.2.1 + 7.2.2-SNAPSHOT test org.dspace.modules additions - 7.2.1 + 7.2.2-SNAPSHOT org.dspace dspace-sword - 7.2.1 + 7.2.2-SNAPSHOT org.dspace dspace-swordv2 - 7.2.1 + 7.2.2-SNAPSHOT org.dspace dspace-oai - 7.2.1 + 7.2.2-SNAPSHOT org.dspace dspace-services - 7.2.1 + 7.2.2-SNAPSHOT org.dspace dspace-server-webapp test-jar - 7.2.1 + 7.2.2-SNAPSHOT test org.dspace dspace-rdf - 7.2.1 + 7.2.2-SNAPSHOT org.dspace dspace-iiif - 7.2.1 + 7.2.2-SNAPSHOT org.dspace dspace-server-webapp - 7.2.1 + 7.2.2-SNAPSHOT jar classes org.dspace dspace-server-webapp - 7.2.1 + 7.2.2-SNAPSHOT war @@ -1883,7 +1883,7 @@ scm:git:git@github.com:DSpace/DSpace.git scm:git:git@github.com:DSpace/DSpace.git git@github.com:DSpace/DSpace.git - dspace-7.2.1 + dspace-7.2 From 1050d02a97cbe0ec83336f8bb1c8906f88aad62c Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Sat, 2 Apr 2022 13:11:11 +0200 Subject: [PATCH 0096/1846] refactored live import client and some utils class --- .../scopus/service/LiveImportClient.java | 3 +- .../scopus/service/LiveImportClientImpl.java | 56 ++++++++----- .../importer/external/service/DoiCheck.java | 52 ++++++++++++ .../config/spring/api/external-services.xml | 2 + .../AbstractLiveImportIntegrationTest.java | 80 +++++++++++++++++++ 5 files changed, 172 insertions(+), 21 deletions(-) create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/service/DoiCheck.java create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/AbstractLiveImportIntegrationTest.java diff --git a/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/LiveImportClient.java b/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/LiveImportClient.java index 50006fd486cd..bbc69c2a3d7e 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/LiveImportClient.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/LiveImportClient.java @@ -7,7 +7,6 @@ */ package org.dspace.importer.external.scopus.service; -import java.io.InputStream; import java.util.Map; /** @@ -17,6 +16,6 @@ */ public interface LiveImportClient { - public InputStream executeHttpGetRequest(int timeout, String URL, Map requestParams); + public String executeHttpGetRequest(int timeout, String URL, Map requestParams); } \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/LiveImportClientImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/LiveImportClientImpl.java index f11e2fc4f207..d5503831d3e8 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/LiveImportClientImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/LiveImportClientImpl.java @@ -9,20 +9,21 @@ import java.io.InputStream; import java.net.URISyntaxException; +import java.nio.charset.Charset; import java.util.Map; import java.util.Objects; +import java.util.Optional; +import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpHost; import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.config.RequestConfig.Builder; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.utils.URIBuilder; -import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; -import org.apache.http.impl.conn.DefaultProxyRoutePlanner; import org.apache.log4j.Logger; import org.dspace.services.ConfigurationService; import org.springframework.beans.factory.annotation.Autowired; @@ -36,34 +37,32 @@ public class LiveImportClientImpl implements LiveImportClient { private static final Logger log = Logger.getLogger(LiveImportClientImpl.class); + private CloseableHttpClient httpClient; + @Autowired private ConfigurationService configurationService; @Override - public InputStream executeHttpGetRequest(int timeout, String URL, Map requestParams) { + public String executeHttpGetRequest(int timeout, String URL, Map requestParams) { HttpGet method = null; - String proxyHost = configurationService.getProperty("http.proxy.host"); - String proxyPort = configurationService.getProperty("http.proxy.port"); - try { - HttpClientBuilder hcBuilder = HttpClients.custom(); + try (CloseableHttpClient httpClient = Optional.ofNullable(this.httpClient) + .orElseGet(HttpClients::createDefault)) { + Builder requestConfigBuilder = RequestConfig.custom(); requestConfigBuilder.setConnectionRequestTimeout(timeout); - - if (StringUtils.isNotBlank(proxyHost) && StringUtils.isNotBlank(proxyPort)) { - HttpHost proxy = new HttpHost(proxyHost, Integer.parseInt(proxyPort), "http"); - DefaultProxyRoutePlanner routePlanner = new DefaultProxyRoutePlanner(proxy); - hcBuilder.setRoutePlanner(routePlanner); - } + RequestConfig defaultRequestConfig = requestConfigBuilder.build(); method = new HttpGet(getSearchUrl(URL, requestParams)); - method.setConfig(requestConfigBuilder.build()); + method.setConfig(defaultRequestConfig); - HttpClient client = hcBuilder.build(); - HttpResponse httpResponse = client.execute(method); + configureProxy(method, defaultRequestConfig); + + HttpResponse httpResponse = httpClient.execute(method); if (isNotSuccessfull(httpResponse)) { throw new RuntimeException(); } - return httpResponse.getEntity().getContent(); + InputStream inputStream = httpResponse.getEntity().getContent(); + return IOUtils.toString(inputStream, Charset.defaultCharset()); } catch (Exception e1) { log.error(e1.getMessage(), e1); } finally { @@ -71,7 +70,18 @@ public InputStream executeHttpGetRequest(int timeout, String URL, Map requestParams) throws URISyntaxException { @@ -91,4 +101,12 @@ private int getStatusCode(HttpResponse response) { return response.getStatusLine().getStatusCode(); } + public CloseableHttpClient getHttpClient() { + return httpClient; + } + + public void setHttpClient(CloseableHttpClient httpClient) { + this.httpClient = httpClient; + } + } \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/service/DoiCheck.java b/dspace-api/src/main/java/org/dspace/importer/external/service/DoiCheck.java new file mode 100644 index 000000000000..1285d6ea32d1 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/service/DoiCheck.java @@ -0,0 +1,52 @@ +/** + * 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.importer.external.service; + +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +/** + * Utility class that provides methods to check if a given string is a DOI and exists on CrossRef services + * + * @author Corrado Lombardi (corrado.lombardi at 4science.it) + */ +public class DoiCheck { + + private static final List DOI_PREFIXES = Arrays.asList("http://dx.doi.org/", "https://dx.doi.org/"); + + private static final Pattern PATTERN = Pattern.compile("10.\\d{4,9}/[-._;()/:A-Z0-9]+" + + "|10.1002/[^\\s]+" + + "|10.\\d{4}/\\d+-\\d+X?(\\d+)" + + "\\d+<[\\d\\w]+:[\\d\\w]*>\\d+.\\d+.\\w+;\\d" + + "|10.1021/\\w\\w\\d++" + + "|10.1207/[\\w\\d]+\\&\\d+_\\d+", + Pattern.CASE_INSENSITIVE); + + + private DoiCheck() { + } + + public static boolean isDoi(final String value) { + + Matcher m = PATTERN.matcher(purgeDoiValue(value)); + return m.matches(); + } + + + public static String purgeDoiValue(final String query) { + String value = query.replaceAll(",", ""); + for (final String prefix : DOI_PREFIXES) { + value = value.replaceAll(prefix, ""); + } + return value.trim(); + } + +} diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/external-services.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/external-services.xml index ac163d35811d..2157556d4694 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/external-services.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/external-services.xml @@ -6,6 +6,8 @@ + + diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AbstractLiveImportIntegrationTest.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AbstractLiveImportIntegrationTest.java new file mode 100644 index 000000000000..ca3195b344fb --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AbstractLiveImportIntegrationTest.java @@ -0,0 +1,80 @@ +/** + * 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.rest; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.UnsupportedEncodingException; +import java.util.Collection; + +import org.apache.http.ProtocolVersion; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.entity.BasicHttpEntity; +import org.apache.tools.ant.filters.StringInputStream; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.importer.external.datamodel.ImportRecord; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; + +/** + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com) + */ +public class AbstractLiveImportIntegrationTest extends AbstractControllerIntegrationTest { + + protected boolean matchRecords(Collection recordsImported, Collection records2match) { + ImportRecord firstImported = recordsImported.iterator().next(); + ImportRecord secondImported = recordsImported.iterator().next(); + ImportRecord first2match = recordsImported.iterator().next(); + ImportRecord second2match = recordsImported.iterator().next(); + boolean checkFirstRecord = firstImported.getValueList().containsAll(first2match.getValueList()); + boolean checkSecondRecord = secondImported.getValueList().containsAll(second2match.getValueList()); + return checkFirstRecord && checkSecondRecord; + } + + protected MetadatumDTO createMetadatumDTO(String schema, String element, String qualifier, String value) { + MetadatumDTO metadatumDTO = new MetadatumDTO(); + metadatumDTO.setSchema(schema); + metadatumDTO.setElement(element); + metadatumDTO.setQualifier(qualifier); + metadatumDTO.setValue(value); + return metadatumDTO; + } + + protected CloseableHttpResponse mockResponse(String xmlExample, int statusCode, String reason) + throws UnsupportedEncodingException { + BasicHttpEntity basicHttpEntity = new BasicHttpEntity(); + basicHttpEntity.setChunked(true); + basicHttpEntity.setContent(new StringInputStream(xmlExample)); + + CloseableHttpResponse response = mock(CloseableHttpResponse.class); + when(response.getStatusLine()).thenReturn(statusLine(statusCode, reason)); + when(response.getEntity()).thenReturn(basicHttpEntity); + return response; + } + + protected StatusLine statusLine(int statusCode, String reason) { + return new StatusLine() { + @Override + public ProtocolVersion getProtocolVersion() { + return new ProtocolVersion("http", 1, 1); + } + + @Override + public int getStatusCode() { + return statusCode; + } + + @Override + public String getReasonPhrase() { + return reason; + } + }; + } + +} \ No newline at end of file From 5d0bf51d5a6c33c3ed39a64b4d5064a7c506de86 Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Sat, 2 Apr 2022 13:12:25 +0200 Subject: [PATCH 0097/1846] [CST-5303] added tests for CrossRef live import service --- .../assetstore/crossRef-test.json | 309 ++++++++++++++++++ ...CrossRefImportMetadataSourceServiceIT.java | 146 +++++++++ .../src/test/resources/test-config.properties | 2 + 3 files changed, 457 insertions(+) create mode 100644 dspace-api/src/test/data/dspaceFolder/assetstore/crossRef-test.json create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/CrossRefImportMetadataSourceServiceIT.java diff --git a/dspace-api/src/test/data/dspaceFolder/assetstore/crossRef-test.json b/dspace-api/src/test/data/dspaceFolder/assetstore/crossRef-test.json new file mode 100644 index 000000000000..69a9433868f9 --- /dev/null +++ b/dspace-api/src/test/data/dspaceFolder/assetstore/crossRef-test.json @@ -0,0 +1,309 @@ +{ + "status": "ok", + "message-type": "work-list", + "message-version": "1.0.0", + "message": { + "facets": {}, + "total-results": 10, + "items": [ + { + "indexed": { + "date-parts": [ + [ + 2021, + 12, + 22 + ] + ], + "date-time": "2021-12-22T10:58:16Z", + "timestamp": 1640170696598 + }, + "reference-count": 0, + "publisher": "Petro Mohyla Black Sea National University", + "issue": "2", + "content-domain": { + "domain": [], + "crossmark-restriction": false + }, + "short-container-title": [ + "Ukr. ž. med. bìol. sportu" + ], + "published-print": { + "date-parts": [ + [ + 2016, + 5, + 19 + ] + ] + }, + "DOI": "10.26693/jmbs01.02.184", + "type": "journal-article", + "created": { + "date-parts": [ + [ + 2017, + 9, + 7 + ] + ], + "date-time": "2017-09-07T13:30:46Z", + "timestamp": 1504791046000 + }, + "page": "184-187", + "source": "Crossref", + "is-referenced-by-count": 0, + "title": [ + "State of Awareness of Freshers’ Groups Chortkiv State Medical College of Prevention of Iodine Deficiency Diseases" + ], + "prefix": "10.26693", + "volume": "1", + "author": [ + { + "given": "L.V.", + "family": "Senyuk", + "sequence": "first", + "affiliation": [] + }, + { + "name": "Chortkiv State Medical College 7, Gogola St., Chortkiv, Ternopil region 48500, Ukraine", + "sequence": "first", + "affiliation": [] + } + ], + "member": "11225", + "published-online": { + "date-parts": [ + [ + 2016, + 5, + 19 + ] + ] + }, + "container-title": [ + "Ukraïnsʹkij žurnal medicini, bìologìï ta sportu" + ], + "original-title": [ + "СТАН ОБІЗНАНОСТІ СТУДЕНТІВ НОВОНАБРАНИХ ГРУП ЧОРТКІВСЬКОГО ДЕРЖАВНОГО МЕДИЧНОГО КОЛЕДЖУ З ПИТАНЬ ПРОФІЛАКТИКИ ЙОДОДЕФІЦИТНИХ ЗАХВОРЮВАНЬ" + ], + "deposited": { + "date-parts": [ + [ + 2017, + 9, + 8 + ] + ], + "date-time": "2017-09-08T10:14:53Z", + "timestamp": 1504865693000 + }, + "score": 22.728952, + "issued": { + "date-parts": [ + [ + 2016, + 5, + 19 + ] + ] + }, + "references-count": 0, + "journal-issue": { + "issue": "2", + "published-online": { + "date-parts": [ + [ + 2016, + 5, + 19 + ] + ] + }, + "published-print": { + "date-parts": [ + [ + 2016, + 5, + 19 + ] + ] + } + }, + "URL": "http://dx.doi.org/10.26693/jmbs01.02.184", + "ISSN": [ + "2415-3060" + ], + "issn-type": [ + { + "value": "2415-3060", + "type": "print" + } + ], + "published": { + "date-parts": [ + [ + 2016, + 5, + 19 + ] + ] + } + }, + { + "indexed": { + "date-parts": [ + [ + 2022, + 3, + 29 + ] + ], + "date-time": "2022-03-29T13:04:48Z", + "timestamp": 1648559088439 + }, + "reference-count": 0, + "publisher": "Petro Mohyla Black Sea National University", + "issue": "2", + "content-domain": { + "domain": [], + "crossmark-restriction": false + }, + "short-container-title": [ + "Ukr. ž. med. bìol. sportu" + ], + "published-print": { + "date-parts": [ + [ + 2016, + 5, + 19 + ] + ] + }, + "DOI": "10.26693/jmbs01.02.105", + "type": "journal-article", + "created": { + "date-parts": [ + [ + 2017, + 9, + 1 + ] + ], + "date-time": "2017-09-01T10:04:04Z", + "timestamp": 1504260244000 + }, + "page": "105-108", + "source": "Crossref", + "is-referenced-by-count": 0, + "title": [ + "Ischemic Heart Disease and Role of Nurse of Cardiology Department" + ], + "prefix": "10.26693", + "volume": "1", + "author": [ + { + "given": "K. І.", + "family": "Kozak", + "sequence": "first", + "affiliation": [] + }, + { + "name": "Chortkiv State Medical College 7, Gogola St., Chortkiv, Ternopil region 48500, Ukraine", + "sequence": "first", + "affiliation": [] + } + ], + "member": "11225", + "published-online": { + "date-parts": [ + [ + 2016, + 5, + 19 + ] + ] + }, + "container-title": [ + "Ukraïnsʹkij žurnal medicini, bìologìï ta sportu" + ], + "original-title": [ + "ІШЕМІЧНА ХВОРОБА СЕРЦЯ ТА РОЛЬ МЕДИЧНОЇ СЕСТРИ КАРДІОЛОГІЧНОГО ВІДДІЛЕННЯ" + ], + "deposited": { + "date-parts": [ + [ + 2017, + 9, + 2 + ] + ], + "date-time": "2017-09-02T12:36:15Z", + "timestamp": 1504355775000 + }, + "score": 18.263277, + "resource": { + "primary": { + "URL": "http://en.jmbs.com.ua/archive/1/2/105" + } + }, + "issued": { + "date-parts": [ + [ + 2016, + 5, + 19 + ] + ] + }, + "references-count": 0, + "journal-issue": { + "issue": "2", + "published-online": { + "date-parts": [ + [ + 2016, + 5, + 19 + ] + ] + }, + "published-print": { + "date-parts": [ + [ + 2016, + 5, + 19 + ] + ] + } + }, + "URL": "http://dx.doi.org/10.26693/jmbs01.02.105", + "ISSN": [ + "2415-3060" + ], + "issn-type": [ + { + "value": "2415-3060", + "type": "print" + } + ], + "published": { + "date-parts": [ + [ + 2016, + 5, + 19 + ] + ] + } + } + ], + "items-per-page": 2, + "query": { + "start-index": 0, + "search-terms": "chortkiv" + } + } +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CrossRefImportMetadataSourceServiceIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CrossRefImportMetadataSourceServiceIT.java new file mode 100644 index 000000000000..ef7ad6e86294 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CrossRefImportMetadataSourceServiceIT.java @@ -0,0 +1,146 @@ +/** + * 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.rest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +import java.io.FileInputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +import org.apache.commons.io.IOUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.impl.client.CloseableHttpClient; +import org.dspace.importer.external.crossref.CrossRefImportMetadataSourceServiceImpl; +import org.dspace.importer.external.datamodel.ImportRecord; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; +import org.dspace.importer.external.scopus.service.LiveImportClientImpl; +import org.junit.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Integration tests for {@link CrossRefImportMetadataSourceServiceImpl} + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com) + */ +public class CrossRefImportMetadataSourceServiceIT extends AbstractLiveImportIntegrationTest { + + @Autowired + private LiveImportClientImpl liveImportClientImpl; + + @Autowired + private CrossRefImportMetadataSourceServiceImpl crossRefServiceImpl; + + @Test + public void crossRefImportMetadataGetRecordsTest() throws Exception { + context.turnOffAuthorisationSystem(); + CloseableHttpClient originalHttpClient = liveImportClientImpl.getHttpClient(); + CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + String path = testProps.get("test.crossRef").toString(); + try (FileInputStream crossRefResp = new FileInputStream(path)) { + + String crossRefRespXmlResp = IOUtils.toString(crossRefResp, Charset.defaultCharset()); + + liveImportClientImpl.setHttpClient(httpClient); + CloseableHttpResponse response = mockResponse(crossRefRespXmlResp, 200, "OK"); + when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response); + + context.restoreAuthSystemState(); + Collection collection2match = getRecords(); + Collection recordsImported = crossRefServiceImpl.getRecords("test query", 0, 2); + assertEquals(2, recordsImported.size()); + assertTrue(matchRecords(recordsImported, collection2match)); + } finally { + liveImportClientImpl.setHttpClient(originalHttpClient); + } + } + + @Test + public void crossRefImportMetadataGetRecordsCountTest() throws Exception { + context.turnOffAuthorisationSystem(); + CloseableHttpClient originalHttpClient = liveImportClientImpl.getHttpClient(); + CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + String path = testProps.get("test.crossRef").toString(); + try (FileInputStream file = new FileInputStream(path)) { + String crossRefRespXmlResp = IOUtils.toString(file, Charset.defaultCharset()); + + liveImportClientImpl.setHttpClient(httpClient); + CloseableHttpResponse response = mockResponse(crossRefRespXmlResp, 200, "OK"); + when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response); + + context.restoreAuthSystemState(); + int tot = crossRefServiceImpl.getRecordsCount("test query"); + assertEquals(10, tot); + } finally { + liveImportClientImpl.setHttpClient(originalHttpClient); + } + } + + private Collection getRecords() { + Collection records = new LinkedList(); + //define first record + List metadatums = new ArrayList(); + MetadatumDTO title = createMetadatumDTO("dc", "title", null, + "State of Awareness of Freshers’ Groups Chortkiv State" + + " Medical College of Prevention of Iodine Deficiency Diseases"); + MetadatumDTO type = createMetadatumDTO("dc", "type", null, "journal-article"); + MetadatumDTO date = createMetadatumDTO("dc", "date", "issued", "2016"); + MetadatumDTO ispartof = createMetadatumDTO("dc", "relation", "ispartof", + "Ukraïnsʹkij žurnal medicini, bìologìï ta sportu"); + MetadatumDTO doi = createMetadatumDTO("dc", "identifier", "doi", "10.26693/jmbs01.02.184"); + MetadatumDTO issn = createMetadatumDTO("dc", "relation", "issn", "2415-3060"); + MetadatumDTO volume = createMetadatumDTO("oaire", "citation", "volume", "1"); + MetadatumDTO issue = createMetadatumDTO("oaire", "citation", "issue", "2"); + + metadatums.add(title); + metadatums.add(type); + metadatums.add(date); + metadatums.add(ispartof); + metadatums.add(doi); + metadatums.add(issn); + metadatums.add(volume); + metadatums.add(issue); + + ImportRecord firstrRecord = new ImportRecord(metadatums); + + //define second record + List metadatums2 = new ArrayList(); + MetadatumDTO title2 = createMetadatumDTO("dc", "title", null, + "Ischemic Heart Disease and Role of Nurse of Cardiology Department"); + MetadatumDTO type2 = createMetadatumDTO("dc", "type", null, "journal-article"); + MetadatumDTO date2 = createMetadatumDTO("dc", "date", "issued", "2016"); + MetadatumDTO ispartof2 = createMetadatumDTO("dc", "relation", "ispartof", + "Ukraïnsʹkij žurnal medicini, bìologìï ta sportu"); + MetadatumDTO doi2 = createMetadatumDTO("dc", "identifier", "doi", "10.26693/jmbs01.02.105"); + MetadatumDTO issn2 = createMetadatumDTO("dc", "relation", "issn", "2415-3060"); + MetadatumDTO volume2 = createMetadatumDTO("oaire", "citation", "volume", "1"); + MetadatumDTO issue2 = createMetadatumDTO("oaire", "citation", "issue", "2"); + + metadatums2.add(title2); + metadatums2.add(type2); + metadatums2.add(date2); + metadatums2.add(ispartof2); + metadatums2.add(doi2); + metadatums2.add(issn2); + metadatums2.add(volume2); + metadatums2.add(issue2); + + ImportRecord secondRecord = new ImportRecord(metadatums2); + records.add(firstrRecord); + records.add(secondRecord); + return records; + } + +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/resources/test-config.properties b/dspace-server-webapp/src/test/resources/test-config.properties index 3af96b20fcf4..3942eb9b1c84 100644 --- a/dspace-server-webapp/src/test/resources/test-config.properties +++ b/dspace-server-webapp/src/test/resources/test-config.properties @@ -14,3 +14,5 @@ test.bitstream = ./target/testing/dspace/assetstore/ConstitutionofIreland.pdf #Path for a test Taskfile for the curate script test.curateTaskFile = ./target/testing/dspace/assetstore/curate.txt + +test.crossRef = ./target/testing/dspace/assetstore/crossRef-test.json From 181bdd04d1145cd4a99528b9bd136c0da18e0065 Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Sat, 2 Apr 2022 13:13:38 +0200 Subject: [PATCH 0098/1846] [CST-5303] porting of CrossRef live import service --- dspace-api/pom.xml | 5 + .../CrossRefAuthorMetadataProcessor.java | 44 +++ .../crossref/CrossRefFieldMapping.java | 37 ++ ...ossRefImportMetadataSourceServiceImpl.java | 332 ++++++++++++++++++ .../JsonPathMetadataProcessor.java | 16 + .../SimpleJsonPathMetadataContributor.java | 138 ++++++++ .../spring-dspace-addon-import-services.xml | 5 + .../spring/api/crossref-integration.xml | 144 ++++++++ .../config/spring/api/external-services.xml | 12 +- 9 files changed, 732 insertions(+), 1 deletion(-) create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefAuthorMetadataProcessor.java create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefFieldMapping.java create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefImportMetadataSourceServiceImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/JsonPathMetadataProcessor.java create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleJsonPathMetadataContributor.java create mode 100644 dspace/config/spring/api/crossref-integration.xml diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 66ff4e69bcf9..ba816fca3858 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -337,6 +337,11 @@ + + net.minidev + json-smart + 2.3 + org.apache.logging.log4j log4j-api diff --git a/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefAuthorMetadataProcessor.java b/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefAuthorMetadataProcessor.java new file mode 100644 index 000000000000..c337eebb032a --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefAuthorMetadataProcessor.java @@ -0,0 +1,44 @@ +/** + * 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.importer.external.crossref; + +import java.util.ArrayList; +import java.util.Collection; + +import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.ReadContext; +import org.dspace.importer.external.metadatamapping.contributor.JsonPathMetadataProcessor; +import org.json.JSONArray; + +public class CrossRefAuthorMetadataProcessor implements JsonPathMetadataProcessor { + + private String givenNameJsonPath; + private String familyNameJsonPath; + + public void setGivenNameJsonPath(String givenNameJsonPath) { + this.givenNameJsonPath = givenNameJsonPath; + } + + public void setFamilyNameJsonPath(String familyNameJsonPath) { + this.familyNameJsonPath = familyNameJsonPath; + } + + @Override + public Collection processMetadata(String json) { + ReadContext ctx = JsonPath.parse(json); + JSONArray givenNames = ctx.read(givenNameJsonPath); + JSONArray familyNames = ctx.read(familyNameJsonPath); + Collection values = new ArrayList<>(); + for (int i = 0; i < givenNames.length(); i++) { + String name = givenNames.get(i).toString(); + values.add(name + " " + familyNames.get(i).toString()); + } + return values; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefFieldMapping.java b/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefFieldMapping.java new file mode 100644 index 000000000000..4bac11ab3446 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefFieldMapping.java @@ -0,0 +1,37 @@ +/** + * 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.importer.external.crossref; + +import java.util.Map; +import javax.annotation.Resource; + +import org.dspace.importer.external.metadatamapping.AbstractMetadataFieldMapping; + +/** + * An implementation of {@link AbstractMetadataFieldMapping} + * Responsible for defining the mapping of the ArXiv metadatum fields on the DSpace metadatum fields + * + * @author Pasquale Cavallo (pasquale.cavallo at 4science dot it) + */ +public class CrossRefFieldMapping extends AbstractMetadataFieldMapping { + + /** + * Defines which metadatum is mapped on which metadatum. Note that while the key must be unique it + * only matters here for postprocessing of the value. The mapped MetadatumContributor has full control over + * what metadatafield is generated. + * + * @param metadataFieldMap The map containing the link between retrieve metadata and metadata that will be set to + * the item. + */ + @Override + @Resource(name = "crossrefMetadataFieldMap") + public void setMetadataFieldMap(Map metadataFieldMap) { + super.setMetadataFieldMap(metadataFieldMap); + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefImportMetadataSourceServiceImpl.java new file mode 100644 index 000000000000..7c7644b3ef98 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefImportMetadataSourceServiceImpl.java @@ -0,0 +1,332 @@ +/** + * 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.importer.external.crossref; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.Callable; +import javax.el.MethodNotFoundException; + +import com.google.gson.Gson; +import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.ReadContext; +import net.minidev.json.JSONArray; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.client.utils.URIBuilder; +import org.dspace.content.Item; +import org.dspace.importer.external.datamodel.ImportRecord; +import org.dspace.importer.external.datamodel.Query; +import org.dspace.importer.external.exception.MetadataSourceException; +import org.dspace.importer.external.scopus.service.LiveImportClient; +import org.dspace.importer.external.service.AbstractImportMetadataSourceService; +import org.dspace.importer.external.service.DoiCheck; +import org.dspace.importer.external.service.components.QuerySource; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Implements a data source for querying CrossRef + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com) + */ +public class CrossRefImportMetadataSourceServiceImpl extends AbstractImportMetadataSourceService + implements QuerySource { + + private static final String ENDPOINT_WORKS = "https://api.crossref.org/works"; + + @Autowired + private LiveImportClient liveImportClient; + + @Override + public String getImportSource() { + return "crossref"; + } + + @Override + public void init() throws Exception {} + + @Override + public ImportRecord getRecord(String recordId) throws MetadataSourceException { + List records = null; + String id = getID(recordId); + if (StringUtils.isNotBlank(id)) { + records = retry(new SearchByIdCallable(id)); + } else { + records = retry(new SearchByIdCallable(recordId)); + } + return records == null || records.isEmpty() ? null : records.get(0); + } + + @Override + public int getRecordsCount(String query) throws MetadataSourceException { + String id = getID(query); + if (StringUtils.isNotBlank(id)) { + return retry(new DoiCheckCallable(id)); + } + return retry(new CountByQueryCallable(query)); + } + + @Override + public int getRecordsCount(Query query) throws MetadataSourceException { + String id = getID(query.toString()); + if (StringUtils.isNotBlank(id)) { + return retry(new DoiCheckCallable(id)); + } + return retry(new CountByQueryCallable(query)); + } + + @Override + public Collection getRecords(String query, int start, int count) throws MetadataSourceException { + String id = getID(query.toString()); + if (StringUtils.isNotBlank(id)) { + return retry(new SearchByIdCallable(id)); + } + return retry(new SearchByQueryCallable(query, count, start)); + } + + @Override + public Collection getRecords(Query query) throws MetadataSourceException { + String id = getID(query.toString()); + if (StringUtils.isNotBlank(id)) { + return retry(new SearchByIdCallable(id)); + } + return retry(new SearchByQueryCallable(query)); + } + + @Override + public ImportRecord getRecord(Query query) throws MetadataSourceException { + List records = null; + String id = getID(query.toString()); + if (StringUtils.isNotBlank(id)) { + records = retry(new SearchByIdCallable(id)); + } else { + records = retry(new SearchByIdCallable(query)); + } + return records == null || records.isEmpty() ? null : records.get(0); + } + + @Override + public Collection findMatchingRecords(Query query) throws MetadataSourceException { + String id = getID(query.toString()); + if (StringUtils.isNotBlank(id)) { + return retry(new SearchByIdCallable(id)); + } + return retry(new FindMatchingRecordCallable(query)); + } + + + @Override + public Collection findMatchingRecords(Item item) throws MetadataSourceException { + throw new MethodNotFoundException("This method is not implemented for CrossRef"); + } + + public String getID(String query) { + if (DoiCheck.isDoi(query)) { + return "filter=doi:" + query; + } + return StringUtils.EMPTY; + } + + private class SearchByQueryCallable implements Callable> { + + private Query query; + + private SearchByQueryCallable(String queryString, Integer maxResult, Integer start) { + query = new Query(); + query.addParameter("query", queryString); + query.addParameter("count", maxResult); + query.addParameter("start", start); + } + + private SearchByQueryCallable(Query query) { + this.query = query; + } + + @Override + public List call() throws Exception { + List results = new ArrayList<>(); + Integer count = query.getParameterAsClass("count", Integer.class); + Integer start = query.getParameterAsClass("start", Integer.class); + + URIBuilder uriBuilder = new URIBuilder(ENDPOINT_WORKS); + uriBuilder.addParameter("query", query.getParameterAsClass("query", String.class)); + if (Objects.nonNull(count)) { + uriBuilder.addParameter("rows", count.toString()); + } + if (Objects.nonNull(start)) { + uriBuilder.addParameter("offset", start.toString()); + } + + String response = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), + new HashMap()); + ReadContext ctx = JsonPath.parse(response); + Object o = ctx.read("$.message.items"); + if (o.getClass().isAssignableFrom(JSONArray.class)) { + JSONArray array = (JSONArray) o; + int size = array.size(); + for (int index = 0; index < size; index++) { + Gson gson = new Gson(); + String innerJson = gson.toJson(array.get(index), LinkedHashMap.class); + results.add(transformSourceRecords(innerJson)); + } + } else { + results.add(transformSourceRecords(o.toString())); + } + return results; + } + + } + + private class SearchByIdCallable implements Callable> { + private Query query; + + private SearchByIdCallable(Query query) { + this.query = query; + } + + private SearchByIdCallable(String id) { + this.query = new Query(); + query.addParameter("id", id); + } + + @Override + public List call() throws Exception { + List results = new ArrayList<>(); + URIBuilder uriBuilder = new URIBuilder( + ENDPOINT_WORKS + "/" + query.getParameterAsClass("id", String.class)); + String responseString = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), + new HashMap()); + ReadContext ctx = JsonPath.parse(responseString); + Object o = ctx.read("$.message"); + if (o.getClass().isAssignableFrom(JSONArray.class)) { + JSONArray array = (JSONArray) o; + int size = array.size(); + for (int index = 0; index < size; index++) { + Gson gson = new Gson(); + String innerJson = gson.toJson(array.get(index), LinkedHashMap.class); + results.add(transformSourceRecords(innerJson)); + } + } else { + Gson gson = new Gson(); + results.add(transformSourceRecords(gson.toJson(o, Object.class))); + } + return results; + } + } + + private class FindMatchingRecordCallable implements Callable> { + + private Query query; + + private FindMatchingRecordCallable(Query q) { + query = q; + } + + @Override + public List call() throws Exception { + String queryValue = query.getParameterAsClass("query", String.class); + Integer count = query.getParameterAsClass("count", Integer.class); + Integer start = query.getParameterAsClass("start", Integer.class); + String author = query.getParameterAsClass("author", String.class); + String title = query.getParameterAsClass("title", String.class); + String bibliographics = query.getParameterAsClass("bibliographics", String.class); + List results = new ArrayList<>(); + URIBuilder uriBuilder = new URIBuilder(ENDPOINT_WORKS); + if (Objects.nonNull(queryValue)) { + uriBuilder.addParameter("query", queryValue); + } + if (Objects.nonNull(count)) { + uriBuilder.addParameter("rows", count.toString()); + } + if (Objects.nonNull(start)) { + uriBuilder.addParameter("offset", start.toString()); + } + if (Objects.nonNull(author)) { + uriBuilder.addParameter("query.author", author); + } + if (Objects.nonNull(title )) { + uriBuilder.addParameter("query.container-title", title); + } + if (Objects.nonNull(bibliographics)) { + uriBuilder.addParameter("query.bibliographic", bibliographics); + } + + String resp = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), + new HashMap()); + ReadContext ctx = JsonPath.parse(resp); + Object o = ctx.read("$.message.items[*]"); + if (o.getClass().isAssignableFrom(JSONArray.class)) { + JSONArray array = (JSONArray) o; + int size = array.size(); + for (int index = 0; index < size; index++) { + Gson gson = new Gson(); + String innerJson = gson.toJson(array.get(index), LinkedHashMap.class); + results.add(transformSourceRecords(innerJson)); + } + } else { + results.add(transformSourceRecords(o.toString())); + } + return results; + } + + } + + private class CountByQueryCallable implements Callable { + private Query query; + + + private CountByQueryCallable(String queryString) { + query = new Query(); + query.addParameter("query", queryString); + } + + private CountByQueryCallable(Query query) { + this.query = query; + } + + + @Override + public Integer call() throws Exception { + URIBuilder uriBuilder = new URIBuilder(ENDPOINT_WORKS); + uriBuilder.addParameter("query", query.getParameterAsClass("query", String.class)); + String responseString = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), + new HashMap()); + ReadContext ctx = JsonPath.parse(responseString); + return ctx.read("$.message.total-results"); + } + } + + private class DoiCheckCallable implements Callable { + + private final Query query; + + private DoiCheckCallable(final String id) { + final Query query = new Query(); + query.addParameter("id", id); + this.query = query; + } + + private DoiCheckCallable(final Query query) { + this.query = query; + } + + @Override + public Integer call() throws Exception { + URIBuilder uriBuilder = new URIBuilder( + ENDPOINT_WORKS + "/" + query.getParameterAsClass("id", String.class)); + String responseString = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), + new HashMap()); + ReadContext ctx = JsonPath.parse(responseString); + return StringUtils.equals(ctx.read("$.status"), "ok") ? 1 : 0; + } + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/JsonPathMetadataProcessor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/JsonPathMetadataProcessor.java new file mode 100644 index 000000000000..961f4a6066e0 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/JsonPathMetadataProcessor.java @@ -0,0 +1,16 @@ +/** + * 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.importer.external.metadatamapping.contributor; + +import java.util.Collection; + +public interface JsonPathMetadataProcessor { + + public Collection processMetadata(String json); + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleJsonPathMetadataContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleJsonPathMetadataContributor.java new file mode 100644 index 000000000000..6148b3a11561 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleJsonPathMetadataContributor.java @@ -0,0 +1,138 @@ +/** + * 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.importer.external.metadatamapping.contributor; + +import java.util.ArrayList; +import java.util.Collection; + +import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.ReadContext; +import net.minidev.json.JSONArray; +import org.dspace.importer.external.metadatamapping.MetadataFieldConfig; +import org.dspace.importer.external.metadatamapping.MetadataFieldMapping; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; + +public class SimpleJsonPathMetadataContributor implements MetadataContributor { + + private String query; + + private MetadataFieldConfig field; + + protected JsonPathMetadataProcessor metadataProcessor; + + /** + * Initialize SimpleJsonPathMetadataContributor with a query, prefixToNamespaceMapping and MetadataFieldConfig + * + * @param query The JSonPath query + * @param field the matadata field to map the result of the Json path query + * MetadataFieldConfig + */ + public SimpleJsonPathMetadataContributor(String query, MetadataFieldConfig field) { + this.query = query; + this.field = field; + } + + + /** + * Unused by this implementation + */ + @Override + public void setMetadataFieldMapping(MetadataFieldMapping> rt) { + + } + + /** + * Empty constructor for SimpleJsonPathMetadataContributor + */ + public SimpleJsonPathMetadataContributor() { + + } + + /** + * Return the MetadataFieldConfig used while retrieving MetadatumDTO + * + * @return MetadataFieldConfig + */ + public MetadataFieldConfig getField() { + return field; + } + + /** + * Setting the MetadataFieldConfig + * + * @param field MetadataFieldConfig used while retrieving MetadatumDTO + */ + public void setField(MetadataFieldConfig field) { + this.field = field; + } + + /** + * Return query used to create the JSonPath + * + * @return the query this instance is based on + */ + public String getQuery() { + return query; + } + + /** + * Return query used to create the JSonPath + * + */ + public void setQuery(String query) { + this.query = query; + } + + /** + * Used to process data got by jsonpath expression, like arrays to stringify, change date format or else + * If it is null, toString will be used. + * + * @param metadataProcessor + */ + public void setMetadataProcessor(JsonPathMetadataProcessor metadataProcessor) { + this.metadataProcessor = metadataProcessor; + } + + /** + * Retrieve the metadata associated with the given object. + * The toString() of the resulting object will be used. + * + * @param t A class to retrieve metadata from. + * @return a collection of import records. Only the identifier of the found records may be put in the record. + */ + @Override + public Collection contributeMetadata(String fullJson) { + Collection metadata = new ArrayList<>(); + Collection metadataValue = new ArrayList<>(); + if (metadataProcessor != null) { + metadataValue = metadataProcessor.processMetadata(fullJson); + } else { + ReadContext ctx = JsonPath.parse(fullJson); + Object o = ctx.read(query); + if (o.getClass().isAssignableFrom(JSONArray.class)) { + JSONArray results = (JSONArray)o; + for (int i = 0; i < results.size(); i++) { + String value = results.get(i).toString(); + metadataValue.add(value); + } + } else { + metadataValue.add(o.toString()); + } + } + for (String value : metadataValue) { + MetadatumDTO metadatumDto = new MetadatumDTO(); + metadatumDto.setValue(value); + metadatumDto.setElement(field.getElement()); + metadatumDto.setQualifier(field.getQualifier()); + metadatumDto.setSchema(field.getSchema()); + metadata.add(metadatumDto); + } + return metadata; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml b/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml index 5e69ee9c4282..96149c581c9b 100644 --- a/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml +++ b/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml @@ -116,6 +116,11 @@ + + + + + diff --git a/dspace/config/spring/api/crossref-integration.xml b/dspace/config/spring/api/crossref-integration.xml new file mode 100644 index 000000000000..1d921d934198 --- /dev/null +++ b/dspace/config/spring/api/crossref-integration.xml @@ -0,0 +1,144 @@ + + + + + + + + Defines which metadatum is mapped on which metadatum. Note that while the key must be unique it + only matters here for postprocessing of the value. The mapped MetadatumContributor has full control over + what metadatafield is generated. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dspace/config/spring/api/external-services.xml b/dspace/config/spring/api/external-services.xml index 7f1295f839df..90da4ed0c3a3 100644 --- a/dspace/config/spring/api/external-services.xml +++ b/dspace/config/spring/api/external-services.xml @@ -94,5 +94,15 @@ - + + + + + + + Publication + + + + \ No newline at end of file From cb44a8cf15c256dffcf94acfea190eefc212d422 Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Mon, 4 Apr 2022 09:56:15 +0200 Subject: [PATCH 0099/1846] [CST-5303] porting of VuFind live import service --- ...VuFindImportMetadataSourceServiceImpl.java | 289 ++++++++++++++++++ .../metadatamapping/VuFindFieldMapping.java | 32 ++ .../spring-dspace-addon-import-services.xml | 8 +- .../config/spring/api/external-services.xml | 11 + .../config/spring/api/vufind-integration.xml | 155 ++++++++++ 5 files changed, 494 insertions(+), 1 deletion(-) create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/vufind/VuFindImportMetadataSourceServiceImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/vufind/metadatamapping/VuFindFieldMapping.java create mode 100644 dspace/config/spring/api/vufind-integration.xml diff --git a/dspace-api/src/main/java/org/dspace/importer/external/vufind/VuFindImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/vufind/VuFindImportMetadataSourceServiceImpl.java new file mode 100644 index 000000000000..3293a0b88600 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/vufind/VuFindImportMetadataSourceServiceImpl.java @@ -0,0 +1,289 @@ +/** + * 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.importer.external.vufind; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.concurrent.Callable; +import javax.el.MethodNotFoundException; + +import com.google.gson.Gson; +import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.ReadContext; +import net.minidev.json.JSONArray; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.client.utils.URIBuilder; +import org.apache.log4j.Logger; +import org.dspace.content.Item; +import org.dspace.importer.external.datamodel.ImportRecord; +import org.dspace.importer.external.datamodel.Query; +import org.dspace.importer.external.exception.MetadataSourceException; +import org.dspace.importer.external.scopus.service.LiveImportClient; +import org.dspace.importer.external.service.AbstractImportMetadataSourceService; +import org.dspace.importer.external.service.components.QuerySource; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Implements a data source for querying VuFind + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com) + */ +public class VuFindImportMetadataSourceServiceImpl extends AbstractImportMetadataSourceService + implements QuerySource { + + private static final Logger log = Logger.getLogger(VuFindImportMetadataSourceServiceImpl.class); + + private static final String ENDPOINT_SEARCH = "https://vufind.org/demo/api/v1/search"; + private static final String ENDPOINT_RECORD = "https://vufind.org/demo/api/v1/record"; + + private String fields; + + @Autowired + private LiveImportClient liveImportClient; + + public VuFindImportMetadataSourceServiceImpl(String fields) { + this.fields = fields; + } + + @Override + public String getImportSource() { + return "VuFind"; + } + + @Override + public ImportRecord getRecord(String id) throws MetadataSourceException { + String records = retry(new GetByVuFindIdCallable(id, fields)); + List importRecords = extractMetadataFromRecordList(records); + return importRecords != null && !importRecords.isEmpty() ? importRecords.get(0) : null; + } + + @Override + public int getRecordsCount(String query) throws MetadataSourceException { + return retry(new CountByQueryCallable(query)); + } + + @Override + public int getRecordsCount(Query query) throws MetadataSourceException { + return retry(new CountByQueryCallable(query)); + } + + @Override + public Collection getRecords(String query, int start, int count) throws MetadataSourceException { + String records = retry(new SearchByQueryCallable(query, count, start, fields)); + return extractMetadataFromRecordList(records); + } + + @Override + public Collection getRecords(Query query) throws MetadataSourceException { + String records = retry(new SearchByQueryCallable(query, fields)); + return extractMetadataFromRecordList(records); + } + + @Override + public ImportRecord getRecord(Query query) throws MetadataSourceException { + String records = retry(new SearchByQueryCallable(query, fields)); + List importRecords = extractMetadataFromRecordList(records); + return importRecords != null && !importRecords.isEmpty() ? importRecords.get(0) : null; + } + + @Override + public Collection findMatchingRecords(Query query) throws MetadataSourceException { + String records = retry(new FindMatchingRecordsCallable(query)); + return extractMetadataFromRecordList(records); + } + + @Override + public Collection findMatchingRecords(Item item) throws MetadataSourceException { + throw new MethodNotFoundException("This method is not implemented for VuFind"); + } + + @Override + public void init() throws Exception {} + + private class CountByQueryCallable implements Callable { + + private Query query; + + public CountByQueryCallable(String queryString) { + query = new Query(); + query.addParameter("query", queryString); + } + + public CountByQueryCallable(Query query) { + this.query = query; + } + + @Override + public Integer call() throws Exception { + Integer start = 0; + Integer count = 1; + int page = start / count + 1; + URIBuilder uriBuilder = new URIBuilder(ENDPOINT_SEARCH); + uriBuilder.addParameter("type", "AllField"); + uriBuilder.addParameter("page", String.valueOf(page)); + uriBuilder.addParameter("limit", count.toString()); + uriBuilder.addParameter("prettyPrint", String.valueOf(true)); + uriBuilder.addParameter("lookfor", query.getParameterAsClass("query", String.class)); + String responseString = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), + new HashMap()); + ReadContext ctx = JsonPath.parse(responseString); + return ctx.read("$.resultCount"); + } + } + + private class GetByVuFindIdCallable implements Callable { + + private String id; + + private String fields; + + public GetByVuFindIdCallable(String id, String fields) { + this.id = id; + if (fields != null && fields.length() > 0) { + this.fields = fields; + } else { + this.fields = null; + } + } + + @Override + public String call() throws Exception { + URIBuilder uriBuilder = new URIBuilder(ENDPOINT_RECORD); + uriBuilder.addParameter("id", id); + uriBuilder.addParameter("prettyPrint", "false"); + if (StringUtils.isNotBlank(fields)) { + for (String field : fields.split(",")) { + uriBuilder.addParameter("field[]", field); + } + } + String response = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), + new HashMap()); + return response; + } + } + + private class SearchByQueryCallable implements Callable { + + private Query query; + + private String fields; + + public SearchByQueryCallable(String queryString, Integer maxResult, Integer start, String fields) { + query = new Query(); + query.addParameter("query", queryString); + query.addParameter("count", maxResult); + query.addParameter("start", start); + if (StringUtils.isNotBlank(fields)) { + this.fields = fields; + } else { + this.fields = null; + } + } + + public SearchByQueryCallable(Query query, String fields) { + this.query = query; + if (StringUtils.isNotBlank(fields)) { + this.fields = fields; + } else { + this.fields = null; + } + } + + @Override + public String call() throws Exception { + Integer start = query.getParameterAsClass("start", Integer.class); + Integer count = query.getParameterAsClass("count", Integer.class); + int page = count != 0 ? start / count : 0; + URIBuilder uriBuilder = new URIBuilder(ENDPOINT_SEARCH); + uriBuilder.addParameter("type", "AllField"); + //page looks 1 based (start = 0, count = 20 -> page = 0) + uriBuilder.addParameter("page", String.valueOf(page + 1)); + uriBuilder.addParameter("limit", count.toString()); + uriBuilder.addParameter("prettyPrint", String.valueOf(true)); + uriBuilder.addParameter("lookfor", query.getParameterAsClass("query", String.class)); + if (StringUtils.isNotBlank(fields)) { + for (String field : fields.split(",")) { + uriBuilder.addParameter("field[]", field); + } + } + return liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), new HashMap()); + } + + } + + public class FindMatchingRecordsCallable implements Callable { + + private Query query; + + private String fields; + + public FindMatchingRecordsCallable(Query query) { + this.query = query; + } + + @Override + public String call() throws Exception { + String author = query.getParameterAsClass("author", String.class); + String title = query.getParameterAsClass("title", String.class); + Integer start = query.getParameterAsClass("start", Integer.class); + Integer count = query.getParameterAsClass("count", Integer.class); + int page = count != 0 ? start / count : 0; + URIBuilder uriBuilder = new URIBuilder(ENDPOINT_RECORD); + uriBuilder.addParameter("type", "AllField"); + //pagination is 1 based (first page: start = 0, count = 20 -> page = 0 -> +1 = 1) + uriBuilder.addParameter("page", String.valueOf(page ++)); + uriBuilder.addParameter("limit", count.toString()); + uriBuilder.addParameter("prettyPrint", "true"); + if (fields != null && !fields.isEmpty()) { + for (String field : fields.split(",")) { + uriBuilder.addParameter("field[]", field); + } + } + String filter = StringUtils.EMPTY; + if (StringUtils.isNotBlank(author)) { + filter = "author:" + author; + } + if (StringUtils.isNotBlank(title)) { + if (StringUtils.isNotBlank(filter)) { + filter = filter + " AND title:" + title; + } else { + filter = "title:" + title; + } + } + uriBuilder.addParameter("lookfor", filter); + return liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), new HashMap()); + } + + } + + private List extractMetadataFromRecordList(String records) { + List recordsResult = new ArrayList<>(); + ReadContext ctx = JsonPath.parse(records); + try { + Object o = ctx.read("$.records[*]"); + if (o.getClass().isAssignableFrom(JSONArray.class)) { + JSONArray array = (JSONArray)o; + int size = array.size(); + for (int index = 0; index < size; index++) { + Gson gson = new Gson(); + String innerJson = gson.toJson(array.get(index), LinkedHashMap.class); + recordsResult.add(transformSourceRecords(innerJson)); + } + } else { + recordsResult.add(transformSourceRecords(o.toString())); + } + } catch (Exception e) { + log.error("Error reading data from VuFind " + e.getMessage(), e); + } + return recordsResult; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/vufind/metadatamapping/VuFindFieldMapping.java b/dspace-api/src/main/java/org/dspace/importer/external/vufind/metadatamapping/VuFindFieldMapping.java new file mode 100644 index 000000000000..baed54f5d76c --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/vufind/metadatamapping/VuFindFieldMapping.java @@ -0,0 +1,32 @@ +/** + * 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.importer.external.vufind.metadatamapping; + +import java.util.Map; +import javax.annotation.Resource; + +import org.dspace.importer.external.metadatamapping.AbstractMetadataFieldMapping; + +@SuppressWarnings("rawtypes") +public class VuFindFieldMapping extends AbstractMetadataFieldMapping { + + /** + * Defines which metadatum is mapped on which metadatum. Note that while the key must be unique it + * only matters here for postprocessing of the value. The mapped MetadatumContributor has full control over + * what metadatafield is generated. + * + * @param metadataFieldMap The map containing the link between retrieve metadata and metadata that will be set to + * the item. + */ + @Override + @Resource(name = "vufindMetadataFieldMap") + public void setMetadataFieldMap(Map metadataFieldMap) { + super.setMetadataFieldMap(metadataFieldMap); + } + +} diff --git a/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml b/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml index 96149c581c9b..8a75b1b14d79 100644 --- a/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml +++ b/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml @@ -115,12 +115,18 @@ - + + + + + + + diff --git a/dspace/config/spring/api/external-services.xml b/dspace/config/spring/api/external-services.xml index 90da4ed0c3a3..83d7a27738e4 100644 --- a/dspace/config/spring/api/external-services.xml +++ b/dspace/config/spring/api/external-services.xml @@ -105,4 +105,15 @@ + + + + + + + Publication + + + + \ No newline at end of file diff --git a/dspace/config/spring/api/vufind-integration.xml b/dspace/config/spring/api/vufind-integration.xml new file mode 100644 index 000000000000..094f3207a99c --- /dev/null +++ b/dspace/config/spring/api/vufind-integration.xml @@ -0,0 +1,155 @@ + + + + + + + Defines which metadatum is mapped on which metadatum. Note that while the key must be unique it + only matters here for postprocessing of the value. The mapped MetadatumContributor has full control over + what metadatafield is generated. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 06b4b219211438d3126a45101cbea4942af60a69 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Mon, 4 Apr 2022 12:04:51 +0200 Subject: [PATCH 0100/1846] [CST-5306] Minor improvements in ResearcherProfileServiceImpl --- .../profile/ResearcherProfileServiceImpl.java | 78 +++++++++---------- .../test/data/dspaceFolder/config/local.cfg | 2 +- 2 files changed, 39 insertions(+), 41 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfileServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfileServiceImpl.java index 75c1db45b018..19c2f03e5e21 100644 --- a/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfileServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfileServiceImpl.java @@ -7,6 +7,9 @@ */ package org.dspace.app.profile; +import static java.util.Optional.empty; +import static java.util.Optional.ofNullable; +import static org.apache.commons.lang3.ArrayUtils.contains; import static org.dspace.content.authority.Choices.CF_ACCEPTED; import static org.dspace.core.Constants.READ; import static org.dspace.eperson.Group.ANONYMOUS; @@ -14,10 +17,9 @@ import java.io.IOException; import java.net.URI; import java.sql.SQLException; -import java.util.Arrays; import java.util.Iterator; import java.util.List; -import java.util.Objects; +import java.util.Optional; import java.util.UUID; import org.apache.commons.collections4.CollectionUtils; @@ -107,18 +109,19 @@ public ResearcherProfile createAndReturn(Context context, EPerson ePerson) throw new ResourceConflictException("A profile is already linked to the provided User", profile); } - Collection collection = findProfileCollection(context); - if (collection == null) { - throw new IllegalStateException("No collection found for researcher profiles"); - } + Collection collection = findProfileCollection(context) + .orElseThrow(() -> new IllegalStateException("No collection found for researcher profiles")); context.turnOffAuthorisationSystem(); - Item item = createProfileItem(context, ePerson, collection); - context.restoreAuthSystemState(); + try { - ResearcherProfile researcherProfile = new ResearcherProfile(item); + Item item = createProfileItem(context, ePerson, collection); + return new ResearcherProfile(item); + + } finally { + context.restoreAuthSystemState(); + } - return researcherProfile; } @Override @@ -160,25 +163,21 @@ public void changeVisibility(Context context, ResearcherProfile profile, boolean @Override public ResearcherProfile claim(final Context context, final EPerson ePerson, final URI uri) throws SQLException, AuthorizeException, SearchServiceException { + Item profileItem = findResearcherProfileItemById(context, ePerson.getID()); if (profileItem != null) { ResearcherProfile profile = new ResearcherProfile(profileItem); throw new ResourceConflictException("A profile is already linked to the provided User", profile); } - Collection collection = findProfileCollection(context); - if (collection == null) { - throw new IllegalStateException("No collection found for researcher profiles"); - } + Item item = findItemByURI(context, uri) + .orElseThrow(() -> new IllegalArgumentException("No item found by URI " + uri)); - final String path = uri.getPath(); - final UUID uuid = UUIDUtils.fromString(path.substring(path.lastIndexOf("/") + 1 )); - Item item = itemService.find(context, uuid); - if (Objects.isNull(item) || !item.isArchived() || item.isWithdrawn() || notClaimableEntityType(item)) { + if (!item.isArchived() || item.isWithdrawn() || notClaimableEntityType(item)) { throw new IllegalArgumentException("Provided uri does not represent a valid Item to be claimed"); } - final String existingOwner = itemService.getMetadataFirstValue(item, "dspace", "object", - "owner", null); + + String existingOwner = itemService.getMetadataFirstValue(item, "dspace", "object", "owner", Item.ANY); if (StringUtils.isNotBlank(existingOwner)) { throw new IllegalArgumentException("Item with provided uri has already an owner"); @@ -192,10 +191,15 @@ public ResearcherProfile claim(final Context context, final EPerson ePerson, fin return new ResearcherProfile(item); } + private Optional findItemByURI(final Context context, final URI uri) throws SQLException { + String path = uri.getPath(); + UUID uuid = UUIDUtils.fromString(path.substring(path.lastIndexOf("/") + 1)); + return ofNullable(itemService.find(context, uuid)); + } + private boolean notClaimableEntityType(final Item item) { - final String entityType = itemService.getEntityType(item); - return Arrays.stream(configurationService.getArrayProperty("claimable.entityType")) - .noneMatch(entityType::equals); + String entityType = itemService.getEntityType(item); + return !contains(configurationService.getArrayProperty("claimable.entityType"), entityType); } private Item findResearcherProfileItemById(Context context, UUID id) throws SQLException, AuthorizeException { @@ -205,18 +209,20 @@ private Item findResearcherProfileItemById(Context context, UUID id) throws SQLE Iterator items = itemService.findByAuthorityValue(context, "dspace", "object", "owner", id.toString()); while (items.hasNext()) { Item item = items.next(); - if (hasEntityTypeMetadataEqualsTo(item, profileType)) { + String entityType = itemService.getEntityType(item); + if (profileType.equals(entityType)) { return item; } } + return null; } @SuppressWarnings("rawtypes") - private Collection findProfileCollection(Context context) throws SQLException, SearchServiceException { + private Optional findProfileCollection(Context context) throws SQLException, SearchServiceException { UUID uuid = UUIDUtils.fromString(configurationService.getProperty("researcher-profile.collection.uuid")); if (uuid != null) { - return collectionService.find(context, uuid); + return ofNullable(collectionService.find(context, uuid)); } String profileType = getProfileType(); @@ -229,28 +235,27 @@ private Collection findProfileCollection(Context context) throws SQLException, S List indexableObjects = discoverResult.getIndexableObjects(); if (CollectionUtils.isEmpty(indexableObjects)) { - return null; + return empty(); } if (indexableObjects.size() > 1) { log.warn("Multiple " + profileType + " type collections were found during profile creation"); - return null; + return empty(); } - return (Collection) indexableObjects.get(0).getIndexedObject(); + return ofNullable((Collection) indexableObjects.get(0).getIndexedObject()); } private Item createProfileItem(Context context, EPerson ePerson, Collection collection) throws AuthorizeException, SQLException { String id = ePerson.getID().toString(); + String fullName = ePerson.getFullName(); WorkspaceItem workspaceItem = workspaceItemService.create(context, collection, true); Item item = workspaceItem.getItem(); - itemService.addMetadata(context, item, "dc", "title", null, null, - ePerson.getFullName()); - itemService.addMetadata(context, item, "dspace", "object", "owner", null, - ePerson.getFullName(), id, CF_ACCEPTED); + itemService.addMetadata(context, item, "dc", "title", null, null, fullName); + itemService.addMetadata(context, item, "dspace", "object", "owner", null, fullName, id, CF_ACCEPTED); item = installItemService.installItem(context, workspaceItem); @@ -261,13 +266,6 @@ private Item createProfileItem(Context context, EPerson ePerson, Collection coll return item; } - private boolean hasEntityTypeMetadataEqualsTo(Item item, String entityType) { - return item.getMetadata().stream().anyMatch(metadataValue -> { - return "dspace.entity.type".equals(metadataValue.getMetadataField().toString('.')) && - entityType.equals(metadataValue.getValue()); - }); - } - private boolean isHardDeleteEnabled() { return configurationService.getBooleanProperty("researcher-profile.hard-delete.enabled"); } diff --git a/dspace-api/src/test/data/dspaceFolder/config/local.cfg b/dspace-api/src/test/data/dspaceFolder/config/local.cfg index 34acade122e6..a9f606c1808d 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/local.cfg +++ b/dspace-api/src/test/data/dspaceFolder/config/local.cfg @@ -20,6 +20,7 @@ # For example, including "dspace.dir" in this local.cfg will override the # default value of "dspace.dir" in the dspace.cfg file. # + ########################## # SERVER CONFIGURATION # ########################## @@ -144,5 +145,4 @@ useProxies = true proxies.trusted.ipranges = 7.7.7.7 proxies.trusted.include_ui_ip = true -# researcher-profile.type researcher-profile.type = Person \ No newline at end of file From e0913ccc5c7574f2f7319914daad794c6741c42f Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Mon, 4 Apr 2022 15:02:29 +0200 Subject: [PATCH 0101/1846] [CST-5303] porting of Scielo live import service --- .../SimpleRisToMetadataConcatContributor.java | 59 +++++ .../SimpleRisToMetadataContributor.java | 71 ++++++ .../scielo/service/ScieloFieldMapping.java | 37 +++ ...ScieloImportMetadataSourceServiceImpl.java | 230 ++++++++++++++++++ .../spring-dspace-addon-import-services.xml | 5 + .../config/spring/api/external-services.xml | 12 +- 6 files changed, 413 insertions(+), 1 deletion(-) create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleRisToMetadataConcatContributor.java create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleRisToMetadataContributor.java create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/scielo/service/ScieloFieldMapping.java create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/scielo/service/ScieloImportMetadataSourceServiceImpl.java diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleRisToMetadataConcatContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleRisToMetadataConcatContributor.java new file mode 100644 index 000000000000..5dd354c6f18c --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleRisToMetadataConcatContributor.java @@ -0,0 +1,59 @@ +/** + * 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.importer.external.metadatamapping.contributor; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.dspace.importer.external.metadatamapping.MetadataFieldConfig; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; + +/** + * This contributor extends SimpleRisToMetadataContributor, + * in particular, this one is able to chain multi values into a single one + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.it) + */ +public class SimpleRisToMetadataConcatContributor extends SimpleRisToMetadataContributor { + + private String tag; + + private MetadataFieldConfig metadata; + + @Override + public Collection contributeMetadata(Map> record) { + List values = new LinkedList<>(); + List fieldValues = record.get(this.tag); + Optional.ofNullable(fieldValues) + .map(fv -> fv.stream()) + .map(s -> s.collect(Collectors.joining(" "))) + .ifPresent(t -> values.add(this.metadataFieldMapping.toDCValue(this.metadata, t))); + return values; + } + + public String getTag() { + return tag; + } + + public void setTag(String tag) { + this.tag = tag; + } + + public MetadataFieldConfig getMetadata() { + return metadata; + } + + public void setMetadata(MetadataFieldConfig metadata) { + this.metadata = metadata; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleRisToMetadataContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleRisToMetadataContributor.java new file mode 100644 index 000000000000..36ea0dd47839 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleRisToMetadataContributor.java @@ -0,0 +1,71 @@ +/** + * 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.importer.external.metadatamapping.contributor; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.dspace.importer.external.metadatamapping.MetadataFieldConfig; +import org.dspace.importer.external.metadatamapping.MetadataFieldMapping; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; + +/** + * Metadata contributor that takes a record defined as Map> + * and turns it into metadatums configured in fieldToMetadata + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.it) + */ +public class SimpleRisToMetadataContributor implements MetadataContributor>> { + + protected Map fieldToMetadata; + + protected MetadataFieldMapping>, + MetadataContributor>>> metadataFieldMapping; + + public SimpleRisToMetadataContributor() {} + + public SimpleRisToMetadataContributor(Map fieldToMetadata) { + this.fieldToMetadata = fieldToMetadata; + } + + @Override + public Collection contributeMetadata(Map> record) { + List values = new LinkedList<>(); + for (String field : fieldToMetadata.keySet()) { + List fieldValues = record.get(field); + if (Objects.nonNull(fieldValues)) { + for (String value : fieldValues) { + values.add(metadataFieldMapping.toDCValue(fieldToMetadata.get(field), value)); + } + } + } + return values; + } + + public Map getFieldToMetadata() { + return fieldToMetadata; + } + + public void setFieldToMetadata(Map fieldToMetadata) { + this.fieldToMetadata = fieldToMetadata; + } + + public MetadataFieldMapping>, + MetadataContributor>>> getMetadataFieldMapping() { + return metadataFieldMapping; + } + + public void setMetadataFieldMapping(MetadataFieldMapping>, + MetadataContributor>>> metadataFieldMapping) { + this.metadataFieldMapping = metadataFieldMapping; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/scielo/service/ScieloFieldMapping.java b/dspace-api/src/main/java/org/dspace/importer/external/scielo/service/ScieloFieldMapping.java new file mode 100644 index 000000000000..0d7183a1f058 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/scielo/service/ScieloFieldMapping.java @@ -0,0 +1,37 @@ +/** + * 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.importer.external.scielo.service; +import java.util.Map; +import javax.annotation.Resource; + +import org.dspace.importer.external.metadatamapping.AbstractMetadataFieldMapping; + +/** + * An implementation of {@link AbstractMetadataFieldMapping} + * Responsible for defining the mapping of the Scielo metadatum fields on the DSpace metadatum fields + * + * @author Boychuk Mykhaylo (boychuk.mykhaylo at 4science dot it) + */ +@SuppressWarnings("rawtypes") +public class ScieloFieldMapping extends AbstractMetadataFieldMapping { + + /** + * Defines which metadatum is mapped on which metadatum. Note that while the key must be unique it + * only matters here for postprocessing of the value. The mapped MetadatumContributor has full control over + * what metadatafield is generated. + * + * @param metadataFieldMap The map containing the link between retrieve metadata and + * metadata that will be set to the item. + */ + @Override + @SuppressWarnings("unchecked") + @Resource(name = "scieloMetadataFieldMap") + public void setMetadataFieldMap(Map metadataFieldMap) { + super.setMetadataFieldMap(metadataFieldMap); + } +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/scielo/service/ScieloImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/scielo/service/ScieloImportMetadataSourceServiceImpl.java new file mode 100644 index 000000000000..2ebe520fde18 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/scielo/service/ScieloImportMetadataSourceServiceImpl.java @@ -0,0 +1,230 @@ +/** + * 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.importer.external.scielo.service; + +import java.io.BufferedReader; +import java.io.StringReader; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.Callable; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.el.MethodNotFoundException; +import javax.ws.rs.BadRequestException; + +import org.apache.http.client.utils.URIBuilder; +import org.dspace.content.Item; +import org.dspace.importer.external.datamodel.ImportRecord; +import org.dspace.importer.external.datamodel.Query; +import org.dspace.importer.external.exception.FileSourceException; +import org.dspace.importer.external.exception.MetadataSourceException; +import org.dspace.importer.external.scopus.service.LiveImportClient; +import org.dspace.importer.external.service.AbstractImportMetadataSourceService; +import org.dspace.importer.external.service.components.QuerySource; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Implements a data source for querying Scielo + * + * @author Boychuk Mykhaylo (boychuk.mykhaylo at 4Science dot it) + */ +public class ScieloImportMetadataSourceServiceImpl extends AbstractImportMetadataSourceService>> + implements QuerySource { + + private static final String ENDPOINT_SEARCH_SCIELO = "https://search.scielo.org/?output=ris&q="; + + private static final String PATTERN = "^([A-Z][A-Z0-9]) - (.*)$"; + + private static final String ID_PATTERN = "^(.....)-(.*)-(...)$"; + + private int timeout = 1000; + + @Autowired + private LiveImportClient liveImportClient; + + @Override + public void init() throws Exception {} + + @Override + public String getImportSource() { + return "scielo"; + } + + @Override + public Collection getRecords(String query, int start, int count) throws MetadataSourceException { + return retry(new SearchByQueryCallable(query, count, start)); + } + + @Override + public Collection getRecords(Query query) throws MetadataSourceException { + return retry(new SearchByQueryCallable(query)); + } + + + @Override + public ImportRecord getRecord(Query query) throws MetadataSourceException { + List records = retry(new SearchByQueryCallable(query)); + return records == null || records.isEmpty() ? null : records.get(0); + } + + @Override + public ImportRecord getRecord(String id) throws MetadataSourceException { + List records = retry(new FindByIdCallable(id)); + return records == null || records.isEmpty() ? null : records.get(0); + } + + @Override + public int getRecordsCount(String query) throws MetadataSourceException { + return retry(new SearchNBByQueryCallable(query)); + } + + @Override + public int getRecordsCount(Query query) throws MetadataSourceException { + throw new MethodNotFoundException("This method is not implemented for Scielo"); + } + + @Override + public Collection findMatchingRecords(Item item) throws MetadataSourceException { + throw new MethodNotFoundException("This method is not implemented for Scielo"); + } + + @Override + public Collection findMatchingRecords(Query query) throws MetadataSourceException { + throw new MethodNotFoundException("This method is not implemented for Scielo"); + } + + private class SearchNBByQueryCallable implements Callable { + + private String query; + + private SearchNBByQueryCallable(String queryString) { + this.query = queryString; + } + + private SearchNBByQueryCallable(Query query) { + this.query = query.getParameterAsClass("query", String.class); + } + + @Override + public Integer call() throws Exception { + String url = ENDPOINT_SEARCH_SCIELO + URLEncoder.encode(query, StandardCharsets.UTF_8); + String resp = liveImportClient.executeHttpGetRequest(timeout, url, new HashMap()); + Map>> records = getRecords(resp); + return Objects.nonNull(records.size()) ? records.size() : 0; + } + } + + private class FindByIdCallable implements Callable> { + + private String id; + + private FindByIdCallable(String id) { + this.id = id; + } + + @Override + public List call() throws Exception { + List results = new ArrayList<>(); + String scieloId = id.trim(); + Pattern risPattern = Pattern.compile(ID_PATTERN); + Matcher risMatcher = risPattern.matcher(scieloId); + if (risMatcher.matches()) { + String url = ENDPOINT_SEARCH_SCIELO + URLEncoder.encode(scieloId, StandardCharsets.UTF_8); + String resp = liveImportClient.executeHttpGetRequest(timeout, url, new HashMap()); + Map>> records = getRecords(resp); + if (Objects.nonNull(records) & !records.isEmpty()) { + results.add(transformSourceRecords(records.get(1))); + } + } else { + throw new BadRequestException("id provided : " + scieloId + " is not an ScieloID"); + } + return results; + } + } + + private class SearchByQueryCallable implements Callable> { + + private Query query; + + private SearchByQueryCallable(String queryString, Integer maxResult, Integer start) { + query = new Query(); + query.addParameter("query", queryString); + query.addParameter("start", start); + query.addParameter("count", maxResult); + } + + private SearchByQueryCallable(Query query) { + this.query = query; + } + + @Override + public List call() throws Exception { + List results = new ArrayList<>(); + String q = query.getParameterAsClass("query", String.class); + Integer count = query.getParameterAsClass("count", Integer.class); + Integer start = query.getParameterAsClass("start", Integer.class); + URIBuilder uriBuilder = new URIBuilder( + ENDPOINT_SEARCH_SCIELO + URLEncoder.encode(q, StandardCharsets.UTF_8)); + uriBuilder.addParameter("start", start.toString()); + uriBuilder.addParameter("count", count.toString()); + String resp = liveImportClient.executeHttpGetRequest(timeout, uriBuilder.toString(), + new HashMap()); + Map>> records = getRecords(resp); + for (int record : records.keySet()) { + results.add(transformSourceRecords(records.get(record))); + } + return results; + } + } + + private Map>> getRecords(String resp) throws FileSourceException { + Map>> records = new HashMap>>(); + BufferedReader reader; + int countRecord = 0; + try { + reader = new BufferedReader(new StringReader(resp)); + String line; + while ((line = reader.readLine()) != null) { + if (line.isEmpty() || line.equals("") || line.matches("^\\s*$")) { + continue; + } + line = line.replaceAll("\\uFEFF", "").trim(); + Pattern risPattern = Pattern.compile(PATTERN); + Matcher risMatcher = risPattern.matcher(line); + if (risMatcher.matches()) { + if (risMatcher.group(1).equals("TY") & risMatcher.group(2).equals("JOUR")) { + countRecord ++; + Map> newMap = new HashMap>(); + records.put(countRecord, newMap); + } else { + Map> tag2values = records.get(countRecord); + List values = tag2values.get(risMatcher.group(1)); + if (Objects.isNull(values)) { + List newValues = new ArrayList(); + newValues.add(risMatcher.group(2)); + tag2values.put(risMatcher.group(1), newValues); + } else { + values.add(risMatcher.group(2)); + tag2values.put(risMatcher.group(1), values); + } + } + } + } + } catch (Exception e) { + throw new FileSourceException("Cannot parse RIS file", e); + } + return records; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml b/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml index 5e69ee9c4282..7686986ae813 100644 --- a/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml +++ b/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml @@ -116,6 +116,11 @@ + + + + + diff --git a/dspace/config/spring/api/external-services.xml b/dspace/config/spring/api/external-services.xml index 7f1295f839df..2259ccb9f9c6 100644 --- a/dspace/config/spring/api/external-services.xml +++ b/dspace/config/spring/api/external-services.xml @@ -94,5 +94,15 @@ - + + + + + + + Publication + + + + \ No newline at end of file From dea0564201313487f2db7215ba7bd2fd7bd922f4 Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Mon, 4 Apr 2022 15:03:35 +0200 Subject: [PATCH 0102/1846] [CST-5303] added tests for Scielo live import service --- .../assetstore/sciello-single-record.txt | 24 ++ .../dspaceFolder/assetstore/scielo-test.txt | 51 ++++ .../ScieloImportMetadataSourceServiceIT.java | 241 ++++++++++++++++++ .../src/test/resources/test-config.properties | 3 + .../config/spring/api/scielo-integration.xml | 109 ++++++++ 5 files changed, 428 insertions(+) create mode 100644 dspace-api/src/test/data/dspaceFolder/assetstore/sciello-single-record.txt create mode 100644 dspace-api/src/test/data/dspaceFolder/assetstore/scielo-test.txt create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/ScieloImportMetadataSourceServiceIT.java create mode 100644 dspace/config/spring/api/scielo-integration.xml diff --git a/dspace-api/src/test/data/dspaceFolder/assetstore/sciello-single-record.txt b/dspace-api/src/test/data/dspaceFolder/assetstore/sciello-single-record.txt new file mode 100644 index 000000000000..bd9934d2bcef --- /dev/null +++ b/dspace-api/src/test/data/dspaceFolder/assetstore/sciello-single-record.txt @@ -0,0 +1,24 @@ +TY - JOUR +AU - Torres Marzo, Ricardo +TI - Requena Jiménez, Miguel, Los espacios de la muerte en Roma, Madrid, Síntesis, 2021, 365 págs. más bibliografía en línea, ISBN 978-84-135759-6-4. +JO - Nova tellus +J2 - Nova tellus +SN - 0185-3058 +VL - 39 +IS - 2 +DO - 10.19130/iifl.nt.2021.39.2.901 +DB - SciELO México +DP - http://www.scielo.org/ +ID - S0185-30582021000200231-mex +LA - es +SP - 231 +EP - 236 +DA - 2021-12 +PY - 2021 +UR - http://www.scielo.org.mx/scielo.php?script=sci_arttext&pid=S0185-30582021000200231&lang=pt +KW - Roma +KW - Historia +KW - ritos funerarios +KW - inframundo +KW - epitafios +ER - \ No newline at end of file diff --git a/dspace-api/src/test/data/dspaceFolder/assetstore/scielo-test.txt b/dspace-api/src/test/data/dspaceFolder/assetstore/scielo-test.txt new file mode 100644 index 000000000000..4cc9d3ad366c --- /dev/null +++ b/dspace-api/src/test/data/dspaceFolder/assetstore/scielo-test.txt @@ -0,0 +1,51 @@ +TY - JOUR +AU - Torres Marzo, Ricardo +TI - Requena Jiménez, Miguel, Los espacios de la muerte en Roma, Madrid, Síntesis, 2021, 365 págs. más bibliografía en línea, ISBN 978-84-135759-6-4. +JO - Nova tellus +J2 - Nova tellus +SN - 0185-3058 +VL - 39 +IS - 2 +DO - 10.19130/iifl.nt.2021.39.2.901 +DB - SciELO México +DP - http://www.scielo.org/ +ID - S0185-30582021000200231-mex +LA - es +SP - 231 +EP - 236 +DA - 2021-12 +PY - 2021 +UR - http://www.scielo.org.mx/scielo.php?script=sci_arttext&pid=S0185-30582021000200231&lang=pt +KW - Roma +KW - Historia +KW - ritos funerarios +KW - inframundo +KW - epitafios +ER - + +TY - JOUR +AU - MAGRI, GEO +TI - Rinegoziazione e revisione del contratto. Tribunale di Roma, Sez. VI, 27 agosto 2020 +JO - Revista de Derecho Privado +J2 - Rev. Derecho Privado +SN - 0123-4366 +VL - +IS - 41 +DO - 10.18601/01234366.n41.14 +DB - SciELO Colômbia +DP - http://www.scielo.org/ +ID - S0123-43662021000200397-col +LA - it +SP - 397 +EP - 418 +DA - 2021-12 +PY - 2021 +AB - ABSTRACT: The Tribunal of Rome imposes an obligation to renegotiate long-term contracts, the balance of which has been modified by the covro pandemic. The decision establishes a general obligation for the parties to execute the contract in good faith and gives the judge the possibility of a judicial review. This is a long-awaited decision in doctrine which complies with the indications of the Supreme Court of Cassation expressed in its memorandum 56/2020. +UR - http://www.scielo.org.co/scielo.php?script=sci_arttext&pid=S0123-43662021000200397&lang=pt +L1 - http://www.scielo.org.co/pdf/rdp/n41/0123-4366-rdp-41-397.pdf +KW - sopravvenienza contrattuale +KW - covro +KW - buona fede in senso oggettivo +KW - obbligo di rinegoziare +KW - revisione del contratto +ER - \ No newline at end of file diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScieloImportMetadataSourceServiceIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScieloImportMetadataSourceServiceIT.java new file mode 100644 index 000000000000..c86c6cfd72c7 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScieloImportMetadataSourceServiceIT.java @@ -0,0 +1,241 @@ +/** + * 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.rest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +import java.io.FileInputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import javax.el.MethodNotFoundException; + +import org.apache.commons.io.IOUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.impl.client.CloseableHttpClient; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.content.Item; +import org.dspace.importer.external.datamodel.ImportRecord; +import org.dspace.importer.external.datamodel.Query; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; +import org.dspace.importer.external.scielo.service.ScieloImportMetadataSourceServiceImpl; +import org.dspace.importer.external.scopus.service.LiveImportClientImpl; +import org.junit.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Integration tests for {@link ScieloImportMetadataSourceServiceImpl} + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com) + */ +public class ScieloImportMetadataSourceServiceIT extends AbstractLiveImportIntegrationTest { + + @Autowired + private LiveImportClientImpl liveImportClientImpl; + + @Autowired + private ScieloImportMetadataSourceServiceImpl scieloServiceImpl; + + @Test + public void scieloImportMetadataGetRecordsTest() throws Exception { + context.turnOffAuthorisationSystem(); + CloseableHttpClient originalHttpClient = liveImportClientImpl.getHttpClient(); + CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + String path = testProps.get("test.scielo").toString(); + try (FileInputStream scieloResp = new FileInputStream(path)) { + + String scieloRipResp = IOUtils.toString(scieloResp, Charset.defaultCharset()); + + liveImportClientImpl.setHttpClient(httpClient); + CloseableHttpResponse response = mockResponse(scieloRipResp, 200, "OK"); + when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response); + + context.restoreAuthSystemState(); + Collection collection2match = getRecords(); + Collection recordsImported = scieloServiceImpl.getRecords("test query", 0, 2); + assertEquals(2, recordsImported.size()); + assertTrue(matchRecords(recordsImported, collection2match)); + } finally { + liveImportClientImpl.setHttpClient(originalHttpClient); + } + } + + @Test + public void scieloImportMetadataGetRecordsCountTest() throws Exception { + context.turnOffAuthorisationSystem(); + CloseableHttpClient originalHttpClient = liveImportClientImpl.getHttpClient(); + CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + String path = testProps.get("test.scielo").toString(); + try (FileInputStream file = new FileInputStream(path)) { + String scieloResp = IOUtils.toString(file, Charset.defaultCharset()); + + liveImportClientImpl.setHttpClient(httpClient); + CloseableHttpResponse response = mockResponse(scieloResp, 200, "OK"); + when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response); + + context.restoreAuthSystemState(); + int tot = scieloServiceImpl.getRecordsCount("test query"); + assertEquals(2, tot); + } finally { + liveImportClientImpl.setHttpClient(originalHttpClient); + } + } + + @Test(expected = MethodNotFoundException.class) + public void scieloImportMetadataFindMatchingRecordsTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + org.dspace.content.Collection col1 = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection 1") + .build(); + + Item testItem = ItemBuilder.createItem(context, col1) + .withTitle("test item") + .withIssueDate("2021") + .build(); + context.restoreAuthSystemState(); + scieloServiceImpl.findMatchingRecords(testItem); + } + + @Test(expected = MethodNotFoundException.class) + public void scieloImportMetadataGetRecordsCountByQueryTest() throws Exception { + Query q = new Query(); + q.addParameter("query", "test query"); + scieloServiceImpl.getRecordsCount(q); + } + + @Test + public void scieloImportMetadataGetRecordsByIdTest() throws Exception { + context.turnOffAuthorisationSystem(); + CloseableHttpClient originalHttpClient = liveImportClientImpl.getHttpClient(); + CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + String path = testProps.get("test.sciello-single").toString(); + try (FileInputStream scieloResp = new FileInputStream(path)) { + + String scieloRipResp = IOUtils.toString(scieloResp, Charset.defaultCharset()); + + liveImportClientImpl.setHttpClient(httpClient); + CloseableHttpResponse response = mockResponse(scieloRipResp, 200, "OK"); + when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response); + + context.restoreAuthSystemState(); + Collection collection2match = getRecords(); + Collection firstRecord = Arrays.asList(collection2match.iterator().next()); + ImportRecord record = scieloServiceImpl.getRecord("S0185-30582021000200231-mex"); + assertNotNull(record); + Collection recordsImported = Arrays.asList(record); + assertTrue(matchRecords(recordsImported, firstRecord)); + } finally { + liveImportClientImpl.setHttpClient(originalHttpClient); + } + } + + private Collection getRecords() { + Collection records = new LinkedList(); + //define first record + List metadatums = new ArrayList(); + MetadatumDTO ispartof = createMetadatumDTO("dc", "relation", "ispartof", "Nova tellus"); + MetadatumDTO date = createMetadatumDTO("dc", "date", "issued", "2021"); + MetadatumDTO citation = createMetadatumDTO("oaire", "citation", "issue", "2"); + MetadatumDTO doi = createMetadatumDTO("dc", "identifier", "doi", "10.19130/iifl.nt.2021.39.2.901"); + MetadatumDTO endPage = createMetadatumDTO("oaire", "citation", "endPage", "236"); + MetadatumDTO subject = createMetadatumDTO("dc", "subject", null, "Roma"); + MetadatumDTO subject2 = createMetadatumDTO("dc", "subject", null, "Historia"); + MetadatumDTO subject3 = createMetadatumDTO("dc", "subject", null, "ritos funerarios"); + MetadatumDTO subject4 = createMetadatumDTO("dc", "subject", null, "inframundo"); + MetadatumDTO subject5 = createMetadatumDTO("dc", "subject", null, "epitafios"); + MetadatumDTO author = createMetadatumDTO("dc", "contributor", "author", "Torres Marzo, Ricardo"); + MetadatumDTO title = createMetadatumDTO("dc", "title", null, "Requena Jiménez, Miguel, Los espacios" + + " de la muerte en Roma, Madrid, Síntesis, 2021, 365 págs." + + " más bibliografía en línea, ISBN 978-84-135759-6-4."); + MetadatumDTO volume = createMetadatumDTO("oaire", "citation", "volume", "39"); + MetadatumDTO issn = createMetadatumDTO("dc", "identifier", "issn", "0185-3058"); + MetadatumDTO other = createMetadatumDTO("dc", "identifier", "other", "S0185-30582021000200231-mex"); + MetadatumDTO startPage = createMetadatumDTO("oaire", "citation", "startPage", "231"); + + metadatums.add(ispartof); + metadatums.add(date); + metadatums.add(citation); + metadatums.add(doi); + metadatums.add(endPage); + metadatums.add(subject); + metadatums.add(subject2); + metadatums.add(subject3); + metadatums.add(subject4); + metadatums.add(subject5); + metadatums.add(author); + metadatums.add(title); + metadatums.add(volume); + metadatums.add(issn); + metadatums.add(other); + metadatums.add(startPage); + + ImportRecord firstrRecord = new ImportRecord(metadatums); + + //define second record + List metadatums2 = new ArrayList(); + MetadatumDTO ispartof2 = createMetadatumDTO("dc", "relation", "ispartof", "Revista de Derecho Privado"); + MetadatumDTO date2 = createMetadatumDTO("dc", "date", "issued", "2021"); + MetadatumDTO citation2 = createMetadatumDTO("oaire", "citation", "issue", "41"); + MetadatumDTO doi2 = createMetadatumDTO("dc", "identifier", "doi", "10.18601/01234366.n41.14"); + MetadatumDTO endPage2 = createMetadatumDTO("oaire", "citation", "endPage", "418"); + MetadatumDTO subject6 = createMetadatumDTO("dc", "subject", null, "sopravvenienza contrattuale"); + MetadatumDTO subject7 = createMetadatumDTO("dc", "subject", null, "covro"); + MetadatumDTO subject8 = createMetadatumDTO("dc", "subject", null, "buona fede in senso oggettivo"); + MetadatumDTO subject9 = createMetadatumDTO("dc", "subject", null, "obbligo di rinegoziare"); + MetadatumDTO subject10 = createMetadatumDTO("dc", "subject", null, "revisione del contratto"); + MetadatumDTO author2 = createMetadatumDTO("dc", "contributor", "author", "MAGRI, GEO"); + MetadatumDTO title2 = createMetadatumDTO("dc", "title", null, + "Rinegoziazione e revisione del contratto. Tribunale di Roma, Sez. VI, 27 agosto 2020"); + MetadatumDTO issn2 = createMetadatumDTO("dc", "identifier", "issn", "0123-4366"); + MetadatumDTO other2 = createMetadatumDTO("dc", "identifier", "other", "S0123-43662021000200397-col"); + MetadatumDTO startPage2 = createMetadatumDTO("oaire", "citation", "startPage", "397"); + MetadatumDTO description = createMetadatumDTO("dc", "description", "abstract", + "ABSTRACT: The Tribunal of Rome imposes an obligation to renegotiate long-term contracts," + + " the balance of which has been modified by the covro pandemic. The decision establishes a" + + " general obligation for the parties to execute the contract in good faith and gives the judge" + + " the possibility of a judicial review. This is a long-awaited decision in doctrine which complies" + + " with the indications of the Supreme Court of Cassation expressed in its memorandum 56/2020."); + + metadatums2.add(ispartof2); + metadatums2.add(date2); + metadatums2.add(citation2); + metadatums2.add(doi2); + metadatums2.add(endPage2); + metadatums2.add(subject6); + metadatums2.add(subject7); + metadatums2.add(subject8); + metadatums2.add(subject9); + metadatums2.add(subject10); + metadatums2.add(author2); + metadatums2.add(title2); + metadatums2.add(issn2); + metadatums2.add(other2); + metadatums2.add(startPage2); + metadatums2.add(description); + + ImportRecord secondRecord = new ImportRecord(metadatums2); + records.add(firstrRecord); + records.add(secondRecord); + return records; + } + +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/resources/test-config.properties b/dspace-server-webapp/src/test/resources/test-config.properties index 3af96b20fcf4..a2a5160d9c40 100644 --- a/dspace-server-webapp/src/test/resources/test-config.properties +++ b/dspace-server-webapp/src/test/resources/test-config.properties @@ -14,3 +14,6 @@ test.bitstream = ./target/testing/dspace/assetstore/ConstitutionofIreland.pdf #Path for a test Taskfile for the curate script test.curateTaskFile = ./target/testing/dspace/assetstore/curate.txt + +test.scielo = ./target/testing/dspace/assetstore/scielo-test.txt +test.sciello-single = ./target/testing/dspace/assetstore/sciello-single-record.txt \ No newline at end of file diff --git a/dspace/config/spring/api/scielo-integration.xml b/dspace/config/spring/api/scielo-integration.xml new file mode 100644 index 000000000000..05ee62c9c199 --- /dev/null +++ b/dspace/config/spring/api/scielo-integration.xml @@ -0,0 +1,109 @@ + + + + + + + Defines which metadatum is mapped on which metadatum. Note that while the key must be unique it + only matters here for postprocessing of the value. The mapped MetadatumContributor has full control over + what metadatafield is generated. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From fb31905ed5f7bb20f014d142395cef8f3ef11ee4 Mon Sep 17 00:00:00 2001 From: eskander Date: Mon, 4 Apr 2022 15:58:06 +0200 Subject: [PATCH 0103/1846] [CST-5534] canSynchronizeWithORCID Authorization feature. --- .../impl/CanSynchronizeWithORCID.java | 73 ++++++++ .../CanSynchronizeWithORCIDIT.java | 162 ++++++++++++++++++ 2 files changed, 235 insertions(+) create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanSynchronizeWithORCID.java create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanSynchronizeWithORCIDIT.java diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanSynchronizeWithORCID.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanSynchronizeWithORCID.java new file mode 100644 index 000000000000..f719265a0b36 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanSynchronizeWithORCID.java @@ -0,0 +1,73 @@ +/** + * 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.rest.authorization.impl; + +import java.sql.SQLException; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.function.Predicate; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.app.rest.authorization.AuthorizationFeature; +import org.dspace.app.rest.authorization.AuthorizationFeatureDocumentation; +import org.dspace.app.rest.model.BaseObjectRest; +import org.dspace.app.rest.model.ItemRest; +import org.dspace.content.Item; +import org.dspace.content.MetadataValue; +import org.dspace.content.service.ItemService; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * The synchronization with ORCID feature. It can be used to verify + * if the user can synchronize with ORCID. + * + * @author Mohamed Eskander (mohamed.eskander at 4science.it) + */ +@Component +@AuthorizationFeatureDocumentation(name = CanSynchronizeWithORCID.NAME, + description = "It can be used to verify if the user can synchronize with ORCID") +public class CanSynchronizeWithORCID implements AuthorizationFeature { + + public static final String NAME = "canSynchronizeWithORCID"; + + @Autowired + private ItemService itemService; + + @Override + public boolean isAuthorized(Context context, BaseObjectRest object) throws SQLException { + + EPerson ePerson = context.getCurrentUser(); + + if (!(object instanceof ItemRest) || Objects.isNull(ePerson)) { + return false; + } + + String id = ((ItemRest) object).getId(); + Item item = itemService.find(context, UUID.fromString(id)); + + return isDspaceObjectOwner(ePerson, item); + } + + @Override + public String[] getSupportedTypes() { + return new String[] { ItemRest.CATEGORY + "." + ItemRest.NAME }; + } + + private boolean isDspaceObjectOwner(EPerson eperson, Item item) { + if (eperson == null) { + return false; + } + List owners = itemService.getMetadataByMetadataString(item, "dspace.object.owner"); + Predicate checkOwner = v -> StringUtils.equals(v.getAuthority(), eperson.getID().toString()); + return owners.stream().anyMatch(checkOwner); + } +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanSynchronizeWithORCIDIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanSynchronizeWithORCIDIT.java new file mode 100644 index 000000000000..a9e7350a679f --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanSynchronizeWithORCIDIT.java @@ -0,0 +1,162 @@ +/** + * 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.rest.authorization; + +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.dspace.app.rest.authorization.impl.CanSynchronizeWithORCID; +import org.dspace.app.rest.converter.ItemConverter; +import org.dspace.app.rest.model.ItemRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.app.rest.utils.Utils; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.EPersonBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.Item; +import org.dspace.eperson.EPerson; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Test for the canSynchronizeWithORCID authorization feature. + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public class CanSynchronizeWithORCIDIT extends AbstractControllerIntegrationTest { + + @Autowired + private Utils utils; + + @Autowired + private ItemConverter itemConverter; + + @Autowired + private AuthorizationFeatureService authorizationFeatureService; + + final String feature = "canSynchronizeWithORCID"; + private Item itemA; + private ItemRest itemARest; + private Community communityA; + private Collection collectionA; + private AuthorizationFeature canSynchronizeWithORCID; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + + context.turnOffAuthorisationSystem(); + + canSynchronizeWithORCID = authorizationFeatureService.find(CanSynchronizeWithORCID.NAME); + + communityA = CommunityBuilder.createCommunity(context) + .withName("communityA").build(); + + collectionA = CollectionBuilder.createCollection(context, communityA) + .withName("collectionA").build(); + + itemA = ItemBuilder.createItem(context, collectionA) + .withTitle("itemA") + .withDspaceObjectOwner("user" , context.getCurrentUser().getID().toString()) + .build(); + + context.restoreAuthSystemState(); + + itemARest = itemConverter.convert(itemA, Projection.DEFAULT); + } + + @Test + public void anonymousHasNotAccessTest() throws Exception { + getClient().perform(get("/api/authz/authorizations/search/object") + .param("embed", "feature") + .param("feature", feature) + .param("uri", utils.linkToSingleResource(itemARest, "self").getHref())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(0))) + .andExpect(jsonPath("$._embedded").doesNotExist()); + } + + @Test + public void testCanSynchronizeWithORCIDIfItemDoesNotHasDspaceObjectOwner() throws Exception { + + EPerson user = context.getCurrentUser(); + + context.turnOffAuthorisationSystem(); + + Item item = ItemBuilder.createItem(context, collectionA) + .withTitle("item") + .build(); + + context.restoreAuthSystemState(); + + String token = getAuthToken(user.getEmail(), password); + + getClient(token).perform(get("/api/authz/authorizations/search/object") + .param("uri", uri(item)) + .param("eperson", user.getID().toString()) + .param("feature", canSynchronizeWithORCID.getName())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(0))) + .andExpect(jsonPath("$._embedded").doesNotExist()); + } + + @Test + public void testCanSynchronizeWithORCIDIfItemHasDspaceObjectOwnerOfAnotherUUID() throws Exception { + + context.turnOffAuthorisationSystem(); + + EPerson anotherUser = EPersonBuilder.createEPerson(context) + .withEmail("user@example.com") + .withPassword(password) + .build(); + + context.restoreAuthSystemState(); + + String token = getAuthToken(anotherUser.getEmail(), password); + + getClient(token).perform(get("/api/authz/authorizations/search/object") + .param("uri", uri(itemA)) + .param("eperson", anotherUser.getID().toString()) + .param("feature", canSynchronizeWithORCID.getName())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(0))) + .andExpect(jsonPath("$._embedded").doesNotExist()); + } + + @Test + public void testCanSynchronizeWithORCIDIfItemHasDspaceObjectOwner() throws Exception { + + EPerson user = context.getCurrentUser(); + + String token = getAuthToken(user.getEmail(), password); + + getClient(token).perform(get("/api/authz/authorizations/search/object") + .param("uri", uri(itemA)) + .param("eperson", user.getID().toString()) + .param("feature", canSynchronizeWithORCID.getName())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded").exists()) + .andExpect(jsonPath("$.page.totalElements", greaterThanOrEqualTo(1))); + } + + private String uri(Item item) { + ItemRest itemRest = itemConverter.convert(item, Projection.DEFAULT); + String itemRestURI = utils.linkToSingleResource(itemRest, "self").getHref(); + return itemRestURI; + } + +} From 2bb9883b888455ad21e5e3632dd1538ad412a1dd Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Fri, 25 Mar 2022 15:42:17 +0100 Subject: [PATCH 0104/1846] [CST-5303] implemented http live import client --- .../scopus/service/LiveImportClient.java | 22 +++++ .../scopus/service/LiveImportClientImpl.java | 94 +++++++++++++++++++ .../config/spring/api/external-services.xml | 2 + 3 files changed, 118 insertions(+) create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/scopus/service/LiveImportClient.java create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/scopus/service/LiveImportClientImpl.java diff --git a/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/LiveImportClient.java b/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/LiveImportClient.java new file mode 100644 index 000000000000..50006fd486cd --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/LiveImportClient.java @@ -0,0 +1,22 @@ +/** + * 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.importer.external.scopus.service; + +import java.io.InputStream; +import java.util.Map; + +/** + * Interface for classes that allow to contact LiveImport clients. + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com) + */ +public interface LiveImportClient { + + public InputStream executeHttpGetRequest(int timeout, String URL, Map requestParams); + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/LiveImportClientImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/LiveImportClientImpl.java new file mode 100644 index 000000000000..f11e2fc4f207 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/LiveImportClientImpl.java @@ -0,0 +1,94 @@ +/** + * 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.importer.external.scopus.service; + +import java.io.InputStream; +import java.net.URISyntaxException; +import java.util.Map; +import java.util.Objects; + +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpHost; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.config.RequestConfig.Builder; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.DefaultProxyRoutePlanner; +import org.apache.log4j.Logger; +import org.dspace.services.ConfigurationService; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Implementation of {@link LiveImportClient}. + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science dot com) + */ +public class LiveImportClientImpl implements LiveImportClient { + + private static final Logger log = Logger.getLogger(LiveImportClientImpl.class); + + @Autowired + private ConfigurationService configurationService; + + @Override + public InputStream executeHttpGetRequest(int timeout, String URL, Map requestParams) { + HttpGet method = null; + String proxyHost = configurationService.getProperty("http.proxy.host"); + String proxyPort = configurationService.getProperty("http.proxy.port"); + try { + HttpClientBuilder hcBuilder = HttpClients.custom(); + Builder requestConfigBuilder = RequestConfig.custom(); + requestConfigBuilder.setConnectionRequestTimeout(timeout); + + if (StringUtils.isNotBlank(proxyHost) && StringUtils.isNotBlank(proxyPort)) { + HttpHost proxy = new HttpHost(proxyHost, Integer.parseInt(proxyPort), "http"); + DefaultProxyRoutePlanner routePlanner = new DefaultProxyRoutePlanner(proxy); + hcBuilder.setRoutePlanner(routePlanner); + } + + method = new HttpGet(getSearchUrl(URL, requestParams)); + method.setConfig(requestConfigBuilder.build()); + + HttpClient client = hcBuilder.build(); + HttpResponse httpResponse = client.execute(method); + if (isNotSuccessfull(httpResponse)) { + throw new RuntimeException(); + } + return httpResponse.getEntity().getContent(); + } catch (Exception e1) { + log.error(e1.getMessage(), e1); + } finally { + if (Objects.nonNull(method)) { + method.releaseConnection(); + } + } + return null; + } + + private String getSearchUrl(String URL, Map requestParams) throws URISyntaxException { + URIBuilder uriBuilder = new URIBuilder(URL); + for (String param : requestParams.keySet()) { + uriBuilder.setParameter(param, requestParams.get(param)); + } + return uriBuilder.toString(); + } + + private boolean isNotSuccessfull(HttpResponse response) { + int statusCode = getStatusCode(response); + return statusCode < 200 || statusCode > 299; + } + + private int getStatusCode(HttpResponse response) { + return response.getStatusLine().getStatusCode(); + } + +} \ No newline at end of file diff --git a/dspace/config/spring/api/external-services.xml b/dspace/config/spring/api/external-services.xml index 9e28e5d55985..7f1295f839df 100644 --- a/dspace/config/spring/api/external-services.xml +++ b/dspace/config/spring/api/external-services.xml @@ -5,6 +5,8 @@ + + From d60567617bff6555472516e80eb286f50322b4c6 Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Tue, 29 Mar 2022 16:00:02 +0200 Subject: [PATCH 0105/1846] [CST-5303] porting of scopus live import services --- .../main/java/org/dspace/core/Constants.java | 6 + .../AuthorMetadataContributor.java | 148 +++++++ .../PageRangeXPathMetadataContributor.java | 98 +++++ ...laceCharacterXPathMetadataContributor.java | 66 +++ .../ReplaceFieldXPathMetadataContributor.java | 73 ++++ .../SimpleXpathMetadatumContributor.java | 6 +- .../scopus/service/LiveImportClient.java | 3 +- .../scopus/service/LiveImportClientImpl.java | 56 ++- .../scopus/service/ScopusFieldMapping.java | 38 ++ ...ScopusImportMetadataSourceServiceImpl.java | 388 ++++++++++++++++++ .../importer/external/service/DoiCheck.java | 47 +++ .../submit/lookup/MapConverterModifier.java | 115 ++++++ .../org/dspace/util/SimpleMapConverter.java | 45 ++ .../spring-dspace-addon-import-services.xml | 8 + .../config/spring/api/external-services.xml | 13 + .../mapConverter-openAccesFlag.properties | 1 + dspace/config/dspace.cfg | 13 + dspace/config/spring/api/crosswalks.xml | 13 + .../config/spring/api/external-services.xml | 11 + .../config/spring/api/scopus-integration.xml | 338 +++++++++++++++ 20 files changed, 1462 insertions(+), 24 deletions(-) create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/AuthorMetadataContributor.java create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/PageRangeXPathMetadataContributor.java create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/ReplaceCharacterXPathMetadataContributor.java create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/ReplaceFieldXPathMetadataContributor.java create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/scopus/service/ScopusFieldMapping.java create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/scopus/service/ScopusImportMetadataSourceServiceImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/service/DoiCheck.java create mode 100644 dspace-api/src/main/java/org/dspace/submit/lookup/MapConverterModifier.java create mode 100644 dspace-api/src/main/java/org/dspace/util/SimpleMapConverter.java create mode 100644 dspace/config/crosswalks/mapConverter-openAccesFlag.properties create mode 100644 dspace/config/spring/api/crosswalks.xml create mode 100644 dspace/config/spring/api/scopus-integration.xml diff --git a/dspace-api/src/main/java/org/dspace/core/Constants.java b/dspace-api/src/main/java/org/dspace/core/Constants.java index f730ef6545f1..a46c9ad4034c 100644 --- a/dspace-api/src/main/java/org/dspace/core/Constants.java +++ b/dspace-api/src/main/java/org/dspace/core/Constants.java @@ -227,6 +227,12 @@ public class Constants { public static final String VIRTUAL_AUTHORITY_PREFIX = "virtual::"; + /** + * The value stored in nested metadata that were left empty to keep them in the + * same number than the parent leading metadata + */ + public static final String PLACEHOLDER_PARENT_METADATA_VALUE = "#PLACEHOLDER_PARENT_METADATA_VALUE#"; + /* * Label used by the special entity type assigned when no explicit assignment is defined */ diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/AuthorMetadataContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/AuthorMetadataContributor.java new file mode 100644 index 000000000000..009584e530aa --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/AuthorMetadataContributor.java @@ -0,0 +1,148 @@ +/** + * 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.importer.external.metadatamapping.contributor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.apache.commons.lang.StringUtils; +import org.dspace.core.Constants; +import org.dspace.importer.external.metadatamapping.MetadataFieldConfig; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; +import org.jaxen.JaxenException; +import org.jdom2.Element; +import org.jdom2.Namespace; + +/** + * @author Boychuk Mykhaylo (boychuk.mykhaylo at 4science dot it) + */ +public class AuthorMetadataContributor extends SimpleXpathMetadatumContributor { + + private static final Namespace NAMESPACE = Namespace.getNamespace("http://www.w3.org/2005/Atom"); + + private MetadataFieldConfig orcid; + private MetadataFieldConfig scopusId; + private MetadataFieldConfig authname; + private MetadataFieldConfig affiliation; + + private Map affId2affName = new HashMap(); + + @Override + public Collection contributeMetadata(Element element) { + List values = new LinkedList<>(); + List metadatums = null; + fillAffillation(element); + try { + List nodes = element.getChildren("author", NAMESPACE); + for (Element el : nodes) { + metadatums = getMetadataOfAuthors(el); + if (Objects.nonNull(metadatums)) { + for (MetadatumDTO metadatum : metadatums) { + values.add(metadatum); + } + } + } + } catch (JaxenException e) { + throw new RuntimeException(e); + } + return values; + } + + private List getMetadataOfAuthors(Element element) throws JaxenException { + List metadatums = new ArrayList(); + Element authname = element.getChild("authname", NAMESPACE); + Element scopusId = element.getChild("authid", NAMESPACE); + Element orcid = element.getChild("orcid", NAMESPACE); + Element afid = element.getChild("afid", NAMESPACE); + + metadatums.add(getMetadata(getElementValue(authname), this.authname)); + metadatums.add(getMetadata(getElementValue(scopusId), this.scopusId)); + metadatums.add(getMetadata(getElementValue(orcid), this.orcid)); + metadatums.add(getMetadata(StringUtils.isNotBlank(afid.getValue()) + ? this.affId2affName.get(afid.getValue()) + : null, this.affiliation)); + return metadatums; + } + + private String getElementValue(Element element) { + if (Objects.nonNull(element)) { + return element.getValue(); + } + return StringUtils.EMPTY; + } + + private MetadatumDTO getMetadata(String value, MetadataFieldConfig metadaConfig) { + MetadatumDTO metadata = new MetadatumDTO(); + if (StringUtils.isNotBlank(value)) { + metadata.setValue(value); + } else { + metadata.setValue(Constants.PLACEHOLDER_PARENT_METADATA_VALUE); + } + metadata.setElement(metadaConfig.getElement()); + metadata.setQualifier(metadaConfig.getQualifier()); + metadata.setSchema(metadaConfig.getSchema()); + return metadata; + } + + private void fillAffillation(Element element) { + try { + List nodes = element.getChildren("affiliation", NAMESPACE); + for (Element el : nodes) { + fillAffiliation2Name(el); + } + } catch (JaxenException e) { + throw new RuntimeException(e); + } + } + + private void fillAffiliation2Name(Element element) throws JaxenException { + Element affilationName = element.getChild("affilname", NAMESPACE); + Element affilationId = element.getChild("afid", NAMESPACE); + if (StringUtils.isNotBlank(affilationId.getValue()) | StringUtils.isNotBlank(affilationName.getValue())) { + affId2affName.put(affilationId.getValue(), affilationName.getValue()); + } + } + + public MetadataFieldConfig getAuthname() { + return authname; + } + + public void setAuthname(MetadataFieldConfig authname) { + this.authname = authname; + } + + public MetadataFieldConfig getOrcid() { + return orcid; + } + + public void setOrcid(MetadataFieldConfig orcid) { + this.orcid = orcid; + } + + public MetadataFieldConfig getScopusId() { + return scopusId; + } + + public void setScopusId(MetadataFieldConfig scopusId) { + this.scopusId = scopusId; + } + + public MetadataFieldConfig getAffiliation() { + return affiliation; + } + + public void setAffiliation(MetadataFieldConfig affiliation) { + this.affiliation = affiliation; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/PageRangeXPathMetadataContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/PageRangeXPathMetadataContributor.java new file mode 100644 index 000000000000..3b03e5b5b549 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/PageRangeXPathMetadataContributor.java @@ -0,0 +1,98 @@ +/** + * 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.importer.external.metadatamapping.contributor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.importer.external.metadatamapping.MetadataFieldConfig; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; +import org.jdom2.Element; +import org.jdom2.Namespace; + +/** + * @author Boychuk Mykhaylo (boychuk.mykhaylo at 4science.com) + */ +public class PageRangeXPathMetadataContributor extends SimpleXpathMetadatumContributor { + + private MetadataFieldConfig startPageMetadata; + + private MetadataFieldConfig endPageMetadata; + + @Override + public Collection contributeMetadata(Element el) { + List values = new LinkedList<>(); + List metadatums = null; + for (String ns : prefixToNamespaceMapping.keySet()) { + List nodes = el.getChildren(query, Namespace.getNamespace(ns)); + for (Element element : nodes) { + metadatums = getMetadatum(element.getValue()); + if (Objects.nonNull(metadatums)) { + for (MetadatumDTO metadatum : metadatums) { + values.add(metadatum); + } + } + } + } + return values; + } + + private List getMetadatum(String value) { + List metadatums = new ArrayList(); + if (StringUtils.isBlank(value)) { + return null; + } + String [] range = value.split("-"); + if (range.length == 2) { + metadatums.add(setStartPage(range)); + metadatums.add(setEndPage(range)); + } else if (range.length != 0) { + metadatums.add(setStartPage(range)); + } + return metadatums; + } + + private MetadatumDTO setEndPage(String[] range) { + MetadatumDTO endPage = new MetadatumDTO(); + endPage.setValue(range[1]); + endPage.setElement(endPageMetadata.getElement()); + endPage.setQualifier(endPageMetadata.getQualifier()); + endPage.setSchema(endPageMetadata.getSchema()); + return endPage; + } + + private MetadatumDTO setStartPage(String[] range) { + MetadatumDTO startPage = new MetadatumDTO(); + startPage.setValue(range[0]); + startPage.setElement(startPageMetadata.getElement()); + startPage.setQualifier(startPageMetadata.getQualifier()); + startPage.setSchema(startPageMetadata.getSchema()); + return startPage; + } + + public MetadataFieldConfig getStartPageMetadata() { + return startPageMetadata; + } + + public void setStartPageMetadata(MetadataFieldConfig startPageMetadata) { + this.startPageMetadata = startPageMetadata; + } + + public MetadataFieldConfig getEndPageMetadata() { + return endPageMetadata; + } + + public void setEndPageMetadata(MetadataFieldConfig endPageMetadata) { + this.endPageMetadata = endPageMetadata; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/ReplaceCharacterXPathMetadataContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/ReplaceCharacterXPathMetadataContributor.java new file mode 100644 index 000000000000..9fb92348be0d --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/ReplaceCharacterXPathMetadataContributor.java @@ -0,0 +1,66 @@ +/** + * 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.importer.external.metadatamapping.contributor; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; + +import org.dspace.importer.external.metadatamapping.MetadataFieldConfig; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; +import org.jdom2.Element; +import org.jdom2.Namespace; + +/** + * This contributor replace specific character in the metadata value. + * It is useful for some provider (e.g. Scopus) which use containing "/" character. + * Actually, "/" will never encode by framework in URL building. In the same ways, if we + * encode "/" -> %2F, it will be encoded by framework and become %252F. + * + * @author Boychuk Mykhaylo (boychuk.mykhaylo at 4science.com) + */ +public class ReplaceCharacterXPathMetadataContributor extends SimpleXpathMetadatumContributor { + + private char characterToBeReplaced; + + private char characterToReplaceWith; + + @Override + public Collection contributeMetadata(Element element) { + List values = new LinkedList<>(); + for (String ns : prefixToNamespaceMapping.keySet()) { + List nodes = element.getChildren(query, Namespace.getNamespace(ns)); + for (Element el : nodes) { + values.add(getMetadatum(field, el.getValue())); + } + } + return values; + } + + private MetadatumDTO getMetadatum(MetadataFieldConfig field, String value) { + MetadatumDTO dcValue = new MetadatumDTO(); + if (Objects.isNull(field)) { + return null; + } + dcValue.setValue(value == null ? null : value.replace(characterToBeReplaced, characterToReplaceWith)); + dcValue.setElement(field.getElement()); + dcValue.setQualifier(field.getQualifier()); + dcValue.setSchema(field.getSchema()); + return dcValue; + } + + public void setCharacterToBeReplaced(int characterToBeReplaced) { + this.characterToBeReplaced = (char)characterToBeReplaced; + } + + public void setCharacterToReplaceWith(int characterToReplaceWith) { + this.characterToReplaceWith = (char)characterToReplaceWith; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/ReplaceFieldXPathMetadataContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/ReplaceFieldXPathMetadataContributor.java new file mode 100644 index 000000000000..15df3c6979a1 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/ReplaceFieldXPathMetadataContributor.java @@ -0,0 +1,73 @@ +/** + * 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.importer.external.metadatamapping.contributor; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; + +import org.dspace.importer.external.metadatamapping.MetadataFieldConfig; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; +import org.dspace.util.SimpleMapConverter; +import org.jdom2.Element; +import org.jdom2.Namespace; + +/** + * This contributor replace metadata value + * if this matched in mapConverter-openAccesFlag.properties file + * + * @author Boychuk Mykhaylo (boychuk.mykhaylo at 4science.com) + */ +public class ReplaceFieldXPathMetadataContributor extends SimpleXpathMetadatumContributor { + + private static final String UNSPECIFIED = "Unspecified"; + + private SimpleMapConverter simpleMapConverter; + + @Override + public Collection contributeMetadata(Element element) { + List values = new LinkedList<>(); + MetadatumDTO metadatum = null; + for (String ns : prefixToNamespaceMapping.keySet()) { + List nodes = element.getChildren(query, Namespace.getNamespace(ns)); + for (Element el : nodes) { + metadatum = getMetadatum(field, el.getValue()); + if (Objects.nonNull(metadatum)) { + values.add(metadatum); + } + } + } + return values; + } + + private MetadatumDTO getMetadatum(MetadataFieldConfig field, String value) { + String convertedValue = simpleMapConverter.getValue(value); + if (UNSPECIFIED.equals(convertedValue)) { + return null; + } + MetadatumDTO dcValue = new MetadatumDTO(); + if (Objects.isNull(field)) { + return null; + } + dcValue.setValue(convertedValue); + dcValue.setElement(field.getElement()); + dcValue.setQualifier(field.getQualifier()); + dcValue.setSchema(field.getSchema()); + return dcValue; + } + + public SimpleMapConverter getSimpleMapConverter() { + return simpleMapConverter; + } + + public void setSimpleMapConverter(SimpleMapConverter simpleMapConverter) { + this.simpleMapConverter = simpleMapConverter; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumContributor.java index 65d6d6694758..10a4ddd4d826 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumContributor.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumContributor.java @@ -33,7 +33,7 @@ * @author Roeland Dillen (roeland at atmire dot com) */ public class SimpleXpathMetadatumContributor implements MetadataContributor { - private MetadataFieldConfig field; + protected MetadataFieldConfig field; private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(); @@ -79,7 +79,7 @@ public void setPrefixToNamespaceMapping(Map prefixToNamespaceMap this.prefixToNamespaceMapping = prefixToNamespaceMapping; } - private Map prefixToNamespaceMapping; + protected Map prefixToNamespaceMapping; /** * Initialize SimpleXpathMetadatumContributor with a query, prefixToNamespaceMapping and MetadataFieldConfig @@ -103,7 +103,7 @@ public SimpleXpathMetadatumContributor() { } - private String query; + protected String query; /** * Return the MetadataFieldConfig used while retrieving MetadatumDTO diff --git a/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/LiveImportClient.java b/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/LiveImportClient.java index 50006fd486cd..bbc69c2a3d7e 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/LiveImportClient.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/LiveImportClient.java @@ -7,7 +7,6 @@ */ package org.dspace.importer.external.scopus.service; -import java.io.InputStream; import java.util.Map; /** @@ -17,6 +16,6 @@ */ public interface LiveImportClient { - public InputStream executeHttpGetRequest(int timeout, String URL, Map requestParams); + public String executeHttpGetRequest(int timeout, String URL, Map requestParams); } \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/LiveImportClientImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/LiveImportClientImpl.java index f11e2fc4f207..d5503831d3e8 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/LiveImportClientImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/LiveImportClientImpl.java @@ -9,20 +9,21 @@ import java.io.InputStream; import java.net.URISyntaxException; +import java.nio.charset.Charset; import java.util.Map; import java.util.Objects; +import java.util.Optional; +import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpHost; import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.config.RequestConfig.Builder; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.utils.URIBuilder; -import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; -import org.apache.http.impl.conn.DefaultProxyRoutePlanner; import org.apache.log4j.Logger; import org.dspace.services.ConfigurationService; import org.springframework.beans.factory.annotation.Autowired; @@ -36,34 +37,32 @@ public class LiveImportClientImpl implements LiveImportClient { private static final Logger log = Logger.getLogger(LiveImportClientImpl.class); + private CloseableHttpClient httpClient; + @Autowired private ConfigurationService configurationService; @Override - public InputStream executeHttpGetRequest(int timeout, String URL, Map requestParams) { + public String executeHttpGetRequest(int timeout, String URL, Map requestParams) { HttpGet method = null; - String proxyHost = configurationService.getProperty("http.proxy.host"); - String proxyPort = configurationService.getProperty("http.proxy.port"); - try { - HttpClientBuilder hcBuilder = HttpClients.custom(); + try (CloseableHttpClient httpClient = Optional.ofNullable(this.httpClient) + .orElseGet(HttpClients::createDefault)) { + Builder requestConfigBuilder = RequestConfig.custom(); requestConfigBuilder.setConnectionRequestTimeout(timeout); - - if (StringUtils.isNotBlank(proxyHost) && StringUtils.isNotBlank(proxyPort)) { - HttpHost proxy = new HttpHost(proxyHost, Integer.parseInt(proxyPort), "http"); - DefaultProxyRoutePlanner routePlanner = new DefaultProxyRoutePlanner(proxy); - hcBuilder.setRoutePlanner(routePlanner); - } + RequestConfig defaultRequestConfig = requestConfigBuilder.build(); method = new HttpGet(getSearchUrl(URL, requestParams)); - method.setConfig(requestConfigBuilder.build()); + method.setConfig(defaultRequestConfig); - HttpClient client = hcBuilder.build(); - HttpResponse httpResponse = client.execute(method); + configureProxy(method, defaultRequestConfig); + + HttpResponse httpResponse = httpClient.execute(method); if (isNotSuccessfull(httpResponse)) { throw new RuntimeException(); } - return httpResponse.getEntity().getContent(); + InputStream inputStream = httpResponse.getEntity().getContent(); + return IOUtils.toString(inputStream, Charset.defaultCharset()); } catch (Exception e1) { log.error(e1.getMessage(), e1); } finally { @@ -71,7 +70,18 @@ public InputStream executeHttpGetRequest(int timeout, String URL, Map requestParams) throws URISyntaxException { @@ -91,4 +101,12 @@ private int getStatusCode(HttpResponse response) { return response.getStatusLine().getStatusCode(); } + public CloseableHttpClient getHttpClient() { + return httpClient; + } + + public void setHttpClient(CloseableHttpClient httpClient) { + this.httpClient = httpClient; + } + } \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/ScopusFieldMapping.java b/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/ScopusFieldMapping.java new file mode 100644 index 000000000000..c8143339b483 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/ScopusFieldMapping.java @@ -0,0 +1,38 @@ +/** + * 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.importer.external.scopus.service; + +import java.util.Map; +import javax.annotation.Resource; + +import org.dspace.importer.external.metadatamapping.AbstractMetadataFieldMapping; + + +/** + * An implementation of {@link AbstractMetadataFieldMapping} + * Responsible for defining the mapping of the Scopus metadatum fields on the DSpace metadatum fields + * + * @author Pasquale Cavallo (pasquale.cavallo at 4science dot it) + */ +public class ScopusFieldMapping extends AbstractMetadataFieldMapping { + + /** + * Defines which metadatum is mapped on which metadatum. Note that while the key must be unique it + * only matters here for postprocessing of the value. The mapped MetadatumContributor has full control over + * what metadatafield is generated. + * + * @param metadataFieldMap The map containing the link between retrieve metadata and metadata that will be set to + * the item. + */ + @Override + @Resource(name = "scopusMetadataFieldMap") + public void setMetadataFieldMap(Map metadataFieldMap) { + super.setMetadataFieldMap(metadataFieldMap); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/ScopusImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/ScopusImportMetadataSourceServiceImpl.java new file mode 100644 index 000000000000..3b16b1993c84 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/ScopusImportMetadataSourceServiceImpl.java @@ -0,0 +1,388 @@ +/** + * 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.importer.external.scopus.service; + +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.Callable; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.el.MethodNotFoundException; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.content.Item; +import org.dspace.importer.external.datamodel.ImportRecord; +import org.dspace.importer.external.datamodel.Query; +import org.dspace.importer.external.exception.MetadataSourceException; +import org.dspace.importer.external.service.AbstractImportMetadataSourceService; +import org.dspace.importer.external.service.DoiCheck; +import org.dspace.importer.external.service.components.QuerySource; +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.JDOMException; +import org.jdom2.Namespace; +import org.jdom2.filter.Filters; +import org.jdom2.input.SAXBuilder; +import org.jdom2.xpath.XPathExpression; +import org.jdom2.xpath.XPathFactory; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Implements a data source for querying Scopus + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science dot com) + */ +public class ScopusImportMetadataSourceServiceImpl extends AbstractImportMetadataSourceService + implements QuerySource { + + private int timeout = 1000; + + int itemPerPage = 25; + + private static final String ENDPOINT_SEARCH_SCOPUS = "https://api.elsevier.com/content/search/scopus"; + + private String apiKey; + private String instKey; + private String viewMode = "COMPLETE"; + + @Autowired + private LiveImportClient liveImportClient; + + public LiveImportClient getLiveImportClient() { + return liveImportClient; + } + + public void setLiveImportClient(LiveImportClient liveImportClient) { + this.liveImportClient = liveImportClient; + } + + @Override + public void init() throws Exception {} + + /** + * The string that identifies this import implementation. Preferable a URI + * + * @return the identifying uri + */ + @Override + public String getImportSource() { + return "scopus"; + } + + @Override + public int getRecordsCount(String query) throws MetadataSourceException { + if (isEID(query)) { + return retry(new FindByIdCallable(query)).size(); + } + if (DoiCheck.isDoi(query)) { + query = DoiCheck.purgeDoiValue(query); + } + return retry(new SearchNBByQueryCallable(query)); + } + + @Override + public int getRecordsCount(Query query) throws MetadataSourceException { + if (isEID(query.toString())) { + return retry(new FindByIdCallable(query.toString())).size(); + } + if (DoiCheck.isDoi(query.toString())) { + query.addParameter("query", DoiCheck.purgeDoiValue(query.toString())); + } + return retry(new SearchNBByQueryCallable(query)); + } + + @Override + public Collection getRecords(String query, int start, + int count) throws MetadataSourceException { + if (isEID(query)) { + return retry(new FindByIdCallable(query)); + } + if (DoiCheck.isDoi(query)) { + query = DoiCheck.purgeDoiValue(query); + } + return retry(new SearchByQueryCallable(query, count, start)); + } + + @Override + public Collection getRecords(Query query) + throws MetadataSourceException { + if (isEID(query.toString())) { + return retry(new FindByIdCallable(query.toString())); + } + if (DoiCheck.isDoi(query.toString())) { + query.addParameter("query", DoiCheck.purgeDoiValue(query.toString())); + } + return retry(new SearchByQueryCallable(query)); + } + + + @Override + public ImportRecord getRecord(Query query) throws MetadataSourceException { + List records = null; + if (DoiCheck.isDoi(query.toString())) { + query.addParameter("query", DoiCheck.purgeDoiValue(query.toString())); + } + if (isEID(query.toString())) { + records = retry(new FindByIdCallable(query.toString())); + } else { + records = retry(new SearchByQueryCallable(query)); + } + return records == null || records.isEmpty() ? null : records.get(0); + } + + @Override + public Collection findMatchingRecords(Item item) + throws MetadataSourceException { + throw new MethodNotFoundException("This method is not implemented for Scopus"); + } + + @Override + public ImportRecord getRecord(String id) throws MetadataSourceException { + List records = retry(new FindByIdCallable(id)); + return records == null || records.isEmpty() ? null : records.get(0); + } + + @Override + public Collection findMatchingRecords(Query query) + throws MetadataSourceException { + if (isEID(query.toString())) { + return retry(new FindByIdCallable(query.toString())); + } + if (DoiCheck.isDoi(query.toString())) { + query.addParameter("query", DoiCheck.purgeDoiValue(query.toString())); + } + return retry(new FindByQueryCallable(query)); + } + + private boolean isEID(String query) { + Pattern pattern = Pattern.compile("2-s2\\.0-\\d+"); + Matcher match = pattern.matcher(query); + if (match.matches()) { + return true; + } + return false; + } + + /** + * This class implements a callable to get the numbers of result + */ + private class SearchNBByQueryCallable implements Callable { + + private String query; + + private SearchNBByQueryCallable(String queryString) { + this.query = queryString; + } + + private SearchNBByQueryCallable(Query query) { + this.query = query.getParameterAsClass("query", String.class); + } + + @Override + public Integer call() throws Exception { + if (StringUtils.isNotBlank(apiKey)) { + // Execute the request. + Map requestParams = getRequestParameters(query, null, null, null); + String response = liveImportClient.executeHttpGetRequest(timeout, ENDPOINT_SEARCH_SCOPUS,requestParams); + + SAXBuilder saxBuilder = new SAXBuilder(); + Document document = saxBuilder.build(new StringReader(response)); + Element root = document.getRootElement(); + + List namespaces = Arrays.asList( + Namespace.getNamespace("opensearch", "http://a9.com/-/spec/opensearch/1.1/")); + XPathExpression xpath = XPathFactory.instance() + .compile("opensearch:totalResults", Filters.element(), null, namespaces); + + Element count = xpath.evaluateFirst(root); + try { + return Integer.parseInt(count.getText()); + } catch (NumberFormatException e) { + return null; + } + } + return null; + } + } + + private class FindByIdCallable implements Callable> { + + private String eid; + + private FindByIdCallable(String eid) { + this.eid = eid; + } + + @Override + public List call() throws Exception { + List results = new ArrayList<>(); + String queryString = "EID(" + eid.replace("!", "/") + ")"; + if (StringUtils.isNotBlank(apiKey)) { + Map requestParams = getRequestParameters(queryString, viewMode, null, null); + String response = liveImportClient.executeHttpGetRequest(timeout, ENDPOINT_SEARCH_SCOPUS,requestParams); + List elements = splitToRecords(response); + for (Element record : elements) { + results.add(transformSourceRecords(record)); + } + } + return results; + } + } + + /** + * This class implements a callable to get the items based on query parameters + */ + private class FindByQueryCallable implements Callable> { + + private String title; + private String author; + private Integer year; + private Integer start; + private Integer count; + + private FindByQueryCallable(Query query) { + this.title = query.getParameterAsClass("title", String.class); + this.year = query.getParameterAsClass("year", Integer.class); + this.author = query.getParameterAsClass("author", String.class); + this.start = query.getParameterAsClass("start", Integer.class) != null ? + query.getParameterAsClass("start", Integer.class) : 0; + this.count = query.getParameterAsClass("count", Integer.class) != null ? + query.getParameterAsClass("count", Integer.class) : 20; + } + + @Override + public List call() throws Exception { + List results = new ArrayList<>(); + String queryString = ""; + StringBuffer query = new StringBuffer(); + if (StringUtils.isNotBlank(title)) { + query.append("title(").append(title).append(""); + } + if (StringUtils.isNotBlank(author)) { + // [FAU] + if (query.length() > 0) { + query.append(" AND "); + } + query.append("AUTH(").append(author).append(")"); + } + if (year != -1) { + // [DP] + if (query.length() > 0) { + query.append(" AND "); + } + query.append("PUBYEAR IS ").append(year); + } + queryString = query.toString(); + + if (apiKey != null && !apiKey.equals("")) { + Map requestParams = getRequestParameters(queryString, viewMode, start, count); + String response = liveImportClient.executeHttpGetRequest(timeout, ENDPOINT_SEARCH_SCOPUS,requestParams); + List elements = splitToRecords(response); + for (Element record : elements) { + results.add(transformSourceRecords(record)); + } + } + return results; + } + } + + private class SearchByQueryCallable implements Callable> { + private Query query; + + + private SearchByQueryCallable(String queryString, Integer maxResult, Integer start) { + query = new Query(); + query.addParameter("query", queryString); + query.addParameter("start", start); + query.addParameter("count", maxResult); + } + + private SearchByQueryCallable(Query query) { + this.query = query; + } + + @Override + public List call() throws Exception { + List results = new ArrayList<>(); + String queryString = query.getParameterAsClass("query", String.class); + Integer start = query.getParameterAsClass("start", Integer.class); + Integer count = query.getParameterAsClass("count", Integer.class); + if (StringUtils.isNotBlank(apiKey)) { + Map requestParams = getRequestParameters(queryString, viewMode, start, count); + String response = liveImportClient.executeHttpGetRequest(timeout, ENDPOINT_SEARCH_SCOPUS,requestParams); + List elements = splitToRecords(response); + for (Element record : elements) { + results.add(transformSourceRecords(record)); + } + } + return results; + } + } + + private Map getRequestParameters(String query, String viewMode, Integer start, Integer count) { + Map params = new HashMap(); + params.put("httpAccept", "application/xml"); + params.put("apiKey", apiKey); + params.put("query", query); + + if (StringUtils.isNotBlank(instKey)) { + params.put("insttoken", instKey); + } + if (StringUtils.isNotBlank(viewMode)) { + params.put("view", viewMode); + } + + params.put("start", (Objects.nonNull(start) ? start + "" : "0")); + params.put("count", (Objects.nonNull(count) ? count + "" : "20")); + return params; + } + + private List splitToRecords(String recordsSrc) { + try { + SAXBuilder saxBuilder = new SAXBuilder(); + Document document = saxBuilder.build(new StringReader(recordsSrc)); + Element root = document.getRootElement(); + List records = root.getChildren("entry",Namespace.getNamespace("http://www.w3.org/2005/Atom")); + return records; + } catch (JDOMException | IOException e) { + return new ArrayList(); + } + } + + public String getViewMode() { + return viewMode; + } + + public void setViewMode(String viewMode) { + this.viewMode = viewMode; + } + + public String getApiKey() { + return apiKey; + } + + public String getInstKey() { + return instKey; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + + public void setInstKey(String instKey) { + this.instKey = instKey; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/service/DoiCheck.java b/dspace-api/src/main/java/org/dspace/importer/external/service/DoiCheck.java new file mode 100644 index 000000000000..3b15a421b853 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/service/DoiCheck.java @@ -0,0 +1,47 @@ +/** + * 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.importer.external.service; + +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Utility class that provides methods to check if a given string is a DOI and exists on CrossRef services + * + * @author Corrado Lombardi (corrado.lombardi at 4science.it) + */ +public class DoiCheck { + + private static final List DOI_PREFIXES = Arrays.asList("http://dx.doi.org/", "https://dx.doi.org/"); + + private static final Pattern PATTERN = Pattern.compile("10.\\d{4,9}/[-._;()/:A-Z0-9]+" + + "|10.1002/[^\\s]+" + + "|10.\\d{4}/\\d+-\\d+X?(\\d+)" + + "\\d+<[\\d\\w]+:[\\d\\w]*>\\d+.\\d+.\\w+;\\d" + + "|10.1021/\\w\\w\\d++" + + "|10.1207/[\\w\\d]+\\&\\d+_\\d+", + Pattern.CASE_INSENSITIVE); + + private DoiCheck() {} + + public static boolean isDoi(final String value) { + Matcher m = PATTERN.matcher(purgeDoiValue(value)); + return m.matches(); + } + + public static String purgeDoiValue(final String query) { + String value = query.replaceAll(",", ""); + for (final String prefix : DOI_PREFIXES) { + value = value.replaceAll(prefix, ""); + } + return value.trim(); + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/submit/lookup/MapConverterModifier.java b/dspace-api/src/main/java/org/dspace/submit/lookup/MapConverterModifier.java new file mode 100644 index 000000000000..ea02661ca22e --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/submit/lookup/MapConverterModifier.java @@ -0,0 +1,115 @@ +/** + * 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.submit.lookup; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import org.dspace.services.ConfigurationService; + +/** + * @author Andrea Bollini + * @author Kostas Stamatis + * @author Luigi Andrea Pascarelli + * @author Panagiotis Koutsourakis + */ +public class MapConverterModifier { + + protected String mappingFile; // The properties absolute filename + + protected String converterNameFile; // The properties filename + + protected ConfigurationService configurationService; + + protected Map mapping; + + protected String defaultValue = ""; + + protected List fieldKeys; + + protected Map regexConfig = new HashMap(); + + public final String REGEX_PREFIX = "regex."; + + public void init() { + this.mappingFile = configurationService.getProperty( + "dspace.dir") + File.separator + "config" + File.separator + "crosswalks" + File.separator + + converterNameFile; + + this.mapping = new HashMap(); + + FileInputStream fis = null; + try { + fis = new FileInputStream(new File(mappingFile)); + Properties mapConfig = new Properties(); + mapConfig.load(fis); + fis.close(); + for (Object key : mapConfig.keySet()) { + String keyS = (String) key; + if (keyS.startsWith(REGEX_PREFIX)) { + String regex = keyS.substring(REGEX_PREFIX.length()); + String regReplace = mapping.get(keyS); + if (regReplace == null) { + regReplace = ""; + } else if (regReplace.equalsIgnoreCase("@ident@")) { + regReplace = "$0"; + } + regexConfig.put(regex, regReplace); + } + if (mapConfig.getProperty(keyS) != null) { + mapping.put(keyS, mapConfig.getProperty(keyS)); + } else { + mapping.put(keyS, ""); + } + } + } catch (Exception e) { + throw new IllegalArgumentException("", e); + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException ioe) { + // ... + } + } + } + for (String keyS : mapping.keySet()) { + if (keyS.startsWith(REGEX_PREFIX)) { + String regex = keyS.substring(REGEX_PREFIX.length()); + String regReplace = mapping.get(keyS); + if (regReplace == null) { + regReplace = ""; + } else if (regReplace.equalsIgnoreCase("@ident@")) { + regReplace = "$0"; + } + regexConfig.put(regex, regReplace); + } + } + } + + public void setFieldKeys(List fieldKeys) { + this.fieldKeys = fieldKeys; + } + + public void setDefaultValue(String defaultValue) { + this.defaultValue = defaultValue; + } + + public void setConverterNameFile(String converterNameFile) { + this.converterNameFile = converterNameFile; + } + + public void setConfigurationService(ConfigurationService configurationService) { + this.configurationService = configurationService; + } +} diff --git a/dspace-api/src/main/java/org/dspace/util/SimpleMapConverter.java b/dspace-api/src/main/java/org/dspace/util/SimpleMapConverter.java new file mode 100644 index 000000000000..5648879b777e --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/util/SimpleMapConverter.java @@ -0,0 +1,45 @@ +/** + * 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.util; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.submit.lookup.MapConverterModifier; + +/** + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.it) + */ +public class SimpleMapConverter extends MapConverterModifier { + + public String getValue(String key) { + boolean matchEmpty = false; + String stringValue = key; + + String tmp = ""; + if (mapping.containsKey(stringValue)) { + tmp = mapping.get(stringValue); + } else { + tmp = defaultValue; + for (String regex : regexConfig.keySet()) { + if (stringValue != null && stringValue.matches(regex)) { + tmp = stringValue.replaceAll(regex, regexConfig.get(regex)); + if (StringUtils.isBlank(tmp)) { + matchEmpty = true; + } + } + } + } + + if ("@@ident@@".equals(tmp)) { + return stringValue; + } else if (StringUtils.isNotBlank(tmp) || (StringUtils.isBlank(tmp) && matchEmpty)) { + return tmp; + } + return stringValue; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml b/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml index 5e69ee9c4282..83b5439ae39a 100644 --- a/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml +++ b/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml @@ -115,6 +115,14 @@ + + + + + + + + diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/external-services.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/external-services.xml index ac163d35811d..47e7519615b7 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/external-services.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/external-services.xml @@ -6,6 +6,8 @@ + + @@ -91,6 +93,17 @@ + + + + + + + Publication + + + + diff --git a/dspace/config/crosswalks/mapConverter-openAccesFlag.properties b/dspace/config/crosswalks/mapConverter-openAccesFlag.properties new file mode 100644 index 000000000000..9703a0f388d9 --- /dev/null +++ b/dspace/config/crosswalks/mapConverter-openAccesFlag.properties @@ -0,0 +1 @@ +true=open access \ No newline at end of file diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index d5c28da0964c..8b985533c284 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -1586,6 +1586,19 @@ request.item.helpdesk.override = false # ${dspace.dir}/config/ directory. module_dir = modules +#################################################################### +#------------------- SCOPUS SERVICES ------------------------------# +#------------------------------------------------------------------# + +scopus.apiKey = +# leave empty if you don't need to use an institutional token +scopus.instToken = +#The view mode to be used for the scopus search endpoint. +#For more details see https://dev.elsevier.com/documentation/ScopusSearchAPI.wadl +scopus.search-api.viewMode = COMPLETE + +#################################################################### + # Load default module configs # ---------------------------- # To exclude a module configuration, simply comment out its "include" statement. diff --git a/dspace/config/spring/api/crosswalks.xml b/dspace/config/spring/api/crosswalks.xml new file mode 100644 index 000000000000..61b43cad277e --- /dev/null +++ b/dspace/config/spring/api/crosswalks.xml @@ -0,0 +1,13 @@ + + + + + + + + + + \ No newline at end of file diff --git a/dspace/config/spring/api/external-services.xml b/dspace/config/spring/api/external-services.xml index 7f1295f839df..af8af57835b6 100644 --- a/dspace/config/spring/api/external-services.xml +++ b/dspace/config/spring/api/external-services.xml @@ -94,5 +94,16 @@ + + + + + + + Publication + + + + diff --git a/dspace/config/spring/api/scopus-integration.xml b/dspace/config/spring/api/scopus-integration.xml new file mode 100644 index 000000000000..ba59928ea101 --- /dev/null +++ b/dspace/config/spring/api/scopus-integration.xml @@ -0,0 +1,338 @@ + + + + + + + Defines which metadatum is mapped on which metadatum. Note that while the key must be unique it + only matters here for postprocessing of the value. The mapped MetadatumContributor has full control over + what metadatafield is generated. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 9bca9e21e184b1a564184c7f0ecb14e28d7b0863 Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Mon, 4 Apr 2022 16:18:13 +0200 Subject: [PATCH 0106/1846] [CST-5303] added tests for scopus live import service --- .../assetstore/scopus-empty-resp.xml | 11 + .../dspaceFolder/assetstore/scopus-ex.xml | 178 ++++++++++++ .../src/test/resources/test-config.properties | 2 + .../ScopusImportMetadataSourceServiceIT.java | 255 ++++++++++++++++++ 4 files changed, 446 insertions(+) create mode 100644 dspace-api/src/test/data/dspaceFolder/assetstore/scopus-empty-resp.xml create mode 100644 dspace-api/src/test/data/dspaceFolder/assetstore/scopus-ex.xml create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/ScopusImportMetadataSourceServiceIT.java diff --git a/dspace-api/src/test/data/dspaceFolder/assetstore/scopus-empty-resp.xml b/dspace-api/src/test/data/dspaceFolder/assetstore/scopus-empty-resp.xml new file mode 100644 index 000000000000..b2b4264b5c34 --- /dev/null +++ b/dspace-api/src/test/data/dspaceFolder/assetstore/scopus-empty-resp.xml @@ -0,0 +1,11 @@ + + + 0 + 0 + 0 + + + + Result set was empty + + \ No newline at end of file diff --git a/dspace-api/src/test/data/dspaceFolder/assetstore/scopus-ex.xml b/dspace-api/src/test/data/dspaceFolder/assetstore/scopus-ex.xml new file mode 100644 index 000000000000..84fd7e71e212 --- /dev/null +++ b/dspace-api/src/test/data/dspaceFolder/assetstore/scopus-ex.xml @@ -0,0 +1,178 @@ + + + 2 + 0 + 2 + + + + + + + + + + + https://api.elsevier.com/content/abstract/scopus_id/85124241875 + SCOPUS_ID:85124241875 + 2-s2.0-85124241875 + Hardy potential versus lower order terms in Dirichlet problems: regularizing effects<sup>†</sup> + Arcoya D. + Mathematics In Engineering + 26403501 + 5 + 1 + + 2023-01-01 + 2023 + 10.3934/mine.2023004 + In this paper, dedicated to Ireneo Peral, we study the regularizing effect of some lower order terms in Dirichlet problems despite the presence of Hardy potentials in the right hand side. + 0 + + https://api.elsevier.com/content/affiliation/affiliation_id/60032350 + 60032350 + Sapienza Università di Roma + Rome + Italy + + + https://api.elsevier.com/content/affiliation/affiliation_id/60027844 + 60027844 + Universidad de Granada + Granada + Spain + + Journal + ar + Article + 3 + + https://api.elsevier.com/content/author/author_id/6602330574 + 6602330574 + Arcoya D. + Arcoya + David + D. + 60027844 + + + https://api.elsevier.com/content/author/author_id/7003612261 + 7003612261 + Boccardo L. + Boccardo + Lucio + L. + 60032350 + + + https://api.elsevier.com/content/author/author_id/6602595438 + 6602595438 + Orsina L. + Orsina + Luigi + L. + 60032350 + + Hardy potentials | Laplace equation | Summability of solutions + 21101039848 + PGC2018-096422-B-I00 + Junta de Andalucía + 1 + true + + all + publisherfullgold + repository + repositoryvor + + + All Open Access + Gold + Green + + + + + + + + https://api.elsevier.com/content/abstract/scopus_id/85124226483 + SCOPUS_ID:85124226483 + 2-s2.0-85124226483 + Large deviations for a binary collision model: energy evaporation<sup>†</sup> + Basile G. + Mathematics In Engineering + 26403501 + 5 + 1 + + 2023-01-01 + 2023 + 10.3934/mine.2023001 + We analyze the large deviations for a discrete energy Kac-like walk. In particular, we exhibit a path, with probability exponentially small in the number of particles, that looses energy. + 0 + + https://api.elsevier.com/content/affiliation/affiliation_id/60032350 + 60032350 + Sapienza Università di Roma + Rome + Italy + + Journal + ar + Article + 4 + + https://api.elsevier.com/content/author/author_id/55613229065 + 55613229065 + Basile G. + Basile + Giada + G. + 60032350 + + + https://api.elsevier.com/content/author/author_id/55893665100 + 55893665100 + Benedetto D. + Benedetto + Dario + D. + 60032350 + + + https://api.elsevier.com/content/author/author_id/7004588675 + 7004588675 + Caglioti E. + Caglioti + Emanuele + E. + 60032350 + + + https://api.elsevier.com/content/author/author_id/7005555198 + 7005555198 + Bertini L. + Bertini + Lorenzo + L. + 60032350 + + Boltzmann equation | Discrete energy model | Kac model | Large deviations | Violation of energy conservation + 21101039848 + undefined + 1 + true + + all + publisherfullgold + repository + repositoryam + + + All Open Access + Gold + Green + + + \ No newline at end of file diff --git a/dspace-api/src/test/resources/test-config.properties b/dspace-api/src/test/resources/test-config.properties index 66a29ab9a09b..ca473dcf86e4 100644 --- a/dspace-api/src/test/resources/test-config.properties +++ b/dspace-api/src/test/resources/test-config.properties @@ -13,3 +13,5 @@ test.folder = ./target/testing/ test.bitstream = ./target/testing/dspace/assetstore/ConstitutionofIreland.pdf test.exportcsv = ./target/testing/dspace/assetstore/test.csv test.importcsv = ./target/testing/dspace/assetstore/testImport.csv +test.scopus = ./target/testing/dspace/assetstore/scopus-ex.xml +test.scopus-empty = ./target/testing/dspace/assetstore/scopus-empty-resp.xml diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScopusImportMetadataSourceServiceIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScopusImportMetadataSourceServiceIT.java new file mode 100644 index 000000000000..a3ec481a21f3 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScopusImportMetadataSourceServiceIT.java @@ -0,0 +1,255 @@ +/** + * 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.rest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.FileInputStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.ProtocolVersion; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.entity.BasicHttpEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.tools.ant.filters.StringInputStream; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.importer.external.datamodel.ImportRecord; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; +import org.dspace.importer.external.scopus.service.LiveImportClientImpl; +import org.dspace.importer.external.scopus.service.ScopusImportMetadataSourceServiceImpl; +import org.junit.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Integration tests for {@link ScopusImportMetadataSourceServiceImpl} + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com) + */ +public class ScopusImportMetadataSourceServiceIT extends AbstractControllerIntegrationTest { + + @Autowired + private ScopusImportMetadataSourceServiceImpl scopusServiceImpl; + + @Autowired + private LiveImportClientImpl liveImportClientImpl; + + @Test + public void scopusImportMetadataGetRecordsTest() throws Exception { + context.turnOffAuthorisationSystem(); + String originApiKey = scopusServiceImpl.getApiKey(); + if (StringUtils.isBlank(originApiKey)) { + scopusServiceImpl.setApiKey("testApiKey"); + } + CloseableHttpClient originalHttpClient = liveImportClientImpl.getHttpClient(); + CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + String path = testProps.get("test.scopus").toString(); + try (FileInputStream file = new FileInputStream(path)) { + String xmlMetricsExample = IOUtils.toString(file, Charset.defaultCharset()); + + liveImportClientImpl.setHttpClient(httpClient); + + CloseableHttpResponse response = mockResponse(xmlMetricsExample, 200, "OK"); + + when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response); + + context.restoreAuthSystemState(); + Collection collection2match = getRecords(); + Collection recordsImported = scopusServiceImpl.getRecords("test query", 0, 2); + assertEquals(2, recordsImported.size()); + assertTrue(matchRecords(recordsImported, collection2match)); + } finally { + liveImportClientImpl.setHttpClient(originalHttpClient); + scopusServiceImpl.setApiKey(originApiKey); + } + } + + @Test + public void scopusImportMetadataGetRecordsCountTest() throws Exception { + context.turnOffAuthorisationSystem(); + String originApiKey = scopusServiceImpl.getApiKey(); + if (StringUtils.isBlank(originApiKey)) { + scopusServiceImpl.setApiKey("testApiKey"); + } + CloseableHttpClient originalHttpClient = liveImportClientImpl.getHttpClient(); + CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + String path = testProps.get("test.scopus").toString(); + try (FileInputStream file = new FileInputStream(path)) { + String xmlMetricsExample = IOUtils.toString(file, Charset.defaultCharset()); + + liveImportClientImpl.setHttpClient(httpClient); + + CloseableHttpResponse response = mockResponse(xmlMetricsExample, 200, "OK"); + + when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response); + + context.restoreAuthSystemState(); + int tot = scopusServiceImpl.getRecordsCount("test query"); + assertEquals(2, tot); + } finally { + liveImportClientImpl.setHttpClient(originalHttpClient); + scopusServiceImpl.setApiKey(originApiKey); + } + } + + @Test + public void scopusImportMetadataGetRecordsEmptyResponceTest() throws Exception { + context.turnOffAuthorisationSystem(); + String originApiKey = scopusServiceImpl.getApiKey(); + if (StringUtils.isBlank(originApiKey)) { + scopusServiceImpl.setApiKey("testApiKey"); + } + CloseableHttpClient originalHttpClient = liveImportClientImpl.getHttpClient(); + CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + String path = testProps.get("test.scopus-empty").toString(); + try (FileInputStream file = new FileInputStream(path)) { + String xmlMetricsExample = IOUtils.toString(file, Charset.defaultCharset()); + + liveImportClientImpl.setHttpClient(httpClient); + + CloseableHttpResponse response = mockResponse(xmlMetricsExample, 200, "OK"); + + when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response); + + context.restoreAuthSystemState(); + Collection recordsImported = scopusServiceImpl.getRecords("test query", 0, 20); + ImportRecord importedRecord = recordsImported.iterator().next(); + assertTrue(importedRecord.getValueList().isEmpty()); + } finally { + liveImportClientImpl.setHttpClient(originalHttpClient); + scopusServiceImpl.setApiKey(originApiKey); + } + } + + private boolean matchRecords(Collection recordsImported, Collection records2match) { + ImportRecord firstImported = recordsImported.iterator().next(); + ImportRecord secondImported = recordsImported.iterator().next(); + ImportRecord first2match = recordsImported.iterator().next(); + ImportRecord second2match = recordsImported.iterator().next(); + boolean checkFirstRecord = firstImported.getValueList().containsAll(first2match.getValueList()); + boolean checkSecondRecord = secondImported.getValueList().containsAll(second2match.getValueList()); + return checkFirstRecord && checkSecondRecord; + } + + private Collection getRecords() { + Collection records = new LinkedList(); + List metadatums = new ArrayList(); + //define first record + MetadatumDTO title = createMetadatumDTO("dc","title", null, + "Hardy potential versus lower order terms in Dirichlet problems: regularizing effects"); + MetadatumDTO doi = createMetadatumDTO("dc", "identifier", "doi", "10.3934/mine.2023004"); + MetadatumDTO date = createMetadatumDTO("dc", "date", "issued", "2023-01-01"); + MetadatumDTO type = createMetadatumDTO("dc", "type", null, "Journal"); + MetadatumDTO citationVolume = createMetadatumDTO("oaire", "citation", "volume", "5"); + MetadatumDTO citationIssue = createMetadatumDTO("oaire", "citation", "issue", "1"); + MetadatumDTO scopusId = createMetadatumDTO("dc", "identifier", "scopus", "2-s2.0-85124241875"); + MetadatumDTO funding = createMetadatumDTO("dc", "relation", "funding", "Junta de Andalucía"); + MetadatumDTO grantno = createMetadatumDTO("dc", "relation", "grantno", "PGC2018-096422-B-I00"); + MetadatumDTO subject = createMetadatumDTO("dc", "subject", null, + "Hardy potentials | Laplace equation | Summability of solutions"); + MetadatumDTO rights = createMetadatumDTO("dc", "rights", null, "open access"); + MetadatumDTO ispartof = createMetadatumDTO("dc", "relation", "ispartof", + "Mathematics In Engineering"); + metadatums.add(title); + metadatums.add(doi); + metadatums.add(date); + metadatums.add(type); + metadatums.add(citationVolume); + metadatums.add(citationIssue); + metadatums.add(scopusId); + metadatums.add(funding); + metadatums.add(grantno); + metadatums.add(subject); + metadatums.add(rights); + metadatums.add(ispartof); + ImportRecord firstrRecord = new ImportRecord(metadatums); + //define second record + MetadatumDTO title2 = createMetadatumDTO("dc","title", null, + "Large deviations for a binary collision model: energy evaporation"); + MetadatumDTO doi2 = createMetadatumDTO("dc", "identifier", "doi", "10.3934/mine.2023001"); + MetadatumDTO date2 = createMetadatumDTO("dc", "date", "issued", "2023-01-01"); + MetadatumDTO type2 = createMetadatumDTO("dc", "type", null, "Journal"); + MetadatumDTO citationVolume2 = createMetadatumDTO("oaire", "citation", "volume", "5"); + MetadatumDTO citationIssue2 = createMetadatumDTO("oaire", "citation", "issue", "1"); + MetadatumDTO scopusId2 = createMetadatumDTO("dc", "identifier", "scopus", "2-s2.0-85124226483"); + MetadatumDTO grantno2 = createMetadatumDTO("dc", "relation", "grantno", "undefined"); + MetadatumDTO subject2 = createMetadatumDTO("dc", "subject", null, + "Boltzmann equation | Discrete energy model | Kac model | Large deviations | Violation of energy conservation"); + MetadatumDTO rights2 = createMetadatumDTO("dc", "rights", null, "open access"); + MetadatumDTO ispartof2 = createMetadatumDTO("dc", "relation", "ispartof", + "Mathematics In Engineering"); + metadatums.add(title2); + metadatums.add(doi2); + metadatums.add(date2); + metadatums.add(type2); + metadatums.add(citationVolume2); + metadatums.add(citationIssue2); + metadatums.add(scopusId2); + metadatums.add(grantno2); + metadatums.add(subject2); + metadatums.add(rights2); + metadatums.add(ispartof2); + ImportRecord secondRecord = new ImportRecord(metadatums); + records.add(firstrRecord); + records.add(secondRecord); + return records; + } + + private MetadatumDTO createMetadatumDTO(String schema, String element, String qualifier, String value) { + MetadatumDTO metadatumDTO = new MetadatumDTO(); + metadatumDTO.setSchema(schema); + metadatumDTO.setElement(element); + metadatumDTO.setQualifier(qualifier); + metadatumDTO.setValue(value); + return metadatumDTO; + } + + private CloseableHttpResponse mockResponse(String xmlExample, int statusCode, String reason) + throws UnsupportedEncodingException { + BasicHttpEntity basicHttpEntity = new BasicHttpEntity(); + basicHttpEntity.setChunked(true); + basicHttpEntity.setContent(new StringInputStream(xmlExample)); + + CloseableHttpResponse response = mock(CloseableHttpResponse.class); + when(response.getStatusLine()).thenReturn(statusLine(statusCode, reason)); + when(response.getEntity()).thenReturn(basicHttpEntity); + return response; + } + + private StatusLine statusLine(int statusCode, String reason) { + return new StatusLine() { + @Override + public ProtocolVersion getProtocolVersion() { + return new ProtocolVersion("http", 1, 1); + } + + @Override + public int getStatusCode() { + return statusCode; + } + + @Override + public String getReasonPhrase() { + return reason; + } + }; + } + +} \ No newline at end of file From bebe3d7bfe97ec5f27b7dec694337b082a272a1d Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Tue, 29 Mar 2022 17:02:46 +0200 Subject: [PATCH 0107/1846] [CST-5303] fix tests --- dspace-server-webapp/src/test/resources/test-config.properties | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dspace-server-webapp/src/test/resources/test-config.properties b/dspace-server-webapp/src/test/resources/test-config.properties index 3af96b20fcf4..8d49daaddfb1 100644 --- a/dspace-server-webapp/src/test/resources/test-config.properties +++ b/dspace-server-webapp/src/test/resources/test-config.properties @@ -14,3 +14,5 @@ test.bitstream = ./target/testing/dspace/assetstore/ConstitutionofIreland.pdf #Path for a test Taskfile for the curate script test.curateTaskFile = ./target/testing/dspace/assetstore/curate.txt +test.scopus = ./target/testing/dspace/assetstore/scopus-ex.xml +test.scopus-empty = ./target/testing/dspace/assetstore/scopus-empty-resp.xml \ No newline at end of file From 55faacb168d32a34082dd8a59fe700b7b287ec5b Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Wed, 30 Mar 2022 21:33:32 +0200 Subject: [PATCH 0108/1846] [CST-5303] porting pubmed live import --- .../SimpleXpathMetadatumContributor.java | 2 +- .../PubmedEuropeFieldMapping.java | 37 +++ ...PubmedEuropeMetadataSourceServiceImpl.java | 310 ++++++++++++++++++ .../spring-dspace-addon-import-services.xml | 12 +- .../spring/api/pubmedeurope-integration.xml | 184 +++++++++++ 5 files changed, 543 insertions(+), 2 deletions(-) create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/pubmedeurope/PubmedEuropeFieldMapping.java create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/pubmedeurope/PubmedEuropeMetadataSourceServiceImpl.java create mode 100644 dspace/config/spring/api/pubmedeurope-integration.xml diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumContributor.java index 10a4ddd4d826..0fe6e30946a7 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumContributor.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumContributor.java @@ -47,7 +47,7 @@ public Map getPrefixToNamespaceMapping() { return prefixToNamespaceMapping; } - private MetadataFieldMapping> metadataFieldMapping; + protected MetadataFieldMapping> metadataFieldMapping; /** * Return metadataFieldMapping diff --git a/dspace-api/src/main/java/org/dspace/importer/external/pubmedeurope/PubmedEuropeFieldMapping.java b/dspace-api/src/main/java/org/dspace/importer/external/pubmedeurope/PubmedEuropeFieldMapping.java new file mode 100644 index 000000000000..a9495c3cb71f --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/pubmedeurope/PubmedEuropeFieldMapping.java @@ -0,0 +1,37 @@ +/** + * 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.importer.external.pubmedeurope; + +import java.util.Map; +import javax.annotation.Resource; + +import org.dspace.importer.external.metadatamapping.AbstractMetadataFieldMapping; + +/** + * An implementation of {@link AbstractMetadataFieldMapping} + * Responsible for defining the mapping of the ArXiv metadatum fields on the DSpace metadatum fields + * + * @author Pasquale Cavallo (pasquale.cavallo at 4science dot it) + */ +public class PubmedEuropeFieldMapping extends AbstractMetadataFieldMapping { + + /** + * Defines which metadatum is mapped on which metadatum. Note that while the key must be unique it + * only matters here for postprocessing of the value. The mapped MetadatumContributor has full control over + * what metadatafield is generated. + * + * @param metadataFieldMap The map containing the link between retrieve metadata and metadata that will be set to + * the item. + */ + @Override + @Resource(name = "pubmedEuropeMetadataFieldMap") + public void setMetadataFieldMap(Map metadataFieldMap) { + super.setMetadataFieldMap(metadataFieldMap); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/importer/external/pubmedeurope/PubmedEuropeMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/pubmedeurope/PubmedEuropeMetadataSourceServiceImpl.java new file mode 100644 index 000000000000..a1fe1159b4a3 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/pubmedeurope/PubmedEuropeMetadataSourceServiceImpl.java @@ -0,0 +1,310 @@ +/** + * 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.importer.external.pubmedeurope; + +import java.io.IOException; +import java.io.StringReader; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.Callable; +import javax.el.MethodNotFoundException; + +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpException; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.utils.URIBuilder; +import org.apache.log4j.Logger; +import org.dspace.content.Item; +import org.dspace.importer.external.datamodel.ImportRecord; +import org.dspace.importer.external.datamodel.Query; +import org.dspace.importer.external.exception.MetadataSourceException; +import org.dspace.importer.external.scopus.service.LiveImportClient; +import org.dspace.importer.external.service.AbstractImportMetadataSourceService; +import org.dspace.importer.external.service.components.QuerySource; +import org.jaxen.JaxenException; +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.JDOMException; +import org.jdom2.filter.Filters; +import org.jdom2.input.SAXBuilder; +import org.jdom2.xpath.XPathExpression; +import org.jdom2.xpath.XPathFactory; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Implements a data source for querying PubmedEurope + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com) + */ +public class PubmedEuropeMetadataSourceServiceImpl extends AbstractImportMetadataSourceService + implements QuerySource { + + private static final Logger log = Logger.getLogger(PubmedEuropeMetadataSourceServiceImpl.class); + + private static final String ENDPOINT_SEARCH = "https://www.ebi.ac.uk/europepmc/webservices/rest/search"; + + @Autowired + private LiveImportClient liveImportClient; + + @Override + public String getImportSource() { + return "pubmedeu"; + } + + @Override + public ImportRecord getRecord(String id) throws MetadataSourceException { + List records = retry(new SearchByIdCallable(id)); + return records == null || records.isEmpty() ? null : records.get(0); + } + + @Override + public int getRecordsCount(String query) throws MetadataSourceException { + return retry(new CountByQueryCallable(query)); + } + + @Override + public int getRecordsCount(Query query) throws MetadataSourceException { + return retry(new CountByQueryCallable(query)); + } + + @Override + public Collection getRecords(String query, int start, int count) throws MetadataSourceException { + return retry(new SearchByQueryCallable(query, count, start)); + } + + @Override + public Collection getRecords(Query query) throws MetadataSourceException { + return retry(new SearchByQueryCallable(query)); + } + + @Override + public ImportRecord getRecord(Query query) throws MetadataSourceException { + List records = retry(new SearchByIdCallable(query)); + return records == null || records.isEmpty() ? null : records.get(0); + } + + @Override + public Collection findMatchingRecords(Query query) throws MetadataSourceException { + return retry(new FindMatchingRecordCallable(query)); + } + + @Override + public Collection findMatchingRecords(Item item) throws MetadataSourceException { + throw new MethodNotFoundException("This method is not implemented for CrossRef"); + } + + @Override + public void init() throws Exception {} + + public List getByPubmedEuropeID(String pubmedID, Integer start, Integer count) + throws IOException, HttpException { + String query = "(EXT_ID:" + pubmedID + ")"; + return search(query.toString(), count, start); + } + + private class SearchByQueryCallable implements Callable> { + + private Query query; + + private SearchByQueryCallable(String queryString, Integer maxResult, Integer start) { + query = new Query(); + query.addParameter("query", queryString); + query.addParameter("count", maxResult); + query.addParameter("start", start); + } + + private SearchByQueryCallable(Query query) { + this.query = query; + } + + @Override + public List call() throws Exception { + Integer count = query.getParameterAsClass("count", Integer.class); + Integer start = query.getParameterAsClass("start", Integer.class); + String queryString = query.getParameterAsClass("query", String.class); + return search(queryString, count, start); + + } + } + + private class SearchByIdCallable implements Callable> { + private Query query; + + private SearchByIdCallable(Query query) { + this.query = query; + } + + private SearchByIdCallable(String id) { + this.query = new Query(); + query.addParameter("id", id); + } + + @Override + public List call() throws Exception { + return getByPubmedEuropeID(query.getParameterAsClass("id", String.class), 1 ,0); + } + } + + public class FindMatchingRecordCallable implements Callable> { + + private Query query; + + private FindMatchingRecordCallable(Query q) { + query = q; + } + + @Override + public List call() throws Exception { + String title = query.getParameterAsClass("title", String.class); + String author = query.getParameterAsClass("author", String.class); + Integer year = query.getParameterAsClass("year", Integer.class); + Integer maxResult = query.getParameterAsClass("maxResult", Integer.class); + Integer start = query.getParameterAsClass("start", Integer.class); + return search(title, author, year, maxResult, start); + } + + } + + private class CountByQueryCallable implements Callable { + private Query query; + + + private CountByQueryCallable(String queryString) { + query = new Query(); + query.addParameter("query", queryString); + } + + private CountByQueryCallable(Query query) { + this.query = query; + } + + @Override + public Integer call() throws Exception { + try { + return count(query.getParameterAsClass("query", String.class)); + } catch (Exception e) { + throw new RuntimeException(); + } + } + } + + public Integer count(String query) throws URISyntaxException, ClientProtocolException, IOException, JaxenException { + try { + String response = liveImportClient.executeHttpGetRequest(1000, buildURI(1, query), + new HashMap()); + SAXBuilder saxBuilder = new SAXBuilder(); + Document document = saxBuilder.build(new StringReader(response)); + Element root = document.getRootElement(); + Element element = root.getChild("hitCount"); + return Integer.parseInt(element.getValue()); + } catch (JDOMException e) { + log.error(e.getMessage(), e); + throw new RuntimeException(e.getMessage(), e); + } + } + + public List search(String title, String author, int year, int count, int start) + throws HttpException, IOException { + StringBuffer query = new StringBuffer(); + query.append("("); + if (StringUtils.isNotBlank(title)) { + query.append("TITLE:").append(title); + query.append(")"); + } + if (StringUtils.isNotBlank(author)) { + String splitRegex = "(\\s*,\\s+|\\s*;\\s+|\\s*;+|\\s*,+|\\s+)"; + String[] authors = author.split(splitRegex); + // [FAU] + if (query.length() > 0) { + query.append(" AND "); + } + query.append("("); + int x = 0; + for (String auth : authors) { + x++; + query.append("AUTH:\"").append(auth).append("\""); + if (x < authors.length) { + query.append(" AND "); + } + } + query.append(")"); + } + if (year != -1) { + // [DP] + if (query.length() > 0) { + query.append(" AND "); + } + query.append("( PUB_YEAR:").append(year).append(")"); + } + query.append(")"); + return search(query.toString(), count, start); + } + + public List search(String query, Integer count, Integer start) throws IOException, HttpException { + List results = new ArrayList<>(); + try { + URIBuilder uriBuilder = new URIBuilder("https://www.ebi.ac.uk/europepmc/webservices/rest/search"); + uriBuilder.addParameter("format", "xml"); + uriBuilder.addParameter("resulttype", "core"); + uriBuilder.addParameter("pageSize", String.valueOf(count)); + uriBuilder.addParameter("query", query); + boolean lastPage = false; + int skipped = 0; + while (!lastPage || results.size() < count) { + String response = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), + new HashMap()); + SAXBuilder saxBuilder = new SAXBuilder(); + Document document = saxBuilder.build(new StringReader(response)); + XPathFactory xpfac = XPathFactory.instance(); + XPathExpression xPath = xpfac.compile("//responseWrapper/resultList/result",Filters.element()); + List records = xPath.evaluate(document); + if (records.size() > 0) { + for (Element item : records) { + if (start > skipped) { + skipped++; + } else { + results.add(transformSourceRecords(item)); + } + if (results.size() == count) { + break; + } + } + } else { + lastPage = true; + break; + } + Element root = document.getRootElement(); + Element nextCursorMark = root.getChild("nextCursorMark"); + String cursorMark = Objects.nonNull(nextCursorMark) ? nextCursorMark.getValue() : StringUtils.EMPTY; + if (!"*".equals(cursorMark)) { + uriBuilder.setParameter("cursorMar", cursorMark); + } else { + lastPage = true; + } + } + } catch (URISyntaxException | JDOMException e) { + log.error(e.getMessage(), e); + throw new RuntimeException(e.getMessage(), e); + } + return results; + } + + private String buildURI(Integer pageSize, String query) throws URISyntaxException { + URIBuilder uriBuilder = new URIBuilder(ENDPOINT_SEARCH); + uriBuilder.addParameter("format", "xml"); + uriBuilder.addParameter("resulttype", "core"); + uriBuilder.addParameter("pageSize", String.valueOf(pageSize)); + uriBuilder.addParameter("query", query); + return uriBuilder.toString(); + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml b/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml index 83b5439ae39a..c9e901d89fed 100644 --- a/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml +++ b/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml @@ -121,9 +121,19 @@ - + + + + + + + + + + + diff --git a/dspace/config/spring/api/pubmedeurope-integration.xml b/dspace/config/spring/api/pubmedeurope-integration.xml new file mode 100644 index 000000000000..3cd8e4908293 --- /dev/null +++ b/dspace/config/spring/api/pubmedeurope-integration.xml @@ -0,0 +1,184 @@ + + + + + + + + Defines which metadatum is mapped on which metadatum. Note that while the key must be unique it + only matters here for postprocessing of the value. The mapped MetadatumContributor has full control over + what metadatafield is generated. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From d2f62939f8ce4c7bcd56bdca38119ca32ca6cd24 Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Wed, 30 Mar 2022 21:36:37 +0200 Subject: [PATCH 0109/1846] [CST-5303] added test for pubmedeurope --- .../assetstore/pubmedeurope-empty.xml | 14 + .../assetstore/pubmedeurope-test.xml | 378 ++++++++++++++++++ .../PubmedEuropeMetadataSourceServiceIT.java | 248 ++++++++++++ .../src/test/resources/test-config.properties | 4 +- .../config/spring/api/external-services.xml | 11 + 5 files changed, 654 insertions(+), 1 deletion(-) create mode 100644 dspace-api/src/test/data/dspaceFolder/assetstore/pubmedeurope-empty.xml create mode 100644 dspace-api/src/test/data/dspaceFolder/assetstore/pubmedeurope-test.xml create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/PubmedEuropeMetadataSourceServiceIT.java diff --git a/dspace-api/src/test/data/dspaceFolder/assetstore/pubmedeurope-empty.xml b/dspace-api/src/test/data/dspaceFolder/assetstore/pubmedeurope-empty.xml new file mode 100644 index 000000000000..2c432eb83248 --- /dev/null +++ b/dspace-api/src/test/data/dspaceFolder/assetstore/pubmedeurope-empty.xml @@ -0,0 +1,14 @@ + + + 6.7 + 0 + + chortkiv&cursorMar + core + * + 25 + + false + + + \ No newline at end of file diff --git a/dspace-api/src/test/data/dspaceFolder/assetstore/pubmedeurope-test.xml b/dspace-api/src/test/data/dspaceFolder/assetstore/pubmedeurope-test.xml new file mode 100644 index 000000000000..e1c4dfab4c64 --- /dev/null +++ b/dspace-api/src/test/data/dspaceFolder/assetstore/pubmedeurope-test.xml @@ -0,0 +1,378 @@ + + + 6.7 + 3 + + chortkiv + core + * + 25 + + false + + + + 21733903 + MED + 21733903 + PMC3234553 + + PMC3234553 + + 10.1098/rspb.2011.0943 + First record of preserved soft parts in a Palaeozoic podocopid (Metacopina) ostracod, Cytherellina submagna: phylogenetic implications. + Olempska E, Horne DJ, Szaniawski H. + + + Olempska E + E + Olempska + E + 0000-0003-0310-3876 + + + Institute of Paleobiology, Polish Academy of Sciences, 00-818 Warszawa, Poland. olempska@twarda.pan.pl + + + + + Horne DJ + D J + Horne + DJ + 0000-0002-2148-437X + + + Szaniawski H + H + Szaniawski + H + + + + 0000-0002-2148-437X + 0000-0003-0310-3876 + + + 1728 + 279 + 1890685 + 2012 Feb + 2 + 2012 + 2012-02-01 + + Proceedings. Biological sciences + Proc Biol Sci + Proc Biol Sci + 101245157 + 0962-8452 + 1471-2954 + + + 2012 + 564-570 + The metacopines represent one of the oldest and most important extinct groups of ostracods, with a fossil record from the Mid-Ordovician to the Early Jurassic. Herein, we report the discovery of a representative of the group with three-dimensionally preserved soft parts. The specimen--a male of Cytherellina submagna--was found in the Early Devonian (416 Ma) of Podolia, Ukraine. A branchial plate (Bp) of the cephalic maxillula (Mx), a pair of thoracic appendages (walking legs), a presumed furca (Fu) and a copulatory organ are preserved. The material also includes phosphatized steinkerns with exceptionally preserved marginal pore canals and muscle scars. The morphology of the preserved limbs and valves of C. submagna suggests its relationship with extant Podocopida, particularly with the superfamilies Darwinuloidea and Sigillioidea, which have many similar characteristic features, including a large Bp on the Mx, the morphology of walking legs, Fu with two terminal claws, internal stop-teeth in the left valve, adductor muscle scar pattern, and a very narrow fused zone along the anterior and posterior margins. More precise determination of affinities will depend on the soft-part morphology of the cephalic segment, which has not been revealed in the present material. + Institute of Paleobiology, Polish Academy of Sciences, 00-818 Warszawa, Poland. olempska@twarda.pan.pl + ppublish + eng + Print-Electronic + + Research Support, Non-U.S. Gov't + research-article + Journal Article + + + + N + Animals + + + N + Crustacea + + + AH + anatomy & histology + Y + + + CL + classification + Y + + + + + Y + Phylogeny + + + Y + Fossils + + + N + Ukraine + + + N + Male + + + + + IM + Index Medicus + + + + + Free + F + pdf + PubMedCentral + https://www.ncbi.nlm.nih.gov/pmc/articles/pmid/21733903/pdf/?tool=EBI + + + Free + F + html + PubMedCentral + https://www.ncbi.nlm.nih.gov/pmc/articles/pmid/21733903/?tool=EBI + + + Subscription required + S + doi + DOI + https://doi.org/10.1098/rspb.2011.0943 + + + Free + F + html + Europe_PMC + https://europepmc.org/articles/PMC3234553 + + + Free + F + pdf + Europe_PMC + https://europepmc.org/articles/PMC3234553?pdf=render + + + Free + F + doi + DOI + https://doi.org/10.1098/rspb.2011.0943 + + + N + Y + Y + Y + N + N + 2 + N + Y + Y + N + N + N + N + N + N + 2012-04-19 + 2011-07-08 + 2011-07-08 + 2020-07-11 + 2021-10-20 + 2011-07-06 + 2011-07-06 + + + UA37818 + PAT + VODKA SYNEVIR + BARANOV VALENTYN VOLODYMYROVYC, DANCHAK ROMAN ADAMOVYCH, FEDORCHUK NATALIIA HRYHORIVNA, FEDOREIKO LIUBOV ROMANIVNA. + + + BARANOV VALENTYN VOLODYMYROVYC + BARANOV VALENTYN VOLODYMYROVYC + + + DANCHAK ROMAN ADAMOVYCH + DANCHAK ROMAN ADAMOVYCH + + + FEDORCHUK NATALIIA HRYHORIVNA + FEDORCHUK NATALIIA HRYHORIVNA + + + FEDOREIKO LIUBOV ROMANIVNA + FEDOREIKO LIUBOV ROMANIVNA + + + 2004 + Vodka contains aqueous-alcoholic mixture, 65.8 % sugar syrup and apple vinegar. As a result the vodka has light taste without vodka acid and light apple aroma. + CHORTKIV DISTILLARY + eng + + Patent + + + UA + C2 + + + C12G3/06 + IPC + http://www.cooperativepatentclassification.org/cpc/scheme/C/scheme-C12G.pdf + + + + UA20000042245 + 2000-04-19 + 0 + + + + UA20000042245 + 2000-04-19 + 1 + + + + + + Free + F + html + SureChembl + https://www.surechembl.org/document/UA-37818-U + + + Free + F + html + EPO + http://v3.espacenet.com/textdoc?DB=EPODOC&IDX=UA37818 + + + N + N + N + N + N + N + 0 + N + N + Y + N + N + N + N + N + N + 2004-12-15 + 2000-04-19 + 2010-11-01 + 2000-04-19 + + + UA37954 + PAT + A VODKA CHARKA + BARANOV VALENTYN VOLODYMYROVYC, DANCHAK ROMAN ADAMOVYCH, FEDORCHUK NATALIIA HRYHORIVNA, FEDOREIKO LIUBOV ROMANIVNA. + + + BARANOV VALENTYN VOLODYMYROVYC + BARANOV VALENTYN VOLODYMYROVYC + + + DANCHAK ROMAN ADAMOVYCH + DANCHAK ROMAN ADAMOVYCH + + + FEDORCHUK NATALIIA HRYHORIVNA + FEDORCHUK NATALIIA HRYHORIVNA + + + FEDOREIKO LIUBOV ROMANIVNA + FEDOREIKO LIUBOV ROMANIVNA + + + 2005 + The invention relates to food industry, and particularly to liqueur and vodka industry, to vodkas compositions. The aim of this invention is producing vodka with high organoleptic indices, and particularly soft taste without vodka bitterness and without vodka aroma, and high biological properties, by selection of necessary ingredients at required quantities. Ingredients ratio at 100 l of finished drink: Carbohydrate module ôAlkosoftö, l 0.07-0.13Citric oil, kg 0,0015-0,0025 Aqueous-alcoholic mixture as calculated per strength of 40% of volume rest. Technical result - preparation of the vodka of given composition with a strength of 40% of volume, which is transparent, colorless, has mild taste without vodka bitterness and without strong vodka aroma which will not cause alcohol withdrawal syndrome and high charge on the body. + CHORTKIV DISTILLARY + eng + + Patent + + + UA + C2 + + + C12G3/06 + IPC + http://www.cooperativepatentclassification.org/cpc/scheme/C/scheme-C12G.pdf + + + + UA20000052634 + 2000-05-10 + 0 + + + + UA20000052634 + 2000-05-10 + 1 + + + + + + Free + F + html + SureChembl + https://www.surechembl.org/document/UA-37954-U + + + Free + F + html + EPO + http://v3.espacenet.com/textdoc?DB=EPODOC&IDX=UA37954 + + + N + N + N + N + N + N + 0 + N + N + Y + N + N + N + N + N + N + 2005-02-15 + 2000-05-10 + 2010-10-18 + 2000-05-10 + + + \ No newline at end of file diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/PubmedEuropeMetadataSourceServiceIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/PubmedEuropeMetadataSourceServiceIT.java new file mode 100644 index 000000000000..78a177737023 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/PubmedEuropeMetadataSourceServiceIT.java @@ -0,0 +1,248 @@ +/** + * 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.rest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.FileInputStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +import org.apache.commons.io.IOUtils; +import org.apache.http.ProtocolVersion; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.entity.BasicHttpEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.tools.ant.filters.StringInputStream; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.importer.external.datamodel.ImportRecord; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; +import org.dspace.importer.external.pubmedeurope.PubmedEuropeMetadataSourceServiceImpl; +import org.dspace.importer.external.scopus.service.LiveImportClientImpl; +import org.junit.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Integration tests for {@link PubmedEuropeMetadataSourceServiceImpl} + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com) + */ +public class PubmedEuropeMetadataSourceServiceIT extends AbstractControllerIntegrationTest { + + @Autowired + private PubmedEuropeMetadataSourceServiceImpl pubmedEuropeMetadataServiceImpl; + + @Autowired + private LiveImportClientImpl liveImportClientImpl; + + @Test + public void pubmedEuropeImportMetadataGetRecordsTest() throws Exception { + context.turnOffAuthorisationSystem(); + + CloseableHttpClient originalHttpClient = liveImportClientImpl.getHttpClient(); + CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + String path = testProps.get("test.pubmedeurope").toString(); + String path2file = testProps.get("test.pubmedeurope-empty").toString(); + try (FileInputStream file = new FileInputStream(path)) { + try (FileInputStream file2 = new FileInputStream(path2file)) { + String xmlMetricsExample = IOUtils.toString(file, Charset.defaultCharset()); + String xmlMetricsExample2 = IOUtils.toString(file2, Charset.defaultCharset()); + + liveImportClientImpl.setHttpClient(httpClient); + + CloseableHttpResponse response = mockResponse(xmlMetricsExample, 200, "OK"); + CloseableHttpResponse response2 = mockResponse(xmlMetricsExample2, 200, "OK"); + + when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response, response2); + + context.restoreAuthSystemState(); + Collection recordsImported = pubmedEuropeMetadataServiceImpl.getRecords("test query", 0, 3); + Collection collection2match = getRecords(); + assertEquals(3, recordsImported.size()); + assertTrue(matchRecords(recordsImported, collection2match)); + } + } finally { + liveImportClientImpl.setHttpClient(originalHttpClient); + } + } + + private boolean matchRecords(Collection recordsImported, Collection records2match) { + ImportRecord firstImported = recordsImported.iterator().next(); + ImportRecord secondImported = recordsImported.iterator().next(); + ImportRecord first2match = recordsImported.iterator().next(); + ImportRecord second2match = recordsImported.iterator().next(); + boolean checkFirstRecord = firstImported.getValueList().containsAll(first2match.getValueList()); + boolean checkSecondRecord = secondImported.getValueList().containsAll(second2match.getValueList()); + return checkFirstRecord && checkSecondRecord; + } + + private CloseableHttpResponse mockResponse(String xmlExample, int statusCode, String reason) + throws UnsupportedEncodingException { + BasicHttpEntity basicHttpEntity = new BasicHttpEntity(); + basicHttpEntity.setChunked(true); + basicHttpEntity.setContent(new StringInputStream(xmlExample)); + + CloseableHttpResponse response = mock(CloseableHttpResponse.class); + when(response.getStatusLine()).thenReturn(statusLine(statusCode, reason)); + when(response.getEntity()).thenReturn(basicHttpEntity); + return response; + } + + private Collection getRecords() { + Collection records = new LinkedList(); + List metadatums = new ArrayList(); + //define first record + MetadatumDTO title = createMetadatumDTO("dc","title", null, + "First record of preserved soft parts in a Palaeozoic podocopid" + + " (Metacopina) ostracod, Cytherellina submagna: phylogenetic implications."); + MetadatumDTO contributor = createMetadatumDTO("dc", "contributor", "author", "Olempska E"); + MetadatumDTO contributor2 = createMetadatumDTO("dc", "contributor", "author", "Horne DJ"); + MetadatumDTO contributor3 = createMetadatumDTO("dc", "contributor", "author", "Szaniawski H"); + MetadatumDTO doi = createMetadatumDTO("dc", "identifier", "doi", "10.1098/rspb.2011.0943"); + MetadatumDTO source = createMetadatumDTO("dc", "source", null, "Proceedings. Biological sciences"); + MetadatumDTO date = createMetadatumDTO("dc", "date", "issued", "2012"); + MetadatumDTO language = createMetadatumDTO("dc", "language", "iso", "eng"); + MetadatumDTO type = createMetadatumDTO("dc", "type", null, "Research Support, Non-U.S. Gov't"); + MetadatumDTO type2 = createMetadatumDTO("dc", "type", null, "research-article"); + MetadatumDTO type3 = createMetadatumDTO("dc", "type", null, "Journal Article"); + + MetadatumDTO issn = createMetadatumDTO("dc", "identifier", "issn", "0962-8452"); + MetadatumDTO pmid = createMetadatumDTO("dc", "identifier", "pmid", "21733903"); + MetadatumDTO description = createMetadatumDTO("dc", "description", "abstract", "The metacopines represent one" + + " of the oldest and most important extinct groups of ostracods, with a fossil record from" + + " the Mid-Ordovician to the Early Jurassic. Herein, we report the discovery of a representative" + + " of the group with three-dimensionally preserved soft parts. The specimen--a male of Cytherellina" + + " submagna--was found in the Early Devonian (416 Ma) of Podolia, Ukraine. A branchial plate (Bp)" + + " of the cephalic maxillula (Mx), a pair of thoracic appendages (walking legs), a presumed furca" + + " (Fu) and a copulatory organ are preserved. The material also includes phosphatized steinkerns" + + " with exceptionally preserved marginal pore canals and muscle scars. The morphology of the" + + " preserved limbs and valves of C. submagna suggests its relationship with extant Podocopida," + + " particularly with the superfamilies Darwinuloidea and Sigillioidea, which have many similar" + + " characteristic features, including a large Bp on the Mx, the morphology of walking legs, Fu" + + " with two terminal claws, internal stop-teeth in the left valve, adductor muscle scar pattern," + + " and a very narrow fused zone along the anterior and posterior margins. More precise" + + " determination of affinities will depend on the soft-part morphology of the cephalic segment," + + " which has not been revealed in the present material."); + + metadatums.add(title); + metadatums.add(contributor); + metadatums.add(contributor2); + metadatums.add(contributor3); + metadatums.add(doi); + metadatums.add(source); + metadatums.add(date); + metadatums.add(language); + metadatums.add(type); + metadatums.add(type2); + metadatums.add(type3); + metadatums.add(issn); + metadatums.add(pmid); + metadatums.add(description); + ImportRecord firstrRecord = new ImportRecord(metadatums); + + //define second record + List metadatums2 = new ArrayList(); + MetadatumDTO title2 = createMetadatumDTO("dc","title", null, "VODKA SYNEVIR"); + MetadatumDTO contributor4 = createMetadatumDTO("dc", "contributor", "author", "BARANOV VALENTYN VOLODYMYROVYC"); + MetadatumDTO contributor5 = createMetadatumDTO("dc", "contributor", "author", "DANCHAK ROMAN ADAMOVYCH"); + MetadatumDTO contributor6 = createMetadatumDTO("dc", "contributor", "author", "FEDORCHUK NATALIIA HRYHORIVNA"); + MetadatumDTO contributor7 = createMetadatumDTO("dc", "contributor", "author", "FEDOREIKO LIUBOV ROMANIVNA"); + MetadatumDTO language2 = createMetadatumDTO("dc", "language", "iso", "eng"); + MetadatumDTO type4 = createMetadatumDTO("dc", "type", null, "Patent"); + MetadatumDTO pmid2 = createMetadatumDTO("dc", "identifier", "pmid", "UA37818"); + MetadatumDTO description2 = createMetadatumDTO("dc", "description", "abstract", + "Vodka contains aqueous-alcoholic" + + " mixture, 65.8 % sugar syrup and apple vinegar." + + " As a result the vodka has light taste without vodka acid and light apple aroma."); + metadatums2.add(title2); + metadatums2.add(contributor4); + metadatums2.add(contributor5); + metadatums2.add(contributor6); + metadatums2.add(contributor7); + metadatums2.add(language2); + metadatums2.add(type4); + metadatums2.add(pmid2); + metadatums2.add(description2); + ImportRecord secondRecord = new ImportRecord(metadatums2); + + //define second record + List metadatums3 = new ArrayList(); + MetadatumDTO title3 = createMetadatumDTO("dc","title", null, "A VODKA CHARKA"); + MetadatumDTO contributor8 = createMetadatumDTO("dc", "contributor", "author", "BARANOV VALENTYN VOLODYMYROVYC"); + MetadatumDTO contributor9 = createMetadatumDTO("dc", "contributor", "author", "HDANCHAK ROMAN ADAMOVYCH"); + MetadatumDTO contributor10 = createMetadatumDTO("dc", "contributor", "author", "FEDORCHUK NATALIIA HRYHORIVNA"); + MetadatumDTO contributor11 = createMetadatumDTO("dc", "contributor", "author", "FEDOREIKO LIUBOV ROMANIVNA"); + MetadatumDTO language3 = createMetadatumDTO("dc", "language", "iso", "eng"); + MetadatumDTO type5 = createMetadatumDTO("dc", "type", null, "Patent"); + MetadatumDTO pmid3 = createMetadatumDTO("dc", "identifier", "pmid", "UA37954"); + MetadatumDTO description3 = createMetadatumDTO("dc", "description", "abstract", "The invention relates to" + + " food industry, and particularly to liqueur and vodka industry, to vodkas compositions." + + " The aim of this invention is producing vodka with high organoleptic indices, and particularly" + + " soft taste without vodka bitterness and without vodka aroma, and high biological properties," + + " by selection of necessary ingredients at required quantities. Ingredients ratio at 100 l of" + + " finished drink: Carbohydrate module ôAlkosoftö, l 0.07-0.13Citric oil, kg 0,0015-0,0025" + + " Aqueous-alcoholic mixture as calculated per strength of 40% of volume rest." + + " Technical result - preparation of the vodka of given composition with a strength of 40% of" + + " volume, which is transparent, colorless, has mild taste without vodka bitterness and without" + + " strong vodka aroma which will not cause alcohol withdrawal syndrome and high charge on the body."); + metadatums3.add(title3); + metadatums3.add(contributor8); + metadatums3.add(contributor9); + metadatums3.add(contributor10); + metadatums3.add(contributor11); + metadatums3.add(language3); + metadatums3.add(type5); + metadatums3.add(pmid3); + metadatums3.add(description3); + ImportRecord thirdRecord = new ImportRecord(metadatums3); + + records.add(firstrRecord); + records.add(secondRecord); + records.add(thirdRecord); + return records; + } + + private MetadatumDTO createMetadatumDTO(String schema, String element, String qualifier, String value) { + MetadatumDTO metadatumDTO = new MetadatumDTO(); + metadatumDTO.setSchema(schema); + metadatumDTO.setElement(element); + metadatumDTO.setQualifier(qualifier); + metadatumDTO.setValue(value); + return metadatumDTO; + } + + private StatusLine statusLine(int statusCode, String reason) { + return new StatusLine() { + @Override + public ProtocolVersion getProtocolVersion() { + return new ProtocolVersion("http", 1, 1); + } + + @Override + public int getStatusCode() { + return statusCode; + } + + @Override + public String getReasonPhrase() { + return reason; + } + }; + } + +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/resources/test-config.properties b/dspace-server-webapp/src/test/resources/test-config.properties index 8d49daaddfb1..ac04d032d6f9 100644 --- a/dspace-server-webapp/src/test/resources/test-config.properties +++ b/dspace-server-webapp/src/test/resources/test-config.properties @@ -15,4 +15,6 @@ test.bitstream = ./target/testing/dspace/assetstore/ConstitutionofIreland.pdf #Path for a test Taskfile for the curate script test.curateTaskFile = ./target/testing/dspace/assetstore/curate.txt test.scopus = ./target/testing/dspace/assetstore/scopus-ex.xml -test.scopus-empty = ./target/testing/dspace/assetstore/scopus-empty-resp.xml \ No newline at end of file +test.scopus-empty = ./target/testing/dspace/assetstore/scopus-empty-resp.xml +test.pubmedeurope = ./target/testing/dspace/assetstore/pubmedeurope-test.xml +test.pubmedeurope-empty = ./target/testing/dspace/assetstore/pubmedeurope-empty.xml \ No newline at end of file diff --git a/dspace/config/spring/api/external-services.xml b/dspace/config/spring/api/external-services.xml index af8af57835b6..35c238135129 100644 --- a/dspace/config/spring/api/external-services.xml +++ b/dspace/config/spring/api/external-services.xml @@ -105,5 +105,16 @@ + + + + + + + Publication + + + + From 9cde585afcb718b386f3520a37cf7b113fdf26aa Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Thu, 31 Mar 2022 22:39:52 +0200 Subject: [PATCH 0110/1846] [CST-5303] porting of wos --- .../contributor/SimpleConcatContributor.java | 46 ++ .../SimpleMultiplePathContributor.java | 55 ++ ...XpathMetadatumAndAttributeContributor.java | 56 +++ .../WosAttribute2ValueContributor.java | 152 ++++++ .../contributor/WosIdentifierContributor.java | 69 +++ .../WosIdentifierRidContributor.java | 65 +++ .../external/wos/service/WOSFieldMapping.java | 37 ++ .../WOSImportMetadataSourceServiceImpl.java | 270 ++++++++++ .../dspaceFolder/assetstore/scopus-ex.xml | 2 - .../dspaceFolder/assetstore/wos-responce.xml | 469 ++++++++++++++++++ .../AbstractLiveImportIntegrationTest.java | 80 +++ .../PubmedEuropeMetadataSourceServiceIT.java | 81 +-- .../ScopusImportMetadataSourceServiceIT.java | 184 +++---- .../WOSImportMetadataSourceServiceIT.java | 171 +++++++ .../src/test/resources/test-config.properties | 3 +- dspace/config/dspace.cfg | 4 + dspace/config/spring/api/wos-integration.xml | 291 +++++++++++ 17 files changed, 1882 insertions(+), 153 deletions(-) create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleConcatContributor.java create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleMultiplePathContributor.java create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumAndAttributeContributor.java create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/WosAttribute2ValueContributor.java create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/WosIdentifierContributor.java create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/WosIdentifierRidContributor.java create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/wos/service/WOSFieldMapping.java create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/wos/service/WOSImportMetadataSourceServiceImpl.java create mode 100644 dspace-api/src/test/data/dspaceFolder/assetstore/wos-responce.xml create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/AbstractLiveImportIntegrationTest.java create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/WOSImportMetadataSourceServiceIT.java create mode 100644 dspace/config/spring/api/wos-integration.xml diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleConcatContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleConcatContributor.java new file mode 100644 index 000000000000..e52f3ec3c30d --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleConcatContributor.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.importer.external.metadatamapping.contributor; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; +import org.jdom2.Element; +import org.jdom2.Namespace; + +/** + * This contributor is able to concat multi value. + * Given a certain path, if it contains several nodes, + * the values of nodes will be concatenated into a single one + * + * @author Boychuk Mykhaylo (boychuk.mykhaylo at 4Science dot it) + */ +public class SimpleConcatContributor extends SimpleXpathMetadatumContributor { + + @Override + public Collection contributeMetadata(Element element) { + List values = new LinkedList<>(); + StringBuilder text = new StringBuilder(); + for (String ns : prefixToNamespaceMapping.keySet()) { + List nodes = element.getChildren(query, Namespace.getNamespace(ns)); + for (Element el : nodes) { + if (StringUtils.isNotBlank(el.getValue())) { + text.append(element.getText()); + } + } + } + if (StringUtils.isNotBlank(text.toString())) { + values.add(metadataFieldMapping.toDCValue(field, text.toString())); + } + return values; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleMultiplePathContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleMultiplePathContributor.java new file mode 100644 index 000000000000..8356101b01b2 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleMultiplePathContributor.java @@ -0,0 +1,55 @@ +/** + * 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.importer.external.metadatamapping.contributor; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +import org.dspace.importer.external.metadatamapping.MetadatumDTO; +import org.jdom2.Element; +import org.jdom2.Namespace; + +/** + * This contributor can perform research on multi-paths + * + * @author Boychuk Mykhaylo (boychuk.mykhaylo at 4Science dot it) + */ +public class SimpleMultiplePathContributor extends SimpleXpathMetadatumContributor { + + private List paths; + + public SimpleMultiplePathContributor() {} + + public SimpleMultiplePathContributor(List paths) { + this.paths = paths; + } + + @Override + public Collection contributeMetadata(Element element) { + List values = new LinkedList<>(); + for (String path : this.paths) { + for (String ns : prefixToNamespaceMapping.keySet()) { + List nodes = element.getChildren(path, Namespace.getNamespace(ns)); + for (Element el : nodes) { + values.add(metadataFieldMapping.toDCValue(field, el.getValue())); + } + } + } + return values; + } + + public List getPaths() { + return paths; + } + + public void setPaths(List paths) { + this.paths = paths; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumAndAttributeContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumAndAttributeContributor.java new file mode 100644 index 000000000000..122961953d82 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumAndAttributeContributor.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.importer.external.metadatamapping.contributor; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.core.Constants; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; +import org.jdom2.Element; +import org.jdom2.Namespace; + +/** + * This contributor checks for each node returned for the supplied path + * if node contains supplied attribute - the value of the current node is taken, + * otherwise #PLACEHOLDER_PARENT_METADATA_VALUE# + * + * @author Boychuk Mykhaylo (boychuk.mykhaylo at 4Science dot it) + */ +public class SimpleXpathMetadatumAndAttributeContributor extends SimpleXpathMetadatumContributor { + + private String attribute; + + @Override + public Collection contributeMetadata(Element element) { + List values = new LinkedList<>(); + for (String ns : prefixToNamespaceMapping.keySet()) { + List nodes = element.getChildren(query, Namespace.getNamespace(ns)); + for (Element el : nodes) { + String attributeValue = el.getAttributeValue(this.attribute); + if (StringUtils.isNotBlank(attributeValue)) { + values.add(metadataFieldMapping.toDCValue(this.field, attributeValue)); + } else { + values.add(metadataFieldMapping.toDCValue(this.field, Constants.PLACEHOLDER_PARENT_METADATA_VALUE)); + } + } + } + return values; + } + + public String getAttribute() { + return attribute; + } + + public void setAttribute(String attribute) { + this.attribute = attribute; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/WosAttribute2ValueContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/WosAttribute2ValueContributor.java new file mode 100644 index 000000000000..1c6a1f887507 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/WosAttribute2ValueContributor.java @@ -0,0 +1,152 @@ +/** + * 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.importer.external.metadatamapping.contributor; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import javax.annotation.Resource; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.importer.external.metadatamapping.MetadataFieldConfig; +import org.dspace.importer.external.metadatamapping.MetadataFieldMapping; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; +import org.jaxen.JaxenException; +import org.jdom2.Element; +import org.jdom2.Namespace; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This contributor checks for each node returned for the given path if the node contains "this.attribute" + * and then checks if the attribute value is one of the values configured + * in the "this.attributeValue2metadata" map, if the value of the current known is taken. + * If "this.firstChild" is true, it takes the value of the child of the known. + * + * @author Boychuk Mykhaylo (boychuk.mykhaylo at 4Science dot it) + */ +public class WosAttribute2ValueContributor implements MetadataContributor { + + private static final Logger log = LoggerFactory.getLogger(WosAttribute2ValueContributor.class); + + private String query; + + private String attribute; + + private boolean firstChild; + + private String childName; + + private Map prefixToNamespaceMapping; + + private Map attributeValue2metadata; + + private MetadataFieldMapping> metadataFieldMapping; + + public WosAttribute2ValueContributor() {} + + public WosAttribute2ValueContributor(String query, + Map prefixToNamespaceMapping, + Map attributeValue2metadata) { + this.query = query; + this.prefixToNamespaceMapping = prefixToNamespaceMapping; + this.attributeValue2metadata = attributeValue2metadata; + } + + @Override + public Collection contributeMetadata(Element element) { + List values = new LinkedList<>(); + try { + for (String ns : prefixToNamespaceMapping.keySet()) { + List nodes = element.getChildren(query, Namespace.getNamespace(ns)); + for (Element el : nodes) { + String attributeValue = el.getAttributeValue(this.attribute); + setField(attributeValue, element, values); + } + } + return values; + } catch (JaxenException e) { + log.warn(query, e); + throw new RuntimeException(e); + } + } + + private void setField(String attributeValue, Element el, List values) throws JaxenException { + for (String id : attributeValue2metadata.keySet()) { + if (StringUtils.equals(id, attributeValue)) { + if (this.firstChild) { + String value = ""; + //el.getFirstChildWithName(new QName(this.childName)).getText(); + values.add(metadataFieldMapping.toDCValue(attributeValue2metadata.get(id), value)); + } else { + values.add(metadataFieldMapping.toDCValue(attributeValue2metadata.get(id), el.getText())); + } + } + } + } + + public MetadataFieldMapping> getMetadataFieldMapping() { + return metadataFieldMapping; + } + + public void setMetadataFieldMapping( + MetadataFieldMapping> metadataFieldMapping) { + this.metadataFieldMapping = metadataFieldMapping; + } + + @Resource(name = "isiFullprefixMapping") + public void setPrefixToNamespaceMapping(Map prefixToNamespaceMapping) { + this.prefixToNamespaceMapping = prefixToNamespaceMapping; + } + + public Map getPrefixToNamespaceMapping() { + return prefixToNamespaceMapping; + } + + public String getAttribute() { + return attribute; + } + + public void setAttribute(String attribute) { + this.attribute = attribute; + } + + public Map getAttributeValue2metadata() { + return attributeValue2metadata; + } + + public void setAttributeValue2metadata(Map attributeValue2metadata) { + this.attributeValue2metadata = attributeValue2metadata; + } + + public String getQuery() { + return query; + } + + public void setQuery(String query) { + this.query = query; + } + + public boolean isFirstChild() { + return firstChild; + } + + public void setFirstChild(boolean firstChild) { + this.firstChild = firstChild; + } + + public String getChildName() { + return childName; + } + + public void setChildName(String childName) { + this.childName = childName; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/WosIdentifierContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/WosIdentifierContributor.java new file mode 100644 index 000000000000..a5da4bf028f8 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/WosIdentifierContributor.java @@ -0,0 +1,69 @@ +/** + * 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.importer.external.metadatamapping.contributor; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.importer.external.metadatamapping.MetadataFieldConfig; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; +import org.jdom2.Element; +import org.jdom2.Namespace; +import org.jdom2.filter.Filters; +import org.jdom2.xpath.XPathExpression; +import org.jdom2.xpath.XPathFactory; + +/** + * This contributor can retrieve the identifiers + * configured in "this.identifire2field" from the WOS response. + * + * @author Boychuk Mykhaylo (boychuk.mykhaylo at 4Science dot it) + */ +public class WosIdentifierContributor extends SimpleXpathMetadatumContributor { + + protected Map identifire2field; + + @Override + public Collection contributeMetadata(Element element) { + List values = new LinkedList<>(); + List namespaces = new ArrayList<>(); + for (String ns : prefixToNamespaceMapping.keySet()) { + namespaces.add(Namespace.getNamespace(prefixToNamespaceMapping.get(ns), ns)); + } + XPathExpression xpath = + XPathFactory.instance().compile(query, Filters.element(), null, namespaces); + + List nodes = xpath.evaluate(element); + for (Element el : nodes) { + String type = el.getAttributeValue("type"); + setIdentyfire(type, el, values); + } + return values; + } + + private void setIdentyfire(String type, Element el, List values) { + for (String id : identifire2field.keySet()) { + if (StringUtils.equals(id, type)) { + String value = el.getAttributeValue("value"); + values.add(metadataFieldMapping.toDCValue(identifire2field.get(id), value)); + } + } + } + + public Map getIdentifire2field() { + return identifire2field; + } + + public void setIdentifire2field(Map identifire2field) { + this.identifire2field = identifire2field; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/WosIdentifierRidContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/WosIdentifierRidContributor.java new file mode 100644 index 000000000000..7ccefeee54eb --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/WosIdentifierRidContributor.java @@ -0,0 +1,65 @@ +/** + * 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.importer.external.metadatamapping.contributor; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.core.Constants; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; +import org.jaxen.JaxenException; +import org.jdom2.Element; +import org.jdom2.Namespace; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author Boychuk Mykhaylo (boychuk.mykhaylo at 4Science dot it) + */ +public class WosIdentifierRidContributor extends SimpleXpathMetadatumContributor { + + private static final Logger log = LoggerFactory.getLogger(WosIdentifierRidContributor.class); + + @Override + public Collection contributeMetadata(Element element) { + List values = new LinkedList<>(); + try { + for (String ns : prefixToNamespaceMapping.keySet()) { + List nodes = element.getChildren(query, Namespace.getNamespace(ns)); + for (Element el : nodes) { + // Element element2 = el.getFirstChildWithName("name"); + if (Objects.nonNull(element)) { + String type = element.getAttributeValue("role"); + setIdentyfire(type, element, values); + } + } + } + return values; + } catch (JaxenException e) { + log.error(query, e); + throw new RuntimeException(e); + } + } + + private void setIdentyfire(String type, Element el, List values) throws JaxenException { + if (StringUtils.equals("researcher_id", type)) { + String value = el.getAttributeValue("r_id"); + if (StringUtils.isNotBlank(value)) { + values.add(metadataFieldMapping.toDCValue(this.field, value)); + } else { + values.add(metadataFieldMapping.toDCValue(this.field, + Constants.PLACEHOLDER_PARENT_METADATA_VALUE)); + } + } + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/wos/service/WOSFieldMapping.java b/dspace-api/src/main/java/org/dspace/importer/external/wos/service/WOSFieldMapping.java new file mode 100644 index 000000000000..39f409c52003 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/wos/service/WOSFieldMapping.java @@ -0,0 +1,37 @@ +/** + * 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.importer.external.wos.service; +import java.util.Map; +import javax.annotation.Resource; + +import org.dspace.importer.external.metadatamapping.AbstractMetadataFieldMapping; + +/** + * An implementation of {@link AbstractMetadataFieldMapping} + * Responsible for defining the mapping of the WOS metadatum fields on the DSpace metadatum fields + * + * @author Boychuk Mykhaylo (boychuk.mykhaylo at 4science dot it) + */ +@SuppressWarnings("rawtypes") +public class WOSFieldMapping extends AbstractMetadataFieldMapping { + + /** + * Defines which metadatum is mapped on which metadatum. Note that while the key must be unique it + * only matters here for postprocessing of the value. The mapped MetadatumContributor has full control over + * what metadatafield is generated. + * + * @param metadataFieldMap The map containing the link between retrieve + * metadata and metadata that will be set to the item. + */ + @Override + @SuppressWarnings("unchecked") + @Resource(name = "wosMetadataFieldMap") + public void setMetadataFieldMap(Map metadataFieldMap) { + super.setMetadataFieldMap(metadataFieldMap); + } +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/wos/service/WOSImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/wos/service/WOSImportMetadataSourceServiceImpl.java new file mode 100644 index 000000000000..75e006431acb --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/wos/service/WOSImportMetadataSourceServiceImpl.java @@ -0,0 +1,270 @@ +/** + * 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.importer.external.wos.service; + +import java.io.IOException; +import java.io.StringReader; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.Callable; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.el.MethodNotFoundException; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.content.Item; +import org.dspace.importer.external.datamodel.ImportRecord; +import org.dspace.importer.external.datamodel.Query; +import org.dspace.importer.external.exception.MetadataSourceException; +import org.dspace.importer.external.scopus.service.LiveImportClient; +import org.dspace.importer.external.service.AbstractImportMetadataSourceService; +import org.dspace.importer.external.service.DoiCheck; +import org.dspace.importer.external.service.components.QuerySource; +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.JDOMException; +import org.jdom2.filter.Filters; +import org.jdom2.input.SAXBuilder; +import org.jdom2.xpath.XPathExpression; +import org.jdom2.xpath.XPathFactory; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Implements a data source for querying WOS + * + * @author Boychuk Mykhaylo (boychuk.mykhaylo at 4Science dot it) + */ +public class WOSImportMetadataSourceServiceImpl extends AbstractImportMetadataSourceService + implements QuerySource { + + private static final String AI_PATTERN = "^AI=(.*)"; + private static final Pattern ISI_PATTERN = Pattern.compile("^\\d{15}$"); + private static final String ENDPOINT_SEARCH_BY_ID_WOS = "https://wos-api.clarivate.com/api/wos/id/"; + private static final String ENDPOINT_SEARCH_WOS = "https://wos-api.clarivate.com/api/wos/?databaseId=WOS&lang=en&usrQuery="; + + private int timeout = 1000; + + private String apiKey; + + @Autowired + private LiveImportClient liveImportClient; + + @Override + public void init() throws Exception {} + + /** + * The string that identifies this import implementation. Preferable a URI + * + * @return the identifying uri + */ + @Override + public String getImportSource() { + return "wos"; + } + + @Override + public Collection getRecords(String query, int start, int count) throws MetadataSourceException { + return retry(new SearchByQueryCallable(query, count, start)); + } + + @Override + public Collection getRecords(Query query) throws MetadataSourceException { + return retry(new SearchByQueryCallable(query)); + } + + + @Override + public ImportRecord getRecord(Query query) throws MetadataSourceException { + List records = retry(new SearchByQueryCallable(query)); + return records == null || records.isEmpty() ? null : records.get(0); + } + + @Override + public ImportRecord getRecord(String id) throws MetadataSourceException { + List records = retry(new FindByIdCallable(id)); + return records == null || records.isEmpty() ? null : records.get(0); + } + + @Override + public int getRecordsCount(String query) throws MetadataSourceException { + return retry(new SearchNBByQueryCallable(query)); + } + + @Override + public int getRecordsCount(Query query) throws MetadataSourceException { + throw new MethodNotFoundException("This method is not implemented for WOS"); + } + + @Override + public Collection findMatchingRecords(Item item) throws MetadataSourceException { + throw new MethodNotFoundException("This method is not implemented for WOS"); + } + + @Override + public Collection findMatchingRecords(Query query) throws MetadataSourceException { + throw new MethodNotFoundException("This method is not implemented for WOS"); + } + + /** + * This class implements a callable to get the numbers of result + */ + private class SearchNBByQueryCallable implements Callable { + + private String query; + + private SearchNBByQueryCallable(String queryString) { + this.query = queryString; + } + + private SearchNBByQueryCallable(Query query) { + this.query = query.getParameterAsClass("query", String.class); + } + + @Override + public Integer call() throws Exception { + if (StringUtils.isNotBlank(apiKey)) { + String queryString = URLEncoder.encode(checkQuery(query), StandardCharsets.UTF_8); + String url = ENDPOINT_SEARCH_WOS + queryString + "&count=1&firstRecord=1"; + String response = liveImportClient.executeHttpGetRequest(timeout, url, getRequestParameters()); + SAXBuilder saxBuilder = new SAXBuilder(); + Document document = saxBuilder.build(new StringReader(response)); + Element root = document.getRootElement(); + XPathExpression xpath = XPathFactory.instance().compile("QueryResult/RecordsFound", + Filters.element(), null); + Element tot = xpath.evaluateFirst(root); + return Integer.valueOf(tot.getValue()); + } + return null; + } + } + + private class FindByIdCallable implements Callable> { + + private String doi; + + private FindByIdCallable(String doi) { + this.doi = URLEncoder.encode(doi, StandardCharsets.UTF_8); + } + + @Override + public List call() throws Exception { + List results = new ArrayList<>(); + if (StringUtils.isNotBlank(apiKey)) { + String url = ENDPOINT_SEARCH_BY_ID_WOS + this.doi + "?databaseId=WOS&lang=en&count=10&firstRecord=1"; + String response = liveImportClient.executeHttpGetRequest(timeout, url, getRequestParameters()); + List elements = splitToRecords(response); + for (Element record : elements) { + results.add(transformSourceRecords(record)); + } + } + return results; + } + } + + private class SearchByQueryCallable implements Callable> { + private Query query; + + + private SearchByQueryCallable(String queryString, Integer maxResult, Integer start) { + query = new Query(); + query.addParameter("query", queryString); + query.addParameter("start", start); + query.addParameter("count", maxResult); + } + + private SearchByQueryCallable(Query query) { + this.query = query; + } + + @Override + public List call() throws Exception { + List results = new ArrayList<>(); + String queryString = checkQuery(query.getParameterAsClass("query", String.class)); + Integer start = query.getParameterAsClass("start", Integer.class); + Integer count = query.getParameterAsClass("count", Integer.class); + if (StringUtils.isNotBlank(apiKey)) { + String url = ENDPOINT_SEARCH_WOS + URLEncoder.encode(queryString, StandardCharsets.UTF_8) + + "&count=" + count + "&firstRecord=" + (start + 1); + String response = liveImportClient.executeHttpGetRequest(timeout, url, getRequestParameters()); + List omElements = splitToRecords(response); + for (Element el : omElements) { + results.add(transformSourceRecords(el)); + } + } + return results; + } + + } + + private Map getRequestParameters() { + Map params = new HashMap(); + params.put("Accept", "application/xml"); + params.put("X-ApiKey", apiKey); + return params; + } + + private String checkQuery(String query) { + Pattern risPattern = Pattern.compile(AI_PATTERN); + Matcher risMatcher = risPattern.matcher(query.trim()); + if (risMatcher.matches()) { + return query; + } + if (DoiCheck.isDoi(query)) { + // FIXME: workaround to be removed once fixed by the community the double post of query param + if (query.startsWith(",")) { + query = query.substring(1); + } + return "DO=(" + query + ")"; + } else if (isIsi(query)) { + return "UT=(" + query + ")"; + } + StringBuilder queryBuilder = new StringBuilder("TS=("); + queryBuilder.append(query).append(")"); + return queryBuilder.toString(); + } + + private boolean isIsi(String query) { + if (query.startsWith("WOS:")) { + return true; + } + Matcher matcher = ISI_PATTERN.matcher(query.trim()); + return matcher.matches(); + } + + private List splitToRecords(String recordsSrc) { + try { + SAXBuilder saxBuilder = new SAXBuilder(); + Document document = saxBuilder.build(new StringReader(recordsSrc)); + Element root = document.getRootElement(); + XPathExpression xpath = XPathFactory.instance().compile("Data/Records/records/REC", + Filters.element(), null); + List records = xpath.evaluate(root); + if (Objects.nonNull(records)) { + return records; + } + } catch (JDOMException | IOException e) { + return new ArrayList(); + } + return new ArrayList(); + } + + public String getApiKey() { + return apiKey; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + +} \ No newline at end of file diff --git a/dspace-api/src/test/data/dspaceFolder/assetstore/scopus-ex.xml b/dspace-api/src/test/data/dspaceFolder/assetstore/scopus-ex.xml index 84fd7e71e212..cd50281f389e 100644 --- a/dspace-api/src/test/data/dspaceFolder/assetstore/scopus-ex.xml +++ b/dspace-api/src/test/data/dspaceFolder/assetstore/scopus-ex.xml @@ -26,7 +26,6 @@ 2023-01-01 2023 10.3934/mine.2023004 - In this paper, dedicated to Ireneo Peral, we study the regularizing effect of some lower order terms in Dirichlet problems despite the presence of Hardy potentials in the right hand side. 0 https://api.elsevier.com/content/affiliation/affiliation_id/60032350 @@ -109,7 +108,6 @@ 2023-01-01 2023 10.3934/mine.2023001 - We analyze the large deviations for a discrete energy Kac-like walk. In particular, we exhibit a path, with probability exponentially small in the number of particles, that looses energy. 0 https://api.elsevier.com/content/affiliation/affiliation_id/60032350 diff --git a/dspace-api/src/test/data/dspaceFolder/assetstore/wos-responce.xml b/dspace-api/src/test/data/dspaceFolder/assetstore/wos-responce.xml new file mode 100644 index 000000000000..dc2341b9f8ca --- /dev/null +++ b/dspace-api/src/test/data/dspaceFolder/assetstore/wos-responce.xml @@ -0,0 +1,469 @@ + + + + + + + WOS:000551093100034 + + + + + + + + 225-234 + + + EDULEARN19: 11TH INTERNATIONAL CONFERENCE ON + EDUCATION AND NEW LEARNING TECHNOLOGIES + EDULEARN Proceedings + EDULEARN PROC + EDULEARN PR + EDULEARN PROC + MENTORING IN EDUCATION. FEMALE ROLE MODELS IN + ITALIAN DESIGN ACADEMIC CULTURE + EDULEARN Proceedings + + + + Bollini, Letizia + Bollini, Letizia + Bollini, L + Letizia + Bollini + + + Chova, LG + Chova, LG + Chova, LG + LG + Chova + + + Martinez, AL + Martinez, AL + Martinez, AL + AL + Martinez + + + Torres, IC + Torres, IC + Torres, IC + IC + Torres + + + + Proceedings Paper + + + + + 11th International Conference on Education and New + Learning Technologies (EDULEARN), JUL 01-03, 2019, Palma, + SPAIN + + + 11th International Conference on Education and New + Learning Technologies (EDULEARN) + + + JUL 01-03, + 2019 + + + + Palma + SPAIN + + + + + + + + LAURI VOLPI 6, VALENICA, BURJASSOT 46100, SPAIN + VALENICA + + + + IATED-INT ASSOC TECHNOLOGY EDUCATION & + DEVELOPMENT + IATED-INT ASSOC TECHNOLOGY EDUCATION & + DEVELOPMENT + + + + + + + + English + + + English + + + Meeting + + + + + + Univ Milano Bicocca, Dept Psychol, Milan, Italy + + Univ Milano Bicocca + University of Milano-Bicocca + + + Dept Psychol + + Milan + Italy + + + + Bollini, Letizia + Bollini, Letizia + Bollini, L + Letizia + Bollini + + + + + + + + Univ Milano Bicocca, Dept Psychol, Milan, Italy + + Univ Milano Bicocca + University of Milano-Bicocca + + + Dept Psychol + + Milan + Italy + + + + Bollini, Letizia + Bollini, Letizia + Bollini, L + Letizia + Bollini + + + + + + + Social Sciences + + + Education & + Educational Research + Education & Educational Research + + + + Design education + female role models + mentoring in education + + + + +

It is widely recognized that mentors and tutors are + empowering roles in education systems. Students can benefit + to be raised in an inclusive environment in which senior + peers or professors play a positive and significant role + model. In particular, looking at the field of design studies, + a tradition of collaborative and collective culture shared + between students and teachers can be identified both in the + Bauhaus and in the Hochschule fur Gestaltung Ulm experiences. + In more recent time, studies show how junior designers + benefit from the presence, and the confront of significant + grown-up or more experienced figures able to listen to, to + coach and to mentor them. Nevertheless, when we look at the + issue from a gender perspective, we may notice a lack of + examples and reference figures, even if female students are a + significant percentage, when not the majority, in the + design's bachelors and masters degrees university + courses. Moreover, in Italy - at the 82 place on 144 in the + worldwide ranking for gender equality and women rights + according to the "Global Gender Gap Index 2017" - + the situation seems to be more difficult than in the other + European or developed countries, also due to the + representation of female roles in society and female bodies + in mass media. On the one hand, the paper offers a deeper + insight into the problem. The results of a census in the + field of digital product design - including human-centered, + interaction, user experience and interface design - are + mapped and discussed to understand the presence of female + professors and their role in BA/BS and MD courses. On the + other hand, the paper proposes some possible initiatives to + spread consciousness and participation and to improve some + changes in academic design culture.

+
+
+
+
+ + BP4FG + : 225-234 2019 + 5526 + + VISUAL DESIGN + EXPERIENCE + + 5526 + + Figures + Plates + Color plates + + + P + IATED-INT ASSOC TECHNOLOGY EDUCATION & + DEVELOPMENT, LAURI VOLPI 6, VALENICA, BURJASSOT 46100, SPAIN + N + + +
+ + + + + + + + + + + + + +
+ + WOS:000551093100033 + + + + + + + + 224-224 + + + EDULEARN19: 11TH INTERNATIONAL CONFERENCE ON + EDUCATION AND NEW LEARNING TECHNOLOGIES + EDULEARN Proceedings + EDULEARN PROC + EDULEARN PR + EDULEARN PROC + DYSLEXIA AND TYPOGRAPHY: HOW TO IMPROVE + EDUCATIONAL PUBLISHING FOR INCLUSIVE DESIGN - A CRITICAL REVIEW + EDULEARN Proceedings + + + + Bollini, L. + Bollini, L. + Bollini, L + L. + Bollini + + + Chova, LG + Chova, LG + Chova, LG + LG + Chova + + + Martinez, AL + Martinez, AL + Martinez, AL + AL + Martinez + + + Torres, IC + Torres, IC + Torres, IC + IC + Torres + + + + Proceedings Paper + + + + + 11th International Conference on Education and New + Learning Technologies (EDULEARN), JUL 01-03, 2019, Palma, + SPAIN + + + 11th International Conference on Education and New + Learning Technologies (EDULEARN) + + + JUL 01-03, + 2019 + + + + Palma + SPAIN + + + + + + + + LAURI VOLPI 6, VALENICA, BURJASSOT 46100, SPAIN + VALENICA + + + + IATED-INT ASSOC TECHNOLOGY EDUCATION & + DEVELOPMENT + IATED-INT ASSOC TECHNOLOGY EDUCATION & + DEVELOPMENT + + + + + + + + English + + + English + + + Meeting + + + + + + Univ Milano Bicocca, Milan, Italy + + Univ Milano Bicocca + University of Milano-Bicocca + + Milan + Italy + + + + Bollini, L. + Bollini, L. + Bollini, L + L. + Bollini + + + + + + + + Univ Milano Bicocca, Milan, Italy + + Univ Milano Bicocca + University of Milano-Bicocca + + Milan + Italy + + + + Bollini, L. + Bollini, L. + Bollini, L + L. + Bollini + + + + + + + Social Sciences + + + Education & + Educational Research + Education & Educational Research + + + + Dyslexia and Typography + Font design + educational publishing + Design for all + Universal design + + + + BP4FG + : 224-224 2019 + 5526 + 5526 + + Figures + Plates + Color plates + + + P + IATED-INT ASSOC TECHNOLOGY EDUCATION & + DEVELOPMENT, LAURI VOLPI 6, VALENICA, BURJASSOT 46100, SPAIN + N + + + + + + + + + + + + + + + + + +
+
+
+ + 1 + 64666906 + 2 + + undefined +
\ No newline at end of file diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AbstractLiveImportIntegrationTest.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AbstractLiveImportIntegrationTest.java new file mode 100644 index 000000000000..ca3195b344fb --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AbstractLiveImportIntegrationTest.java @@ -0,0 +1,80 @@ +/** + * 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.rest; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.UnsupportedEncodingException; +import java.util.Collection; + +import org.apache.http.ProtocolVersion; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.entity.BasicHttpEntity; +import org.apache.tools.ant.filters.StringInputStream; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.importer.external.datamodel.ImportRecord; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; + +/** + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com) + */ +public class AbstractLiveImportIntegrationTest extends AbstractControllerIntegrationTest { + + protected boolean matchRecords(Collection recordsImported, Collection records2match) { + ImportRecord firstImported = recordsImported.iterator().next(); + ImportRecord secondImported = recordsImported.iterator().next(); + ImportRecord first2match = recordsImported.iterator().next(); + ImportRecord second2match = recordsImported.iterator().next(); + boolean checkFirstRecord = firstImported.getValueList().containsAll(first2match.getValueList()); + boolean checkSecondRecord = secondImported.getValueList().containsAll(second2match.getValueList()); + return checkFirstRecord && checkSecondRecord; + } + + protected MetadatumDTO createMetadatumDTO(String schema, String element, String qualifier, String value) { + MetadatumDTO metadatumDTO = new MetadatumDTO(); + metadatumDTO.setSchema(schema); + metadatumDTO.setElement(element); + metadatumDTO.setQualifier(qualifier); + metadatumDTO.setValue(value); + return metadatumDTO; + } + + protected CloseableHttpResponse mockResponse(String xmlExample, int statusCode, String reason) + throws UnsupportedEncodingException { + BasicHttpEntity basicHttpEntity = new BasicHttpEntity(); + basicHttpEntity.setChunked(true); + basicHttpEntity.setContent(new StringInputStream(xmlExample)); + + CloseableHttpResponse response = mock(CloseableHttpResponse.class); + when(response.getStatusLine()).thenReturn(statusLine(statusCode, reason)); + when(response.getEntity()).thenReturn(basicHttpEntity); + return response; + } + + protected StatusLine statusLine(int statusCode, String reason) { + return new StatusLine() { + @Override + public ProtocolVersion getProtocolVersion() { + return new ProtocolVersion("http", 1, 1); + } + + @Override + public int getStatusCode() { + return statusCode; + } + + @Override + public String getReasonPhrase() { + return reason; + } + }; + } + +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/PubmedEuropeMetadataSourceServiceIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/PubmedEuropeMetadataSourceServiceIT.java index 78a177737023..d6b3cc8143da 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/PubmedEuropeMetadataSourceServiceIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/PubmedEuropeMetadataSourceServiceIT.java @@ -9,11 +9,9 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.io.FileInputStream; -import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; @@ -21,13 +19,8 @@ import java.util.List; import org.apache.commons.io.IOUtils; -import org.apache.http.ProtocolVersion; -import org.apache.http.StatusLine; import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.entity.BasicHttpEntity; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.tools.ant.filters.StringInputStream; -import org.dspace.app.rest.test.AbstractControllerIntegrationTest; import org.dspace.importer.external.datamodel.ImportRecord; import org.dspace.importer.external.metadatamapping.MetadatumDTO; import org.dspace.importer.external.pubmedeurope.PubmedEuropeMetadataSourceServiceImpl; @@ -42,7 +35,7 @@ * * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com) */ -public class PubmedEuropeMetadataSourceServiceIT extends AbstractControllerIntegrationTest { +public class PubmedEuropeMetadataSourceServiceIT extends AbstractLiveImportIntegrationTest { @Autowired private PubmedEuropeMetadataSourceServiceImpl pubmedEuropeMetadataServiceImpl; @@ -60,13 +53,13 @@ public void pubmedEuropeImportMetadataGetRecordsTest() throws Exception { String path2file = testProps.get("test.pubmedeurope-empty").toString(); try (FileInputStream file = new FileInputStream(path)) { try (FileInputStream file2 = new FileInputStream(path2file)) { - String xmlMetricsExample = IOUtils.toString(file, Charset.defaultCharset()); - String xmlMetricsExample2 = IOUtils.toString(file2, Charset.defaultCharset()); + String pubmedEuropeXmlResp = IOUtils.toString(file, Charset.defaultCharset()); + String pubmedEuropeXmlResp2 = IOUtils.toString(file2, Charset.defaultCharset()); liveImportClientImpl.setHttpClient(httpClient); - CloseableHttpResponse response = mockResponse(xmlMetricsExample, 200, "OK"); - CloseableHttpResponse response2 = mockResponse(xmlMetricsExample2, 200, "OK"); + CloseableHttpResponse response = mockResponse(pubmedEuropeXmlResp, 200, "OK"); + CloseableHttpResponse response2 = mockResponse(pubmedEuropeXmlResp2, 200, "OK"); when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response, response2); @@ -81,26 +74,26 @@ public void pubmedEuropeImportMetadataGetRecordsTest() throws Exception { } } - private boolean matchRecords(Collection recordsImported, Collection records2match) { - ImportRecord firstImported = recordsImported.iterator().next(); - ImportRecord secondImported = recordsImported.iterator().next(); - ImportRecord first2match = recordsImported.iterator().next(); - ImportRecord second2match = recordsImported.iterator().next(); - boolean checkFirstRecord = firstImported.getValueList().containsAll(first2match.getValueList()); - boolean checkSecondRecord = secondImported.getValueList().containsAll(second2match.getValueList()); - return checkFirstRecord && checkSecondRecord; - } + @Test + public void pubmedEuropeImportMetadataGetRecordsCountTest() throws Exception { + context.turnOffAuthorisationSystem(); - private CloseableHttpResponse mockResponse(String xmlExample, int statusCode, String reason) - throws UnsupportedEncodingException { - BasicHttpEntity basicHttpEntity = new BasicHttpEntity(); - basicHttpEntity.setChunked(true); - basicHttpEntity.setContent(new StringInputStream(xmlExample)); + CloseableHttpClient originalHttpClient = liveImportClientImpl.getHttpClient(); + CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + String path = testProps.get("test.pubmedeurope").toString(); + try (FileInputStream file = new FileInputStream(path)) { + String pubmedEuropeXmlResp = IOUtils.toString(file, Charset.defaultCharset()); + + liveImportClientImpl.setHttpClient(httpClient); + CloseableHttpResponse response = mockResponse(pubmedEuropeXmlResp, 200, "OK"); + when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response); - CloseableHttpResponse response = mock(CloseableHttpResponse.class); - when(response.getStatusLine()).thenReturn(statusLine(statusCode, reason)); - when(response.getEntity()).thenReturn(basicHttpEntity); - return response; + context.restoreAuthSystemState(); + int tot = pubmedEuropeMetadataServiceImpl.getRecordsCount("test query"); + assertEquals(3, tot); + } finally { + liveImportClientImpl.setHttpClient(originalHttpClient); + } } private Collection getRecords() { @@ -217,32 +210,4 @@ private Collection getRecords() { return records; } - private MetadatumDTO createMetadatumDTO(String schema, String element, String qualifier, String value) { - MetadatumDTO metadatumDTO = new MetadatumDTO(); - metadatumDTO.setSchema(schema); - metadatumDTO.setElement(element); - metadatumDTO.setQualifier(qualifier); - metadatumDTO.setValue(value); - return metadatumDTO; - } - - private StatusLine statusLine(int statusCode, String reason) { - return new StatusLine() { - @Override - public ProtocolVersion getProtocolVersion() { - return new ProtocolVersion("http", 1, 1); - } - - @Override - public int getStatusCode() { - return statusCode; - } - - @Override - public String getReasonPhrase() { - return reason; - } - }; - } - } \ No newline at end of file diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScopusImportMetadataSourceServiceIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScopusImportMetadataSourceServiceIT.java index a3ec481a21f3..bf01c71f839f 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScopusImportMetadataSourceServiceIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScopusImportMetadataSourceServiceIT.java @@ -9,11 +9,9 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.io.FileInputStream; -import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; @@ -22,13 +20,8 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; -import org.apache.http.ProtocolVersion; -import org.apache.http.StatusLine; import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.entity.BasicHttpEntity; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.tools.ant.filters.StringInputStream; -import org.dspace.app.rest.test.AbstractControllerIntegrationTest; import org.dspace.importer.external.datamodel.ImportRecord; import org.dspace.importer.external.metadatamapping.MetadatumDTO; import org.dspace.importer.external.scopus.service.LiveImportClientImpl; @@ -43,7 +36,7 @@ * * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com) */ -public class ScopusImportMetadataSourceServiceIT extends AbstractControllerIntegrationTest { +public class ScopusImportMetadataSourceServiceIT extends AbstractLiveImportIntegrationTest { @Autowired private ScopusImportMetadataSourceServiceImpl scopusServiceImpl; @@ -62,12 +55,10 @@ public void scopusImportMetadataGetRecordsTest() throws Exception { CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); String path = testProps.get("test.scopus").toString(); try (FileInputStream file = new FileInputStream(path)) { - String xmlMetricsExample = IOUtils.toString(file, Charset.defaultCharset()); + String scopusXmlResp = IOUtils.toString(file, Charset.defaultCharset()); liveImportClientImpl.setHttpClient(httpClient); - - CloseableHttpResponse response = mockResponse(xmlMetricsExample, 200, "OK"); - + CloseableHttpResponse response = mockResponse(scopusXmlResp, 200, "OK"); when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response); context.restoreAuthSystemState(); @@ -92,12 +83,10 @@ public void scopusImportMetadataGetRecordsCountTest() throws Exception { CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); String path = testProps.get("test.scopus").toString(); try (FileInputStream file = new FileInputStream(path)) { - String xmlMetricsExample = IOUtils.toString(file, Charset.defaultCharset()); + String scopusXmlResp = IOUtils.toString(file, Charset.defaultCharset()); liveImportClientImpl.setHttpClient(httpClient); - - CloseableHttpResponse response = mockResponse(xmlMetricsExample, 200, "OK"); - + CloseableHttpResponse response = mockResponse(scopusXmlResp, 200, "OK"); when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response); context.restoreAuthSystemState(); @@ -120,12 +109,10 @@ public void scopusImportMetadataGetRecordsEmptyResponceTest() throws Exception { CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); String path = testProps.get("test.scopus-empty").toString(); try (FileInputStream file = new FileInputStream(path)) { - String xmlMetricsExample = IOUtils.toString(file, Charset.defaultCharset()); + String scopusXmlResp = IOUtils.toString(file, Charset.defaultCharset()); liveImportClientImpl.setHttpClient(httpClient); - - CloseableHttpResponse response = mockResponse(xmlMetricsExample, 200, "OK"); - + CloseableHttpResponse response = mockResponse(scopusXmlResp, 200, "OK"); when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response); context.restoreAuthSystemState(); @@ -138,37 +125,40 @@ public void scopusImportMetadataGetRecordsEmptyResponceTest() throws Exception { } } - private boolean matchRecords(Collection recordsImported, Collection records2match) { - ImportRecord firstImported = recordsImported.iterator().next(); - ImportRecord secondImported = recordsImported.iterator().next(); - ImportRecord first2match = recordsImported.iterator().next(); - ImportRecord second2match = recordsImported.iterator().next(); - boolean checkFirstRecord = firstImported.getValueList().containsAll(first2match.getValueList()); - boolean checkSecondRecord = secondImported.getValueList().containsAll(second2match.getValueList()); - return checkFirstRecord && checkSecondRecord; - } - private Collection getRecords() { Collection records = new LinkedList(); - List metadatums = new ArrayList(); //define first record + List metadatums = new ArrayList(); + MetadatumDTO doi = createMetadatumDTO("dc", "identifier", "doi", "10.3934/mine.2023004"); MetadatumDTO title = createMetadatumDTO("dc","title", null, "Hardy potential versus lower order terms in Dirichlet problems: regularizing effects"); - MetadatumDTO doi = createMetadatumDTO("dc", "identifier", "doi", "10.3934/mine.2023004"); - MetadatumDTO date = createMetadatumDTO("dc", "date", "issued", "2023-01-01"); MetadatumDTO type = createMetadatumDTO("dc", "type", null, "Journal"); + MetadatumDTO date = createMetadatumDTO("dc", "date", "issued", "2023-01-01"); MetadatumDTO citationVolume = createMetadatumDTO("oaire", "citation", "volume", "5"); MetadatumDTO citationIssue = createMetadatumDTO("oaire", "citation", "issue", "1"); MetadatumDTO scopusId = createMetadatumDTO("dc", "identifier", "scopus", "2-s2.0-85124241875"); MetadatumDTO funding = createMetadatumDTO("dc", "relation", "funding", "Junta de Andalucía"); MetadatumDTO grantno = createMetadatumDTO("dc", "relation", "grantno", "PGC2018-096422-B-I00"); MetadatumDTO subject = createMetadatumDTO("dc", "subject", null, - "Hardy potentials | Laplace equation | Summability of solutions"); + "Hardy potentials | Laplace equation | Summability of solutions"); + MetadatumDTO author = createMetadatumDTO("dc", "contributor", "author", "Arcoya D."); + MetadatumDTO scopusAuthorId = createMetadatumDTO("person", "identifier", "scopus-author-id", "6602330574"); + MetadatumDTO orcid = createMetadatumDTO("person", "identifier", "orcid", "#PLACEHOLDER_PARENT_METADATA_VALUE#"); + MetadatumDTO orgunit = createMetadatumDTO("oairecerif", "affiliation", "orgunit", "Universidad de Granada"); + MetadatumDTO author2 = createMetadatumDTO("dc", "contributor", "author", "Boccardo L."); + MetadatumDTO scopusAuthorId2 = createMetadatumDTO("person", "identifier", "scopus-author-id", "7003612261"); + MetadatumDTO orcid2 = createMetadatumDTO("person", "identifier", "orcid","#PLACEHOLDER_PARENT_METADATA_VALUE#"); + MetadatumDTO orgunit2 = createMetadatumDTO("oairecerif", "affiliation","orgunit","Sapienza Università di Roma"); + MetadatumDTO author3 = createMetadatumDTO("dc", "contributor", "author", "Orsina L."); + MetadatumDTO scopusAuthorId3 = createMetadatumDTO("person", "identifier", "scopus-author-id", "6602595438"); + MetadatumDTO orcid3 = createMetadatumDTO("person", "identifier", "orcid","#PLACEHOLDER_PARENT_METADATA_VALUE#"); + MetadatumDTO orgunit3 = createMetadatumDTO("oairecerif", "affiliation","orgunit","Sapienza Università di Roma"); MetadatumDTO rights = createMetadatumDTO("dc", "rights", null, "open access"); - MetadatumDTO ispartof = createMetadatumDTO("dc", "relation", "ispartof", - "Mathematics In Engineering"); - metadatums.add(title); + MetadatumDTO ispartof = createMetadatumDTO("dc", "relation", "ispartof", "Mathematics In Engineering"); + MetadatumDTO ispartofseries = createMetadatumDTO("dc","relation","ispartofseries","Mathematics In Engineering"); + metadatums.add(doi); + metadatums.add(title); metadatums.add(date); metadatums.add(type); metadatums.add(citationVolume); @@ -177,13 +167,28 @@ private Collection getRecords() { metadatums.add(funding); metadatums.add(grantno); metadatums.add(subject); + metadatums.add(author); + metadatums.add(scopusAuthorId); + metadatums.add(orcid); + metadatums.add(orgunit); + metadatums.add(author2); + metadatums.add(scopusAuthorId2); + metadatums.add(orcid2); + metadatums.add(orgunit2); + metadatums.add(author3); + metadatums.add(scopusAuthorId3); + metadatums.add(orcid3); + metadatums.add(orgunit3); metadatums.add(rights); metadatums.add(ispartof); + metadatums.add(ispartofseries); ImportRecord firstrRecord = new ImportRecord(metadatums); + //define second record + List metadatums2 = new ArrayList(); + MetadatumDTO doi2 = createMetadatumDTO("dc", "identifier", "doi", "10.3934/mine.2023001"); MetadatumDTO title2 = createMetadatumDTO("dc","title", null, "Large deviations for a binary collision model: energy evaporation"); - MetadatumDTO doi2 = createMetadatumDTO("dc", "identifier", "doi", "10.3934/mine.2023001"); MetadatumDTO date2 = createMetadatumDTO("dc", "date", "issued", "2023-01-01"); MetadatumDTO type2 = createMetadatumDTO("dc", "type", null, "Journal"); MetadatumDTO citationVolume2 = createMetadatumDTO("oaire", "citation", "volume", "5"); @@ -192,64 +197,59 @@ private Collection getRecords() { MetadatumDTO grantno2 = createMetadatumDTO("dc", "relation", "grantno", "undefined"); MetadatumDTO subject2 = createMetadatumDTO("dc", "subject", null, "Boltzmann equation | Discrete energy model | Kac model | Large deviations | Violation of energy conservation"); + + MetadatumDTO author4 = createMetadatumDTO("dc", "contributor", "author", "Basile G."); + MetadatumDTO scopusAuthorId4 = createMetadatumDTO("person", "identifier", "scopus-author-id", "55613229065"); + MetadatumDTO orcid4 = createMetadatumDTO("person", "identifier", "orcid","#PLACEHOLDER_PARENT_METADATA_VALUE#"); + MetadatumDTO orgunit4 = createMetadatumDTO("oairecerif", "affiliation","orgunit","Sapienza Università di Roma"); + MetadatumDTO author5 = createMetadatumDTO("dc", "contributor", "author", "Benedetto D."); + MetadatumDTO scopusAuthorId5 = createMetadatumDTO("person", "identifier", "scopus-author-id", "55893665100"); + MetadatumDTO orcid5 = createMetadatumDTO("person", "identifier", "orcid","#PLACEHOLDER_PARENT_METADATA_VALUE#"); + MetadatumDTO orgunit5 = createMetadatumDTO("oairecerif", "affiliation","orgunit","Sapienza Università di Roma"); + MetadatumDTO author6 = createMetadatumDTO("dc", "contributor", "author", "Caglioti E."); + MetadatumDTO scopusAuthorId6 = createMetadatumDTO("person", "identifier", "scopus-author-id", "7004588675"); + MetadatumDTO orcid6 = createMetadatumDTO("person", "identifier", "orcid","#PLACEHOLDER_PARENT_METADATA_VALUE#"); + MetadatumDTO orgunit6 = createMetadatumDTO("oairecerif", "affiliation","orgunit","Sapienza Università di Roma"); + MetadatumDTO author7 = createMetadatumDTO("dc", "contributor", "author", "Bertini L."); + MetadatumDTO scopusAuthorId7 = createMetadatumDTO("person", "identifier", "scopus-author-id", "7005555198"); + MetadatumDTO orcid7 = createMetadatumDTO("person", "identifier", "orcid","#PLACEHOLDER_PARENT_METADATA_VALUE#"); + MetadatumDTO orgunit7 = createMetadatumDTO("oairecerif", "affiliation","orgunit","Sapienza Università di Roma"); MetadatumDTO rights2 = createMetadatumDTO("dc", "rights", null, "open access"); - MetadatumDTO ispartof2 = createMetadatumDTO("dc", "relation", "ispartof", - "Mathematics In Engineering"); - metadatums.add(title2); - metadatums.add(doi2); - metadatums.add(date2); - metadatums.add(type2); - metadatums.add(citationVolume2); - metadatums.add(citationIssue2); - metadatums.add(scopusId2); - metadatums.add(grantno2); - metadatums.add(subject2); - metadatums.add(rights2); - metadatums.add(ispartof2); - ImportRecord secondRecord = new ImportRecord(metadatums); + MetadatumDTO ispartof2 = createMetadatumDTO("dc", "relation", "ispartof", "Mathematics In Engineering"); + MetadatumDTO ispartofseries2 = createMetadatumDTO("dc", "relation", "ispartofseries", + "Mathematics In Engineering"); + metadatums2.add(title2); + metadatums2.add(doi2); + metadatums2.add(date2); + metadatums2.add(type2); + metadatums2.add(citationVolume2); + metadatums2.add(citationIssue2); + metadatums2.add(scopusId2); + metadatums2.add(grantno2); + metadatums2.add(subject2); + metadatums2.add(author4); + metadatums2.add(scopusAuthorId4); + metadatums2.add(orcid4); + metadatums2.add(orgunit4); + metadatums2.add(author5); + metadatums2.add(scopusAuthorId5); + metadatums2.add(orcid5); + metadatums2.add(orgunit5); + metadatums2.add(author6); + metadatums2.add(scopusAuthorId6); + metadatums2.add(orcid6); + metadatums2.add(orgunit6); + metadatums2.add(author7); + metadatums2.add(scopusAuthorId7); + metadatums2.add(orcid7); + metadatums2.add(orgunit7); + metadatums2.add(rights2); + metadatums2.add(ispartof2); + metadatums2.add(ispartofseries2); + ImportRecord secondRecord = new ImportRecord(metadatums2); records.add(firstrRecord); records.add(secondRecord); return records; } - private MetadatumDTO createMetadatumDTO(String schema, String element, String qualifier, String value) { - MetadatumDTO metadatumDTO = new MetadatumDTO(); - metadatumDTO.setSchema(schema); - metadatumDTO.setElement(element); - metadatumDTO.setQualifier(qualifier); - metadatumDTO.setValue(value); - return metadatumDTO; - } - - private CloseableHttpResponse mockResponse(String xmlExample, int statusCode, String reason) - throws UnsupportedEncodingException { - BasicHttpEntity basicHttpEntity = new BasicHttpEntity(); - basicHttpEntity.setChunked(true); - basicHttpEntity.setContent(new StringInputStream(xmlExample)); - - CloseableHttpResponse response = mock(CloseableHttpResponse.class); - when(response.getStatusLine()).thenReturn(statusLine(statusCode, reason)); - when(response.getEntity()).thenReturn(basicHttpEntity); - return response; - } - - private StatusLine statusLine(int statusCode, String reason) { - return new StatusLine() { - @Override - public ProtocolVersion getProtocolVersion() { - return new ProtocolVersion("http", 1, 1); - } - - @Override - public int getStatusCode() { - return statusCode; - } - - @Override - public String getReasonPhrase() { - return reason; - } - }; - } - } \ No newline at end of file diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WOSImportMetadataSourceServiceIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WOSImportMetadataSourceServiceIT.java new file mode 100644 index 000000000000..38c605e06880 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WOSImportMetadataSourceServiceIT.java @@ -0,0 +1,171 @@ +/** + * 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.rest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +import java.io.FileInputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.impl.client.CloseableHttpClient; +import org.dspace.importer.external.datamodel.ImportRecord; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; +import org.dspace.importer.external.scopus.service.LiveImportClientImpl; +import org.dspace.importer.external.wos.service.WOSImportMetadataSourceServiceImpl; +import org.junit.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Integration tests for {@link WOSImportMetadataSourceServiceImpl} + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com) + */ +public class WOSImportMetadataSourceServiceIT extends AbstractLiveImportIntegrationTest { + + @Autowired + private WOSImportMetadataSourceServiceImpl wosImportMetadataService; + + @Autowired + private LiveImportClientImpl liveImportClientImpl; + + @Test + public void wosImportMetadataGetRecordsTest() throws Exception { + context.turnOffAuthorisationSystem(); + String originApiKey = wosImportMetadataService.getApiKey(); + if (StringUtils.isBlank(originApiKey)) { + wosImportMetadataService.setApiKey("testApiKey"); + } + CloseableHttpClient originalHttpClient = liveImportClientImpl.getHttpClient(); + CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + String path2file = testProps.get("test.wos").toString(); + try (FileInputStream file = new FileInputStream(path2file)) { + String wosXmlResp = IOUtils.toString(file, Charset.defaultCharset()); + + liveImportClientImpl.setHttpClient(httpClient); + + CloseableHttpResponse response = mockResponse(wosXmlResp, 200, "OK"); + + when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response); + + context.restoreAuthSystemState(); + Collection recordsImported = wosImportMetadataService.getRecords("test query", 0, 2); + Collection collection2match = getRecords(); + assertEquals(2, recordsImported.size()); + assertTrue(matchRecords(recordsImported, collection2match)); + } finally { + liveImportClientImpl.setHttpClient(originalHttpClient); + wosImportMetadataService.setApiKey(originApiKey); + } + } + + @Test + public void wosImportMetadataGetRecordsCountTest() throws Exception { + context.turnOffAuthorisationSystem(); + String originApiKey = wosImportMetadataService.getApiKey(); + if (StringUtils.isBlank(originApiKey)) { + wosImportMetadataService.setApiKey("testApiKey"); + } + CloseableHttpClient originalHttpClient = liveImportClientImpl.getHttpClient(); + CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + String path = testProps.get("test.wos").toString(); + try (FileInputStream file = new FileInputStream(path)) { + String wosXmlResp = IOUtils.toString(file, Charset.defaultCharset()); + + liveImportClientImpl.setHttpClient(httpClient); + CloseableHttpResponse response = mockResponse(wosXmlResp, 200, "OK"); + + when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response); + + context.restoreAuthSystemState(); + int tot = wosImportMetadataService.getRecordsCount("test query"); + assertEquals(2, tot); + } finally { + liveImportClientImpl.setHttpClient(originalHttpClient); + wosImportMetadataService.setApiKey(originApiKey); + } + } + + private Collection getRecords() { + Collection records = new LinkedList(); + List metadatums = new ArrayList(); + //define first record + MetadatumDTO edition = createMetadatumDTO("oaire","citation", "edition", "WOS.ISSHP"); + MetadatumDTO date = createMetadatumDTO("dc", "date", "issued", "2019"); + MetadatumDTO startPage = createMetadatumDTO("oaire","citation", "startPage", "225"); + MetadatumDTO endPage = createMetadatumDTO("oaire","citation", "endPage", "234"); + MetadatumDTO type = createMetadatumDTO("dc", "type", null, "Book in series"); + MetadatumDTO issn = createMetadatumDTO("dc", "relation", "issn", "2340-1117"); + MetadatumDTO isbn = createMetadatumDTO("dc", "identifier", "isbn", "978-84-09-12031-4"); + MetadatumDTO iso = createMetadatumDTO("dc", "language", "iso", "1"); + MetadatumDTO author = createMetadatumDTO("dc", "contributor", "author", "Bollini, Letizia"); + MetadatumDTO author2 = createMetadatumDTO("dc", "contributor", "author", "Chova, LG"); + MetadatumDTO author3 = createMetadatumDTO("dc", "contributor", "author", "Martinez, AL"); + MetadatumDTO author4 = createMetadatumDTO("dc", "contributor", "author", "Torres, IC"); + MetadatumDTO isi = createMetadatumDTO("dc", "identifier", "isi", "WOS:000551093100034"); + metadatums.add(edition); + metadatums.add(date); + metadatums.add(startPage); + metadatums.add(endPage); + metadatums.add(type); + metadatums.add(issn); + metadatums.add(isbn); + metadatums.add(iso); + metadatums.add(author); + metadatums.add(author2); + metadatums.add(author3); + metadatums.add(author4); + metadatums.add(isi); + ImportRecord firstrRecord = new ImportRecord(metadatums); + + //define second record + List metadatums2 = new ArrayList(); + MetadatumDTO edition2 = createMetadatumDTO("oaire","citation", "edition", "WOS.ISSHP"); + MetadatumDTO date2 = createMetadatumDTO("dc", "date", "issued", "2019"); + MetadatumDTO startPage2 = createMetadatumDTO("oaire","citation", "startPage", "224"); + MetadatumDTO endPage2 = createMetadatumDTO("oaire","citation", "endPage", "224"); + MetadatumDTO type2 = createMetadatumDTO("dc", "type", null, "Book in series"); + MetadatumDTO issn2 = createMetadatumDTO("dc", "relation", "issn", "2340-1117"); + MetadatumDTO isbn2 = createMetadatumDTO("dc", "identifier", "isbn", "978-84-09-12031-4"); + MetadatumDTO iso2 = createMetadatumDTO("dc", "language", "iso", "1"); + MetadatumDTO author5 = createMetadatumDTO("dc", "contributor", "author", "Bollini, Letizia"); + MetadatumDTO author6 = createMetadatumDTO("dc", "contributor", "author", "Chova, LG"); + MetadatumDTO author7 = createMetadatumDTO("dc", "contributor", "author", "Martinez, AL"); + MetadatumDTO author8 = createMetadatumDTO("dc", "contributor", "author", "Torres, IC"); + MetadatumDTO isi2 = createMetadatumDTO("dc", "identifier", "isi", "WOS:000551093100033"); + metadatums2.add(edition2); + metadatums2.add(date2); + metadatums2.add(startPage2); + metadatums2.add(endPage2); + metadatums2.add(type2); + metadatums2.add(issn2); + metadatums2.add(isbn2); + metadatums2.add(iso2); + metadatums2.add(author5); + metadatums2.add(author6); + metadatums2.add(author7); + metadatums2.add(author8); + metadatums2.add(isi2); + ImportRecord secondRecord = new ImportRecord(metadatums2); + + records.add(firstrRecord); + records.add(secondRecord); + return records; + } + +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/resources/test-config.properties b/dspace-server-webapp/src/test/resources/test-config.properties index ac04d032d6f9..8ff06d65f5b0 100644 --- a/dspace-server-webapp/src/test/resources/test-config.properties +++ b/dspace-server-webapp/src/test/resources/test-config.properties @@ -17,4 +17,5 @@ test.curateTaskFile = ./target/testing/dspace/assetstore/curate.txt test.scopus = ./target/testing/dspace/assetstore/scopus-ex.xml test.scopus-empty = ./target/testing/dspace/assetstore/scopus-empty-resp.xml test.pubmedeurope = ./target/testing/dspace/assetstore/pubmedeurope-test.xml -test.pubmedeurope-empty = ./target/testing/dspace/assetstore/pubmedeurope-empty.xml \ No newline at end of file +test.pubmedeurope-empty = ./target/testing/dspace/assetstore/pubmedeurope-empty.xml +test.wos = ./target/testing/dspace/assetstore/wos-responce.xml \ No newline at end of file diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index 8b985533c284..78a2f7225e1e 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -1598,7 +1598,11 @@ scopus.instToken = scopus.search-api.viewMode = COMPLETE #################################################################### +#------------------- WOS SERVICES ---------------------------------# +#------------------------------------------------------------------# +wos.apiKey = +#################################################################### # Load default module configs # ---------------------------- # To exclude a module configuration, simply comment out its "include" statement. diff --git a/dspace/config/spring/api/wos-integration.xml b/dspace/config/spring/api/wos-integration.xml new file mode 100644 index 000000000000..b5862112688c --- /dev/null +++ b/dspace/config/spring/api/wos-integration.xml @@ -0,0 +1,291 @@ + + + + + + + Defines which metadatum is mapped on which metadatum. Note that while the key must be unique it + only matters here for postprocessing of the value. The mapped MetadatumContributor has full control over + what metadatafield is generated. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + static_data/item/keywords_plus/keyword + static_data/fullrecord_metadata/keywords/keyword + static_data/fullrecord_metadata/category_info/headings/heading + static_data/fullrecord_metadata/category_info/subheadings/subheading + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From a829cb9fece5637d1103a8ffb22ac8b7e2033b22 Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Fri, 1 Apr 2022 00:35:46 +0200 Subject: [PATCH 0111/1846] [CST-5303] fix failed test --- .../org/dspace/app/rest/ExternalSourcesRestControllerIT.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java index 9205c3b88ea4..8b39e2004eca 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java @@ -41,6 +41,7 @@ public void findAllExternalSources() throws Exception { .andExpect(jsonPath("$._embedded.externalsources", Matchers.hasItems( ExternalSourceMatcher.matchExternalSource("mock", "mock", false), ExternalSourceMatcher.matchExternalSource("orcid", "orcid", false), + ExternalSourceMatcher.matchExternalSource("scopus", "scopus", false), ExternalSourceMatcher.matchExternalSource( "sherpaJournalIssn", "sherpaJournalIssn", false), ExternalSourceMatcher.matchExternalSource( @@ -52,7 +53,7 @@ public void findAllExternalSources() throws Exception { ExternalSourceMatcher.matchExternalSource( "openAIREFunding", "openAIREFunding", false) ))) - .andExpect(jsonPath("$.page.totalElements", Matchers.is(7))); + .andExpect(jsonPath("$.page.totalElements", Matchers.is(8))); } @Test From 6cecbbcdb9eb5ae2cc6f495d00956de2808cd676 Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Fri, 1 Apr 2022 13:01:36 +0200 Subject: [PATCH 0112/1846] [CST-5303] porting of cinii live import service --- .../external/cinii/CiniiFieldMapping.java | 37 ++ .../CiniiImportMetadataSourceServiceImpl.java | 391 ++++++++++++++++++ .../spring-dspace-addon-import-services.xml | 5 + .../config/spring/api/cinii-integration.xml | 110 +++++ .../config/spring/api/external-services.xml | 11 + 5 files changed, 554 insertions(+) create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/cinii/CiniiFieldMapping.java create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/cinii/CiniiImportMetadataSourceServiceImpl.java create mode 100644 dspace/config/spring/api/cinii-integration.xml diff --git a/dspace-api/src/main/java/org/dspace/importer/external/cinii/CiniiFieldMapping.java b/dspace-api/src/main/java/org/dspace/importer/external/cinii/CiniiFieldMapping.java new file mode 100644 index 000000000000..ad010d6c754b --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/cinii/CiniiFieldMapping.java @@ -0,0 +1,37 @@ +/** + * 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.importer.external.cinii; + +import java.util.Map; +import javax.annotation.Resource; + +import org.dspace.importer.external.metadatamapping.AbstractMetadataFieldMapping; + +/** + * An implementation of {@link AbstractMetadataFieldMapping} + * Responsible for defining the mapping of the ArXiv metadatum fields on the DSpace metadatum fields + * + * @author Pasquale Cavallo (pasquale.cavallo at 4science dot it) + */ +public class CiniiFieldMapping extends AbstractMetadataFieldMapping { + + /** + * Defines which metadatum is mapped on which metadatum. Note that while the key must be unique it + * only matters here for postprocessing of the value. The mapped MetadatumContributor has full control over + * what metadatafield is generated. + * + * @param metadataFieldMap The map containing the link between retrieve metadata and metadata that will be set to + * the item. + */ + @Override + @Resource(name = "ciniiMetadataFieldMap") + public void setMetadataFieldMap(Map metadataFieldMap) { + super.setMetadataFieldMap(metadataFieldMap); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/importer/external/cinii/CiniiImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/cinii/CiniiImportMetadataSourceServiceImpl.java new file mode 100644 index 000000000000..39b345c552c7 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/cinii/CiniiImportMetadataSourceServiceImpl.java @@ -0,0 +1,391 @@ +/** + * 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.importer.external.cinii; + +import java.io.IOException; +import java.io.StringReader; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.Callable; +import javax.el.MethodNotFoundException; + +import org.apache.commons.lang.StringUtils; +import org.apache.http.HttpException; +import org.apache.http.client.utils.URIBuilder; +import org.apache.log4j.Logger; +import org.dspace.content.Item; +import org.dspace.importer.external.datamodel.ImportRecord; +import org.dspace.importer.external.datamodel.Query; +import org.dspace.importer.external.exception.MetadataSourceException; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; +import org.dspace.importer.external.scopus.service.LiveImportClient; +import org.dspace.importer.external.service.AbstractImportMetadataSourceService; +import org.dspace.importer.external.service.components.QuerySource; +import org.dspace.services.ConfigurationService; +import org.jdom2.Attribute; +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.JDOMException; +import org.jdom2.Namespace; +import org.jdom2.filter.Filters; +import org.jdom2.input.SAXBuilder; +import org.jdom2.xpath.XPathExpression; +import org.jdom2.xpath.XPathFactory; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Implements a data source for querying Cinii + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com) + */ +public class CiniiImportMetadataSourceServiceImpl extends AbstractImportMetadataSourceService + implements QuerySource { + + private static final Logger log = Logger.getLogger(CiniiImportMetadataSourceServiceImpl.class); + + private static final String ENDPOINT_SEARCH = "https://ci.nii.ac.jp/naid/"; + + @Autowired + private LiveImportClient liveImportClient; + + @Autowired + private ConfigurationService configurationService; + + @Override + public String getImportSource() { + return "cinii"; + } + + @Override + public void init() throws Exception {} + + @Override + public ImportRecord getRecord(String id) throws MetadataSourceException { + List records = retry(new SearchByIdCallable(id)); + return records == null || records.isEmpty() ? null : records.get(0); + } + + @Override + public int getRecordsCount(String query) throws MetadataSourceException { + return retry(new CountByQueryCallable(query)); + } + + @Override + public int getRecordsCount(Query query) throws MetadataSourceException { + return retry(new CountByQueryCallable(query)); + } + + @Override + public Collection getRecords(String query, int start, int count) throws MetadataSourceException { + return retry(new SearchByQueryCallable(query, count, start)); + } + + @Override + public Collection getRecords(Query query) throws MetadataSourceException { + return retry(new SearchByQueryCallable(query)); + } + + @Override + public ImportRecord getRecord(Query query) throws MetadataSourceException { + List records = retry(new SearchByIdCallable(query)); + return records == null || records.isEmpty() ? null : records.get(0); + } + + @Override + public Collection findMatchingRecords(Query query) throws MetadataSourceException { + return retry(new FindMatchingRecordCallable(query)); + } + + @Override + public Collection findMatchingRecords(Item item) throws MetadataSourceException { + throw new MethodNotFoundException("This method is not implemented for CrossRef"); + } + + + private class SearchByQueryCallable implements Callable> { + + private Query query; + + private SearchByQueryCallable(String queryString, Integer maxResult, Integer start) { + query = new Query(); + query.addParameter("query", queryString); + query.addParameter("count", maxResult); + query.addParameter("start", start); + } + + private SearchByQueryCallable(Query query) { + this.query = query; + } + + @Override + public List call() throws Exception { + List records = new LinkedList(); + Integer count = query.getParameterAsClass("count", Integer.class); + Integer start = query.getParameterAsClass("start", Integer.class); + String queryString = query.getParameterAsClass("query", String.class); + String appId = configurationService.getProperty("submission.lookup.cinii.appid"); + List ids = getCiniiIds(appId, count, null, null, null, start, queryString); + if (ids != null && ids.size() > 0) { + for (String id : ids) { + List tmp = search(id, appId); + if (tmp != null) { + MetadatumDTO metadatumDto = new MetadatumDTO(); + metadatumDto.setSchema("dc"); + metadatumDto.setElement("identifier"); + metadatumDto.setQualifier("other"); + metadatumDto.setValue(id); + for (ImportRecord ir : tmp) { + ir.addValue(metadatumDto); + } + records.addAll(tmp); + } + } + } + return records; + } + } + + private class SearchByIdCallable implements Callable> { + private Query query; + + private SearchByIdCallable(Query query) { + this.query = query; + } + + private SearchByIdCallable(String id) { + this.query = new Query(); + query.addParameter("id", id); + } + + @Override + public List call() throws Exception { + String appId = configurationService.getProperty("submission.lookup.cinii.appid"); + String id = query.getParameterAsClass("id", String.class); + List importRecord = search(id, appId); + MetadatumDTO metadatumDto = new MetadatumDTO(); + metadatumDto.setSchema("dc"); + metadatumDto.setElement("identifier"); + metadatumDto.setQualifier("other"); + metadatumDto.setValue(id); + for (ImportRecord ir : importRecord) { + ir.addValue(metadatumDto); + } + return importRecord; + } + } + + private class FindMatchingRecordCallable implements Callable> { + + private Query query; + + private FindMatchingRecordCallable(Query q) { + query = q; + } + + @Override + public List call() throws Exception { + List records = new LinkedList(); + String title = query.getParameterAsClass("title", String.class); + String author = query.getParameterAsClass("author", String.class); + Integer year = query.getParameterAsClass("year", Integer.class); + Integer maxResult = query.getParameterAsClass("maxResult", Integer.class); + Integer start = query.getParameterAsClass("start", Integer.class); + String appId = configurationService.getProperty("submission.lookup.cinii.appid"); + List ids = getCiniiIds(appId, maxResult, author, title, year, start, null); + if (ids != null && ids.size() > 0) { + for (String id : ids) { + List tmp = search(id, appId); + if (tmp != null) { + MetadatumDTO metadatumDto = new MetadatumDTO(); + metadatumDto.setSchema("dc"); + metadatumDto.setElement("identifier"); + metadatumDto.setQualifier("other"); + metadatumDto.setValue(id); + for (ImportRecord ir : tmp) { + ir.addValue(metadatumDto); + } + records.addAll(tmp); + } + } + } + return records; + } + + } + + private class CountByQueryCallable implements Callable { + private Query query; + + + private CountByQueryCallable(String queryString) { + query = new Query(); + query.addParameter("query", queryString); + } + + private CountByQueryCallable(Query query) { + this.query = query; + } + + @Override + public Integer call() throws Exception { + String appId = configurationService.getProperty("submission.lookup.cinii.appid"); + String queryString = query.getParameterAsClass("query", String.class); + return countCiniiElement(appId, null, null, null, null, null, queryString); + } + } + + + /** + * Get metadata by searching CiNii RDF API with CiNii NAID + * + * @param id CiNii NAID to search by + * @param appId registered application identifier for the API + * @return record metadata + * @throws IOException A general class of exceptions produced by failed or interrupted I/O operations. + * @throws HttpException Represents a XML/HTTP fault and provides access to the HTTP status code. + */ + protected List search(String id, String appId) + throws IOException, HttpException { + try { + List records = new LinkedList(); + URIBuilder uriBuilder = new URIBuilder(ENDPOINT_SEARCH + id + ".rdf?appid=" + appId); + String response = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), + new HashMap()); + List elements = splitToRecords(response); + for (Element record : elements) { + records.add(transformSourceRecords(record)); + } + return records; + } catch (URISyntaxException e) { + log.error(e.getMessage(), e); + throw new RuntimeException(e.getMessage(), e); + } + } + + private List splitToRecords(String recordsSrc) { + try { + SAXBuilder saxBuilder = new SAXBuilder(); + Document document = saxBuilder.build(new StringReader(recordsSrc)); + Element root = document.getRootElement(); + List namespaces = Arrays + .asList(Namespace.getNamespace("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#")); + XPathExpression xpath = XPathFactory.instance().compile("rdf:Description", Filters.element(), + null, namespaces); + Element record = xpath.evaluateFirst(root); + return Arrays.asList(record); + } catch (JDOMException | IOException e) { + log.error(e.getMessage(), e); + throw new RuntimeException(e.getMessage(), e); + } + } + + private List getCiniiIds(String appId, Integer maxResult, String author, String title, + Integer year, Integer start, String query) { + try { + List ids = new ArrayList<>(); + URIBuilder uriBuilder = new URIBuilder("https://ci.nii.ac.jp/opensearch/search"); + uriBuilder.addParameter("format", "rss"); + if (StringUtils.isNotBlank(appId)) { + uriBuilder.addParameter("appid", appId); + } + if (maxResult != null && maxResult != 0) { + uriBuilder.addParameter("count", maxResult.toString()); + } + if (start != null) { + uriBuilder.addParameter("start", start.toString()); + } + if (title != null) { + uriBuilder.addParameter("title", title); + } + if (author != null) { + uriBuilder.addParameter("author", author); + } + if (query != null) { + uriBuilder.addParameter("q", query); + } + if (year != null && year != -1 && year != 0) { + uriBuilder.addParameter("year_from", String.valueOf(year)); + uriBuilder.addParameter("year_to", String.valueOf(year)); + } + String response = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), + new HashMap()); + int url_len = "http://ci.nii.ac.jp/naid/".length(); + SAXBuilder saxBuilder = new SAXBuilder(); + Document document = saxBuilder.build(new StringReader(response)); + Element root = document.getRootElement(); + List namespaces = Arrays.asList( + Namespace.getNamespace("ns", "http://purl.org/rss/1.0/"), + Namespace.getNamespace("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#")); + XPathExpression xpath = XPathFactory.instance().compile("//ns:item/@rdf:about", + Filters.attribute(), null, namespaces); + List recordsList = xpath.evaluate(root); + for (Attribute item : recordsList) { + String value = item.getValue(); + if (value.length() > url_len) { + ids.add(value.substring(url_len + 1)); + } + } + return ids; + } catch (JDOMException | IOException | URISyntaxException e) { + log.error(e.getMessage(), e); + throw new RuntimeException(e.getMessage(), e); + } + } + + private Integer countCiniiElement(String appId, Integer maxResult, String author, String title, + Integer year, Integer start, String query) { + try { + URIBuilder uriBuilder = new URIBuilder("https://ci.nii.ac.jp/opensearch/search"); + uriBuilder.addParameter("format", "rss"); + uriBuilder.addParameter("appid", appId); + if (maxResult != null && maxResult != 0) { + uriBuilder.addParameter("count", maxResult.toString()); + } + if (start != null) { + uriBuilder.addParameter("start", start.toString()); + } + if (title != null) { + uriBuilder.addParameter("title", title); + } + if (author != null) { + uriBuilder.addParameter("author", author); + } + if (query != null) { + uriBuilder.addParameter("q", query); + } + if (year != null && year != -1 && year != 0) { + uriBuilder.addParameter("year_from", String.valueOf(year)); + uriBuilder.addParameter("year_to", String.valueOf(year)); + } + String response = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), + new HashMap()); + + SAXBuilder saxBuilder = new SAXBuilder(); + Document document = saxBuilder.build(new StringReader(response)); + Element root = document.getRootElement(); + List namespaces = Arrays + .asList(Namespace.getNamespace("opensearch", "http://a9.com/-/spec/opensearch/1.1/")); + XPathExpression xpath = XPathFactory.instance().compile("//opensearch:totalResults", + Filters.element(), null, namespaces); + List nodes = xpath.evaluate(root); + if (nodes != null && !nodes.isEmpty()) { + return Integer.parseInt(((Element) nodes.get(0)).getText()); + } + return 0; + } catch (JDOMException | IOException | URISyntaxException e) { + log.error(e.getMessage(), e); + throw new RuntimeException(e.getMessage(), e); + } + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml b/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml index c9e901d89fed..26849f88e122 100644 --- a/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml +++ b/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml @@ -134,6 +134,11 @@
+ + + + + diff --git a/dspace/config/spring/api/cinii-integration.xml b/dspace/config/spring/api/cinii-integration.xml new file mode 100644 index 000000000000..04bc267e3664 --- /dev/null +++ b/dspace/config/spring/api/cinii-integration.xml @@ -0,0 +1,110 @@ + + + + + + + + + + Defines which metadatum is mapped on which metadatum. Note that while the key must be unique it + only matters here for postprocessing of the value. The mapped MetadatumContributor has full control over + what metadatafield is generated. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dspace/config/spring/api/external-services.xml b/dspace/config/spring/api/external-services.xml index 35c238135129..ea09a6d46c36 100644 --- a/dspace/config/spring/api/external-services.xml +++ b/dspace/config/spring/api/external-services.xml @@ -116,5 +116,16 @@ + + + + + + + Publication + + + + From f5a1f2f9d86957804afc920d1a903a87b3effaa6 Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Fri, 1 Apr 2022 13:02:35 +0200 Subject: [PATCH 0113/1846] [CST-5303] added tests for cinii live import service --- .../dspaceFolder/assetstore/cinii-first.xml | 114 +++++++++++ .../assetstore/cinii-responce-ids.xml | 69 +++++++ .../dspaceFolder/assetstore/cinii-second.xml | 151 +++++++++++++++ .../CiniiImportMetadataSourceServiceIT.java | 177 ++++++++++++++++++ .../src/test/resources/test-config.properties | 5 +- 5 files changed, 515 insertions(+), 1 deletion(-) create mode 100644 dspace-api/src/test/data/dspaceFolder/assetstore/cinii-first.xml create mode 100644 dspace-api/src/test/data/dspaceFolder/assetstore/cinii-responce-ids.xml create mode 100644 dspace-api/src/test/data/dspaceFolder/assetstore/cinii-second.xml create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/CiniiImportMetadataSourceServiceIT.java diff --git a/dspace-api/src/test/data/dspaceFolder/assetstore/cinii-first.xml b/dspace-api/src/test/data/dspaceFolder/assetstore/cinii-first.xml new file mode 100644 index 000000000000..f002f4b0bf31 --- /dev/null +++ b/dspace-api/src/test/data/dspaceFolder/assetstore/cinii-first.xml @@ -0,0 +1,114 @@ + + + + + + A Review of the Chinese Government Support and Sustainability Assessment for Ecovillage Development with a Global Perspective + Gao Xihong + Wang Fan + Liu Chenxi + Luo Tao + Zhang Yukun + Nuti Camillo + 空間計画と持続可能な開発国際学会 + International Review for Spatial Planning and Sustainable Development + 10 + 1 + 43 + 73 + 2022 + <p>Having achieved substantial progress in urban development over the past three decades, the Chinese government has turned to ecovillage development as one of the more effective ways to solve increasingly serious rural issues, such as poverty, rural hollowing, a deteriorating natural environment, and farmland abandonment. However, in spite of various promotional policies and substantial financial investment, there are very few studies assessing the impact of governmental support on ecovillage development. This paper presents a study applying both qualitative research and quantitative analysis to compare the effects of the support, especially in funding and policies, on their development. A comparison was made of three cases, one in China and two elsewhere. To provide a common basis for comparison, three quantification based assessments were examined with a view to applying them in this study.<b> </b>These were the Evaluation for Construction of Beautiful Village (ECBV) from China, and the BREEAM Community and LEED-ND, two well-established international examples. The following analyses using the three methods reveal the strengths and weaknesses of the Chinese government support in ecovillage development, and limitations of the quantification-based assessment methods. Proposals are made for improving the nature of government support and the use of the ECBV. These research outcomes can help formulate the rural development policies in the critical time of socio-economic transition in China, and the research process could be a reference to review quantification based assessment methods in other developing countries with similar levels of development. </p> + + + + + + 2022 + 130008141851 + ENG + J-STAGE + + 10.14246/irspsd.10.1_43 + + + + A Review of the Chinese Government Support and Sustainability Assessment for Ecovillage Development with a Global Perspective + Gao Xihong + Wang Fan + Liu Chenxi + Luo Tao + Zhang Yukun + Nuti Camillo + International Community of Spatial Planning and Sustainable Development + International Review for Spatial Planning and Sustainable Development + <p>Having achieved substantial progress in urban development over the past three decades, the Chinese government has turned to ecovillage development as one of the more effective ways to solve increasingly serious rural issues, such as poverty, rural hollowing, a deteriorating natural environment, and farmland abandonment. However, in spite of various promotional policies and substantial financial investment, there are very few studies assessing the impact of governmental support on ecovillage development. This paper presents a study applying both qualitative research and quantitative analysis to compare the effects of the support, especially in funding and policies, on their development. A comparison was made of three cases, one in China and two elsewhere. To provide a common basis for comparison, three quantification based assessments were examined with a view to applying them in this study.<b> </b>These were the Evaluation for Construction of Beautiful Village (ECBV) from China, and the BREEAM Community and LEED-ND, two well-established international examples. The following analyses using the three methods reveal the strengths and weaknesses of the Chinese government support in ecovillage development, and limitations of the quantification-based assessment methods. Proposals are made for improving the nature of government support and the use of the ECBV. These research outcomes can help formulate the rural development policies in the critical time of socio-economic transition in China, and the research process could be a reference to review quantification based assessment methods in other developing countries with similar levels of development. </p> + + + + + + + + + + + Gao Xihong + + + School of Architecture and Urban Planning, Fuzhou University|Department of Architecture, Roma Tre University + + + + + + + Wang Fan + + + School of Architecture and Urban Planning, Fuzhou University|Institute of Sustainable Building Design, School of Energy, Geoscience, Infrastructure and Society, Heriot-Watt University + + + + + + + Liu Chenxi + + + Institute of Sustainable Building Design, School of Energy, Geoscience, Infrastructure and Society, Heriot-Watt University + + + + + + + Luo Tao + + + School of Architecture and Urban Planning, Fuzhou University + + + + + + + Zhang Yukun + + + School of Architecture, Tianjin University + + + + + + + Nuti Camillo + + + Department of Architecture, Roma Tre University + + + + + + \ No newline at end of file diff --git a/dspace-api/src/test/data/dspaceFolder/assetstore/cinii-responce-ids.xml b/dspace-api/src/test/data/dspaceFolder/assetstore/cinii-responce-ids.xml new file mode 100644 index 000000000000..20db8bdba19d --- /dev/null +++ b/dspace-api/src/test/data/dspaceFolder/assetstore/cinii-responce-ids.xml @@ -0,0 +1,69 @@ + + + + CiNii OpenSearch - roma + CiNii OpenSearch - roma + https://ci.nii.ac.jp/opensearch/search?format=rss&q=roma&count=2&start=1&REMOTE_ADDR2=93.45.72.79 + 2022-04-01T18:36:12+09:00 + 32 + 1 + 2 + + + + + + + + + A Review of the Chinese Government Support and Sustainability Assessment for Ecovillage Development with a Global Perspective + https://ci.nii.ac.jp/naid/130008141851 + + Gao Xihong + Wang Fan + Liu Chenxi + Luo Tao + Zhang Yukun + Nuti Camillo + 空間計画と持続可能な開発国際学会 + International Review for Spatial Planning and Sustainable Development + 10 + 1 + 43 + 73 + 2022 + <p>Having achieved substantial progress in urban development over the past three decades, the Chinese government has turned to ecovillage development as one of the more effective ways to solve increasingly serious rural issues, such as poverty, rural hollowing, a deteriorating natural environment, and farmland abandonment. However, in spite of various promotional policies and substantial financial investment, there are very few studies assessing the impact of governmental support on ecovillage development. This paper presents a study applying both qualitative research and quantitative analysis to compare the effects of the support, especially in funding and policies, on their development. A comparison was made of three cases, one in China and two elsewhere. To provide a common basis for comparison, three quantification based assessments were examined with a view to applying them in this study.<b> </b>These were the Evaluation for Construction of Beautiful Village (ECBV) from China, and the BREEAM Community and LEED-ND, two well-established international examples. The following analyses using the three methods reveal the strengths and weaknesses of the Chinese government support in ecovillage development, and limitations of the quantification-based assessment methods. Proposals are made for improving the nature of government support and the use of the ECBV. These research outcomes can help formulate the rural development policies in the critical time of socio-economic transition in China, and the research process could be a reference to review quantification based assessment methods in other developing countries with similar levels of development. </p> + 2022 + + + Surface Electronic States and Inclining Surfaces in MoTe2 Probed by Photoemission Spectromicroscopy + https://ci.nii.ac.jp/naid/210000159181 + + Matsumoto Ryoya + Saini Naurang L. + Giampietri Alessio + Kandyba Viktor + Barinov Alexey + Jha Rajveer + Higashinaka Ryuji + Matsuda Tatsuma D. + Aoki Yuji + Mizokawa Takashi + Physical Society of Japan + Journal of the Physical Society of Japan + 0031-9015 + 90 + 8 + 84704 + 84704 + 2021-08-15 + 2021-08-15 + + \ No newline at end of file diff --git a/dspace-api/src/test/data/dspaceFolder/assetstore/cinii-second.xml b/dspace-api/src/test/data/dspaceFolder/assetstore/cinii-second.xml new file mode 100644 index 000000000000..37f0df47291f --- /dev/null +++ b/dspace-api/src/test/data/dspaceFolder/assetstore/cinii-second.xml @@ -0,0 +1,151 @@ + + + + + + Surface Electronic States and Inclining Surfaces in MoTe2 Probed by Photoemission Spectromicroscopy + Matsumoto Ryoya + Saini Naurang L. + Giampietri Alessio + Kandyba Viktor + Barinov Alexey + Jha Rajveer + Higashinaka Ryuji + Matsuda Tatsuma D. + Aoki Yuji + Mizokawa Takashi + Physical Society of Japan + Journal of the Physical Society of Japan + 0031-9015 + 90 + 8 + 84704 + 84704 + 2021-08-15 + + 2021-08-15 + 210000159181 + Crossref + + + + + Surface Electronic States and Inclining Surfaces in MoTe2 Probed by Photoemission Spectromicroscopy + Matsumoto Ryoya + Saini Naurang L. + Giampietri Alessio + Kandyba Viktor + Barinov Alexey + Jha Rajveer + Higashinaka Ryuji + Matsuda Tatsuma D. + Aoki Yuji + Mizokawa Takashi + Physical Society of Japan + Journal of the Physical Society of Japan + + + + + + + Matsumoto Ryoya + + + Department of Applied Physics, Waseda University, Shinjuku, Tokyo 169-8555, Japan + + + + + + + Saini Naurang L. + + + Dipartimento di Fisica, Universitá di Roma "La Sapienza" - Piazzale Aldo Moro 2, 00185 Roma, Italy + + + + + + + Giampietri Alessio + + + Sincrotrone Trieste S.C.p.A., Area Science Park, 34012 Basovizza, Trieste, Italy + + + + + + + Kandyba Viktor + + + Sincrotrone Trieste S.C.p.A., Area Science Park, 34012 Basovizza, Trieste, Italy + + + + + + + Barinov Alexey + + + Sincrotrone Trieste S.C.p.A., Area Science Park, 34012 Basovizza, Trieste, Italy + + + + + + + Jha Rajveer + + + Department of Physics, Tokyo Metropolitan University, Hachioji, Tokyo 192-0397, Japan + + + + + + + Higashinaka Ryuji + + + Department of Physics, Tokyo Metropolitan University, Hachioji, Tokyo 192-0397, Japan + + + + + + + Matsuda Tatsuma D. + + + Department of Physics, Tokyo Metropolitan University, Hachioji, Tokyo 192-0397, Japan + + + + + + + Aoki Yuji + + + Department of Physics, Tokyo Metropolitan University, Hachioji, Tokyo 192-0397, Japan + + + + + + + Mizokawa Takashi + + + Department of Applied Physics, Waseda University, Shinjuku, Tokyo 169-8555, Japan + + + + + + \ No newline at end of file diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CiniiImportMetadataSourceServiceIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CiniiImportMetadataSourceServiceIT.java new file mode 100644 index 000000000000..ad58b250aaaf --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CiniiImportMetadataSourceServiceIT.java @@ -0,0 +1,177 @@ +/** + * 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.rest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +import java.io.FileInputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; + +import org.apache.commons.io.IOUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.impl.client.CloseableHttpClient; +import org.dspace.importer.external.cinii.CiniiImportMetadataSourceServiceImpl; +import org.dspace.importer.external.datamodel.ImportRecord; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; +import org.dspace.importer.external.scopus.service.LiveImportClientImpl; +import org.junit.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Integration tests for {@link CiniiImportMetadataSourceServiceImpl} + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com) + */ +public class CiniiImportMetadataSourceServiceIT extends AbstractLiveImportIntegrationTest { + + @Autowired + private LiveImportClientImpl liveImportClientImpl; + + @Autowired + private CiniiImportMetadataSourceServiceImpl ciniiServiceImpl; + + @Test + public void ciniiImportMetadataGetRecordsTest() throws Exception { + context.turnOffAuthorisationSystem(); + CloseableHttpClient originalHttpClient = liveImportClientImpl.getHttpClient(); + CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + String path = testProps.get("test.ciniiIds").toString(); + String path2 = testProps.get("test.ciniiFirst").toString(); + String path3 = testProps.get("test.ciniiSecond").toString(); + FileInputStream ciniiIds = null; + FileInputStream ciniiFirst = null; + FileInputStream ciniiSecond = null; + try { + ciniiIds = new FileInputStream(path); + ciniiFirst = new FileInputStream(path2); + ciniiSecond = new FileInputStream(path3); + + String ciniiIdsXmlResp = IOUtils.toString(ciniiIds, Charset.defaultCharset()); + String ciniiFirstXmlResp = IOUtils.toString(ciniiFirst, Charset.defaultCharset()); + String ciniiSecondXmlResp = IOUtils.toString(ciniiSecond, Charset.defaultCharset()); + + liveImportClientImpl.setHttpClient(httpClient); + CloseableHttpResponse response = mockResponse(ciniiIdsXmlResp, 200, "OK"); + CloseableHttpResponse response2 = mockResponse(ciniiFirstXmlResp, 200, "OK"); + CloseableHttpResponse response3 = mockResponse(ciniiSecondXmlResp, 200, "OK"); + when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response, response2, response3); + + context.restoreAuthSystemState(); + Collection collection2match = getRecords(); + Collection recordsImported = ciniiServiceImpl.getRecords("test query", 0, 2); + assertEquals(2, recordsImported.size()); + assertTrue(matchRecords(recordsImported, collection2match)); + } finally { + liveImportClientImpl.setHttpClient(originalHttpClient); + if (Objects.nonNull(ciniiIds)) { + ciniiIds.close(); + } + if (Objects.nonNull(ciniiFirst)) { + ciniiFirst.close(); + } + if (Objects.nonNull(ciniiSecond)) { + ciniiSecond.close(); + } + } + } + + @Test + public void ciniiImportMetadataGetRecordsCountTest() throws Exception { + context.turnOffAuthorisationSystem(); + CloseableHttpClient originalHttpClient = liveImportClientImpl.getHttpClient(); + CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + String path = testProps.get("test.ciniiIds").toString(); + try (FileInputStream file = new FileInputStream(path)) { + String ciniiXmlResp = IOUtils.toString(file, Charset.defaultCharset()); + + liveImportClientImpl.setHttpClient(httpClient); + CloseableHttpResponse response = mockResponse(ciniiXmlResp, 200, "OK"); + when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response); + + context.restoreAuthSystemState(); + int tot = ciniiServiceImpl.getRecordsCount("test query"); + assertEquals(32, tot); + } finally { + liveImportClientImpl.setHttpClient(originalHttpClient); + } + } + + private Collection getRecords() { + Collection records = new LinkedList(); + //define first record + List metadatums = new ArrayList(); + MetadatumDTO title = createMetadatumDTO("dc", "title", null, + "A Review of the Chinese Government Support and Sustainability" + + " Assessment for Ecovillage Development with a Global Perspective"); + MetadatumDTO source = createMetadatumDTO("dc", "source", null, + "International Review for Spatial Planning and Sustainable Development"); + MetadatumDTO date = createMetadatumDTO("dc", "date", "issued", "2022"); + MetadatumDTO language = createMetadatumDTO("dc", "language", "iso", "ENG"); + MetadatumDTO identifier = createMetadatumDTO("dc", "identifier", "other", "130008141851"); + MetadatumDTO description = createMetadatumDTO("dc", "description", "abstract", + "

Having achieved substantial progress in urban development over the past three decades," + + " the Chinese government has turned to ecovillage development as one of the more effective" + + " ways to solve increasingly serious rural issues, such as poverty, rural hollowing," + + " a deteriorating natural environment, and farmland abandonment. However, in spite of" + + " various promotional policies and substantial financial investment, there are very few" + + " studies assessing the impact of governmental support on ecovillage development." + + " This paper presents a study applying both qualitative research and quantitative analysis" + + " to compare the effects of the support, especially in funding and policies, on their development." + + " A comparison was made of three cases, one in China and two elsewhere. To provide a common basis" + + " for comparison, three quantification based assessments were examined with a view to applying" + + " them in this study. These were the Evaluation for Construction of Beautiful Village" + + " (ECBV) from China, and the BREEAM Community and LEED-ND, two well-established international" + + " examples. The following analyses using the three methods reveal the strengths and weaknesses" + + " of the Chinese government support in ecovillage development, and limitations of the" + + " quantification-based assessment methods. Proposals are made for improving the nature of" + + " government support and the use of the ECBV. These research outcomes can help formulate" + + " the rural development policies in the critical time of socio-economic transition in China," + + " and the research process could be a reference to review quantification based assessment" + + " methods in other developing countries with similar levels of development.

"); + + metadatums.add(title); + metadatums.add(source); + metadatums.add(date); + metadatums.add(language); + metadatums.add(identifier); + metadatums.add(description); + + ImportRecord firstrRecord = new ImportRecord(metadatums); + + //define second record + List metadatums2 = new ArrayList(); + MetadatumDTO title2 = createMetadatumDTO("dc", "title", null, + "Surface Electronic States and Inclining Surfaces in MoTe2 Probed by Photoemission Spectromicroscopy"); + MetadatumDTO source2 = createMetadatumDTO("dc", "source", null, + "Journal of the Physical Society of Japan"); + MetadatumDTO date2 = createMetadatumDTO("dc", "date", "issued", "2021-08-15"); + MetadatumDTO issn = createMetadatumDTO("dc", "identifier", "issn", "0031-9015"); + MetadatumDTO identifier2 = createMetadatumDTO("dc", "identifier", "other", "210000159181"); + + metadatums2.add(title2); + metadatums2.add(source2); + metadatums2.add(date2); + metadatums2.add(issn); + metadatums2.add(identifier2); + + ImportRecord secondRecord = new ImportRecord(metadatums2); + records.add(firstrRecord); + records.add(secondRecord); + return records; + } + +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/resources/test-config.properties b/dspace-server-webapp/src/test/resources/test-config.properties index 8ff06d65f5b0..2f24a55d5e2f 100644 --- a/dspace-server-webapp/src/test/resources/test-config.properties +++ b/dspace-server-webapp/src/test/resources/test-config.properties @@ -18,4 +18,7 @@ test.scopus = ./target/testing/dspace/assetstore/scopus-ex.xml test.scopus-empty = ./target/testing/dspace/assetstore/scopus-empty-resp.xml test.pubmedeurope = ./target/testing/dspace/assetstore/pubmedeurope-test.xml test.pubmedeurope-empty = ./target/testing/dspace/assetstore/pubmedeurope-empty.xml -test.wos = ./target/testing/dspace/assetstore/wos-responce.xml \ No newline at end of file +test.wos = ./target/testing/dspace/assetstore/wos-responce.xml +test.ciniiIds = ./target/testing/dspace/assetstore/cinii-responce-ids.xml +test.ciniiFirst = ./target/testing/dspace/assetstore/cinii-first.xml +test.ciniiSecond = ./target/testing/dspace/assetstore/cinii-second.xml \ No newline at end of file From c7a8e1b3b54ef5654b049b965f424a856f3e5d4e Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 23 Mar 2022 11:52:14 -0500 Subject: [PATCH 0114/1846] Update to latest Solr to prove it doesn't break SWORDv2 --- dspace-rest/pom.xml | 11 ++++++++ pom.xml | 68 ++++++++++++++++++++++++++++++++++++++------- 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/dspace-rest/pom.xml b/dspace-rest/pom.xml index 4433cffd33c5..5377dab901d3 100644 --- a/dspace-rest/pom.xml +++ b/dspace-rest/pom.xml @@ -102,6 +102,17 @@ com.fasterxml.jackson.module jackson-module-jaxb-annotations ${jackson.version} + + + + jakarta.activation + jakarta.activation-api + + + jakarta.xml.bind + jakarta.xml.bind-api + +
diff --git a/pom.xml b/pom.xml index f050e1e36fa5..ff74fb8b6b70 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ 5.6.5.Final 6.0.23.Final 42.3.3 - 8.8.1 + 8.11.1 3.4.0 2.10.0 @@ -57,7 +57,7 @@ https://jena.apache.org/documentation/migrate_jena2_jena3.html --> 2.13.0 - 2.30.1 + 2.35 UTF-8 @@ -1106,6 +1106,13 @@ org.postgresql postgresql ${postgresql.driver.version} + + + + org.checkerframework + checker-qual + +
+ + + com.rometools + rome + + + org.tukaani + xz + com.google.protobuf protobuf-java - net.sf.ehcache - ehcache-core + com.healthmarketscience.jackcess + jackcess + + + com.healthmarketscience.jackcess + jackcess-encrypt - jakarta.ws.rs - jakarta.ws.rs-api + com.fasterxml.woodstox + woodstox-core - javax.xml.soap - javax.xml.soap-api + org.apache.james + apache-mime4j-dom + + + org.apache.commons + commons-compress + + + org.tallison + isoparser + + + org.tallison + metadata-extractor + + + com.epam + parso org.jvnet.staxex stax-ex + + + com.sun.activation + jakarta.activation + + + org.checkerframework + checker-qual + + xom From 84ffe9db9e7a91de2dc247cb25a9fa7c011c2498 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 28 Mar 2022 11:01:06 -0500 Subject: [PATCH 0115/1846] Upgrade Apache POI --- .../dspace/app/mediafilter/ExcelFilter.java | 2 +- .../dspace/app/mediafilter/PoiWordFilter.java | 6 ++-- .../app/mediafilter/PowerPointFilter.java | 28 +++++++++---------- pom.xml | 18 ++---------- 4 files changed, 18 insertions(+), 36 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/ExcelFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/ExcelFilter.java index c17d168c0435..1ebd7b871534 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/ExcelFilter.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/ExcelFilter.java @@ -12,8 +12,8 @@ import org.apache.commons.io.IOUtils; import org.apache.logging.log4j.Logger; -import org.apache.poi.POITextExtractor; import org.apache.poi.extractor.ExtractorFactory; +import org.apache.poi.extractor.POITextExtractor; import org.apache.poi.hssf.extractor.ExcelExtractor; import org.apache.poi.xssf.extractor.XSSFExcelExtractor; import org.dspace.content.Item; diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/PoiWordFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/PoiWordFilter.java index 8c198c447768..942eafa04d25 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/PoiWordFilter.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/PoiWordFilter.java @@ -12,10 +12,8 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; -import org.apache.poi.POITextExtractor; import org.apache.poi.extractor.ExtractorFactory; -import org.apache.poi.openxml4j.exceptions.OpenXML4JException; -import org.apache.xmlbeans.XmlException; +import org.apache.poi.extractor.POITextExtractor; import org.dspace.content.Item; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -55,7 +53,7 @@ public InputStream getDestinationStream(Item currentItem, InputStream source, bo // get input stream from bitstream, pass to filter, get string back POITextExtractor extractor = ExtractorFactory.createExtractor(source); text = extractor.getText(); - } catch (IOException | OpenXML4JException | XmlException e) { + } catch (IOException e) { System.err.format("Invalid File Format: %s%n", e.getMessage()); LOG.error("Unable to parse the bitstream: ", e); throw e; diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/PowerPointFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/PowerPointFilter.java index 86b7096f68f9..a825064353e4 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/PowerPointFilter.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/PowerPointFilter.java @@ -11,10 +11,10 @@ import java.io.InputStream; import org.apache.logging.log4j.Logger; -import org.apache.poi.POITextExtractor; import org.apache.poi.extractor.ExtractorFactory; -import org.apache.poi.hslf.extractor.PowerPointExtractor; -import org.apache.poi.xslf.extractor.XSLFPowerPointExtractor; +import org.apache.poi.extractor.POITextExtractor; +import org.apache.poi.sl.extractor.SlideShowExtractor; +import org.apache.poi.xslf.extractor.XSLFExtractor; import org.dspace.content.Item; /* @@ -70,7 +70,6 @@ public InputStream getDestinationStream(Item currentItem, InputStream source, bo try { String extractedText = null; - new ExtractorFactory(); POITextExtractor pptExtractor = ExtractorFactory .createExtractor(source); @@ -78,17 +77,16 @@ public InputStream getDestinationStream(Item currentItem, InputStream source, bo // require different classes and APIs for text extraction // If this is a PowerPoint XML file, extract accordingly - if (pptExtractor instanceof XSLFPowerPointExtractor) { - - // The true method arguments indicate that text from - // the slides and the notes is desired - extractedText = ((XSLFPowerPointExtractor) pptExtractor) - .getText(true, true); - } else if (pptExtractor instanceof PowerPointExtractor) { // Legacy PowerPoint files - - extractedText = ((PowerPointExtractor) pptExtractor).getText() - + " " + ((PowerPointExtractor) pptExtractor).getNotes(); - + if (pptExtractor instanceof XSLFExtractor) { + // Extract text from both slides and notes + ((XSLFExtractor) pptExtractor).setNotesByDefault(true); + ((XSLFExtractor) pptExtractor).setSlidesByDefault(true); + extractedText = ((XSLFExtractor) pptExtractor).getText(); + } else if (pptExtractor instanceof SlideShowExtractor) { // Legacy PowerPoint files + // Extract text from both slides and notes + ((SlideShowExtractor) pptExtractor).setNotesByDefault(true); + ((SlideShowExtractor) pptExtractor).setSlidesByDefault(true); + extractedText = ((SlideShowExtractor) pptExtractor).getText(); } if (extractedText != null) { // if verbose flag is set, print out extracted text diff --git a/pom.xml b/pom.xml index ff74fb8b6b70..fbdcd098d2d5 100644 --- a/pom.xml +++ b/pom.xml @@ -39,7 +39,7 @@ 9.4.41.v20210516 2.17.1 2.0.24 - 3.17 + 5.2.2 1.18.0 1.7.25 @@ -1611,25 +1611,11 @@ org.apache.poi poi-ooxml ${poi-version} - - - - com.github.virtuald - curvesapi - - org.apache.poi - poi-ooxml-schemas + poi-ooxml-full ${poi-version} - - - - org.apache.xmlbeans - xmlbeans - - org.apache.xmlbeans From b894071c1666cf24be7281136a2f8453da643b20 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 28 Mar 2022 16:12:46 -0500 Subject: [PATCH 0116/1846] Switch to SolrJ & Solr-Core. We don't actually use Solr-Cell. Upgrade Tika dependencies --- dspace-api/pom.xml | 59 +++--------- dspace-server-webapp/pom.xml | 75 ++++++--------- dspace/modules/server/pom.xml | 25 ----- dspace/solr/search/conf/solrconfig.xml | 3 - pom.xml | 125 ++++++++----------------- 5 files changed, 81 insertions(+), 206 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index b8fae8d86c7b..009b18d0acd8 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -394,7 +394,7 @@ org.ow2.asm asm-commons - + org.bouncycastle bcpkix-jdk15on @@ -606,27 +606,13 @@ httpmime + org.apache.solr solr-solrj ${solr.client.version} - - - - org.eclipse.jetty - jetty-http - - - org.eclipse.jetty - jetty-io - - - org.eclipse.jetty - jetty-util - - - + org.apache.solr @@ -654,39 +640,10 @@ - - org.apache.solr - solr-cell - - - - org.apache.commons - commons-text - - - - org.eclipse.jetty - jetty-http - - - org.eclipse.jetty - jetty-io - - - org.eclipse.jetty - jetty-util - - - org.apache.lucene lucene-core - - - org.apache.tika - tika-parsers - org.apache.lucene lucene-analyzers-icu @@ -707,6 +664,16 @@ xmlbeans + + + org.apache.tika + tika-core + + + org.apache.tika + tika-parsers-standard-package + + com.maxmind.geoip2 geoip2 diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index f90ea51b1585..4677366a2c57 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -371,13 +371,12 @@ dspace-services + + org.dspace dspace-iiif - - - org.dspace dspace-oai @@ -423,6 +422,32 @@ ${nimbus-jose-jwt.version} + + org.apache.solr + solr-solrj + ${solr.client.version} + + + + + org.springframework.boot + spring-boot-starter-cache + ${spring-boot.version} + + + + javax.cache + cache-api + 1.1.0 + + + + org.ehcache + ehcache + ${ehcache.version} + + org.springframework.boot @@ -495,50 +520,6 @@ - - - org.springframework.boot - spring-boot-starter-cache - ${spring-boot.version} - - - - javax.cache - cache-api - 1.1.0 - - - - org.ehcache - ehcache - ${ehcache.version} - - - org.apache.solr - solr-cell - test - - - - org.apache.commons - commons-text - - - - org.eclipse.jetty - jetty-http - - - org.eclipse.jetty - jetty-io - - - org.eclipse.jetty - jetty-util - - - org.apache.lucene lucene-analyzers-icu diff --git a/dspace/modules/server/pom.xml b/dspace/modules/server/pom.xml index c789cf505459..ad47074392fa 100644 --- a/dspace/modules/server/pom.xml +++ b/dspace/modules/server/pom.xml @@ -324,31 +324,6 @@ just adding new jar in the classloader mockito-inline test - - org.apache.solr - solr-cell - test - - - - org.apache.commons - commons-text - - - - org.eclipse.jetty - jetty-http - - - org.eclipse.jetty - jetty-io - - - org.eclipse.jetty - jetty-util - - - org.apache.lucene lucene-analyzers-icu diff --git a/dspace/solr/search/conf/solrconfig.xml b/dspace/solr/search/conf/solrconfig.xml index 204c1a056ef5..8d45105df55e 100644 --- a/dspace/solr/search/conf/solrconfig.xml +++ b/dspace/solr/search/conf/solrconfig.xml @@ -30,9 +30,6 @@ regex='icu4j-.*\.jar'/> - - ${solr.data.dir:} diff --git a/pom.xml b/pom.xml index fbdcd098d2d5..c53a58d10530 100644 --- a/pom.xml +++ b/pom.xml @@ -36,12 +36,15 @@ 2.3.1 1.1.0 - 9.4.41.v20210516 + 9.4.44.v20210927 2.17.1 2.0.24 5.2.2 1.18.0 1.7.25 + 2.3.0 + + 1.70 @@ -1115,28 +1118,6 @@ - - - org.apache.zookeeper - zookeeper - 3.4.14 - - - - - org.apache.james - apache-mime4j-core - 0.8.3 - - - - - org.ow2.asm - asm - 8.0.1 - - org.hibernate hibernate-core @@ -1293,7 +1274,7 @@ org.apache.solr - solr-cell + solr-solrj ${solr.client.version} @@ -1301,69 +1282,43 @@ lucene-core ${solr.client.version} - + + org.apache.tika - tika-parsers - 1.27 - - - - - com.rometools - rome - - - org.tukaani - xz - - - com.google.protobuf - protobuf-java - - - com.healthmarketscience.jackcess - jackcess - - - com.healthmarketscience.jackcess - jackcess-encrypt - - - com.fasterxml.woodstox - woodstox-core - - - org.apache.james - apache-mime4j-dom - - - org.apache.commons - commons-compress - - - org.tallison - isoparser - - - org.tallison - metadata-extractor - - - com.epam - parso - - - org.jvnet.staxex - stax-ex - - - - com.sun.activation - jakarta.activation - - + tika-core + ${tika.version} + + + org.apache.tika + tika-parsers-standard-package + ${tika.version} + + + + org.bouncycastle + bcpkix-jdk15on + ${bouncycastle.version} + + + + org.bouncycastle + bcprov-jdk15on + ${bouncycastle.version} + + + org.apache.james + apache-mime4j-core + 0.8.4 + + + + org.ow2.asm + asm + 8.0.1 + + @@ -1401,7 +1356,7 @@ org.slf4j slf4j-log4j12 - + org.apache.commons commons-csv From e49a1d86af4df9829b39e3775be99d24483d878c Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 28 Mar 2022 16:13:38 -0500 Subject: [PATCH 0117/1846] Fix compilation issues. Old log4j v1 update to v2 --- .../org/dspace/authority/orcid/Orcidv3SolrAuthorityImpl.java | 5 +++-- .../main/java/org/dspace/content/logic/DefaultFilter.java | 5 +++-- .../dspace/content/logic/condition/InCommunityCondition.java | 5 +++-- .../dspace/content/logic/condition/IsWithdrawnCondition.java | 5 +++-- .../content/logic/condition/MetadataValueMatchCondition.java | 5 +++-- .../logic/condition/MetadataValuesMatchCondition.java | 5 +++-- .../content/logic/condition/ReadableByGroupCondition.java | 5 +++-- .../java/org/dspace/google/GoogleAsyncEventListener.java | 5 +++-- 8 files changed, 24 insertions(+), 16 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/authority/orcid/Orcidv3SolrAuthorityImpl.java b/dspace-api/src/main/java/org/dspace/authority/orcid/Orcidv3SolrAuthorityImpl.java index a1c3867fb9d3..6753a5d113b7 100644 --- a/dspace-api/src/main/java/org/dspace/authority/orcid/Orcidv3SolrAuthorityImpl.java +++ b/dspace-api/src/main/java/org/dspace/authority/orcid/Orcidv3SolrAuthorityImpl.java @@ -21,7 +21,8 @@ import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.authority.AuthorityValue; import org.dspace.authority.SolrAuthorityInterface; import org.dspace.external.OrcidRestConnector; @@ -40,7 +41,7 @@ */ public class Orcidv3SolrAuthorityImpl implements SolrAuthorityInterface { - private static Logger log = Logger.getLogger(Orcidv3SolrAuthorityImpl.class); + private final static Logger log = LogManager.getLogger(); private OrcidRestConnector orcidRestConnector; private String OAUTHUrl; diff --git a/dspace-api/src/main/java/org/dspace/content/logic/DefaultFilter.java b/dspace-api/src/main/java/org/dspace/content/logic/DefaultFilter.java index c0649e9ea29f..490c3949ea3a 100644 --- a/dspace-api/src/main/java/org/dspace/content/logic/DefaultFilter.java +++ b/dspace-api/src/main/java/org/dspace/content/logic/DefaultFilter.java @@ -7,7 +7,8 @@ */ package org.dspace.content.logic; -import org.apache.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.content.Item; import org.dspace.core.Context; @@ -21,7 +22,7 @@ */ public class DefaultFilter implements Filter { private LogicalStatement statement; - private static Logger log = Logger.getLogger(Filter.class); + private final static Logger log = LogManager.getLogger(); /** * Set statement from Spring configuration in item-filters.xml diff --git a/dspace-api/src/main/java/org/dspace/content/logic/condition/InCommunityCondition.java b/dspace-api/src/main/java/org/dspace/content/logic/condition/InCommunityCondition.java index b9c1d15d2a5a..9f588f9c3be1 100644 --- a/dspace-api/src/main/java/org/dspace/content/logic/condition/InCommunityCondition.java +++ b/dspace-api/src/main/java/org/dspace/content/logic/condition/InCommunityCondition.java @@ -10,7 +10,8 @@ import java.sql.SQLException; import java.util.List; -import org.apache.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.content.Collection; import org.dspace.content.Community; import org.dspace.content.DSpaceObject; @@ -26,7 +27,7 @@ * @version $Revision$ */ public class InCommunityCondition extends AbstractCondition { - private static Logger log = Logger.getLogger(InCommunityCondition.class); + private final static Logger log = LogManager.getLogger(); /** * Return true if item is in one of the specified collections diff --git a/dspace-api/src/main/java/org/dspace/content/logic/condition/IsWithdrawnCondition.java b/dspace-api/src/main/java/org/dspace/content/logic/condition/IsWithdrawnCondition.java index 6475ef09e24e..6424e6f35f3b 100644 --- a/dspace-api/src/main/java/org/dspace/content/logic/condition/IsWithdrawnCondition.java +++ b/dspace-api/src/main/java/org/dspace/content/logic/condition/IsWithdrawnCondition.java @@ -7,7 +7,8 @@ */ package org.dspace.content.logic.condition; -import org.apache.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.content.Item; import org.dspace.content.logic.LogicalStatementException; import org.dspace.core.Context; @@ -19,7 +20,7 @@ * @version $Revision$ */ public class IsWithdrawnCondition extends AbstractCondition { - private static Logger log = Logger.getLogger(IsWithdrawnCondition.class); + private final static Logger log = LogManager.getLogger(); /** * Return true if item is withdrawn diff --git a/dspace-api/src/main/java/org/dspace/content/logic/condition/MetadataValueMatchCondition.java b/dspace-api/src/main/java/org/dspace/content/logic/condition/MetadataValueMatchCondition.java index d9c774485ac2..4e30c75a2a36 100644 --- a/dspace-api/src/main/java/org/dspace/content/logic/condition/MetadataValueMatchCondition.java +++ b/dspace-api/src/main/java/org/dspace/content/logic/condition/MetadataValueMatchCondition.java @@ -11,7 +11,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.apache.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.content.Item; import org.dspace.content.MetadataValue; import org.dspace.content.logic.LogicalStatementException; @@ -26,7 +27,7 @@ */ public class MetadataValueMatchCondition extends AbstractCondition { - private static Logger log = Logger.getLogger(MetadataValueMatchCondition.class); + private final static Logger log = LogManager.getLogger(); /** * Return true if any value for a specified field in the item matches a specified regex pattern diff --git a/dspace-api/src/main/java/org/dspace/content/logic/condition/MetadataValuesMatchCondition.java b/dspace-api/src/main/java/org/dspace/content/logic/condition/MetadataValuesMatchCondition.java index df9cbfbf1dad..74ccfa4ca842 100644 --- a/dspace-api/src/main/java/org/dspace/content/logic/condition/MetadataValuesMatchCondition.java +++ b/dspace-api/src/main/java/org/dspace/content/logic/condition/MetadataValuesMatchCondition.java @@ -11,7 +11,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.apache.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.content.Item; import org.dspace.content.MetadataValue; import org.dspace.content.logic.LogicalStatementException; @@ -26,7 +27,7 @@ */ public class MetadataValuesMatchCondition extends AbstractCondition { - private static Logger log = Logger.getLogger(MetadataValuesMatchCondition.class); + private final static Logger log = LogManager.getLogger(); /** * Return true if any value for a specified field in the item matches any of the specified regex patterns diff --git a/dspace-api/src/main/java/org/dspace/content/logic/condition/ReadableByGroupCondition.java b/dspace-api/src/main/java/org/dspace/content/logic/condition/ReadableByGroupCondition.java index e76772803c85..65f9925222c1 100644 --- a/dspace-api/src/main/java/org/dspace/content/logic/condition/ReadableByGroupCondition.java +++ b/dspace-api/src/main/java/org/dspace/content/logic/condition/ReadableByGroupCondition.java @@ -10,7 +10,8 @@ import java.sql.SQLException; import java.util.List; -import org.apache.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.factory.AuthorizeServiceFactory; import org.dspace.authorize.service.AuthorizeService; @@ -27,7 +28,7 @@ * @version $Revision$ */ public class ReadableByGroupCondition extends AbstractCondition { - private static Logger log = Logger.getLogger(ReadableByGroupCondition.class); + private final static Logger log = LogManager.getLogger(); // Authorize service AuthorizeService authorizeService = AuthorizeServiceFactory.getInstance().getAuthorizeService(); diff --git a/dspace-api/src/main/java/org/dspace/google/GoogleAsyncEventListener.java b/dspace-api/src/main/java/org/dspace/google/GoogleAsyncEventListener.java index cf5c40976d95..9cca3777ef04 100644 --- a/dspace-api/src/main/java/org/dspace/google/GoogleAsyncEventListener.java +++ b/dspace-api/src/main/java/org/dspace/google/GoogleAsyncEventListener.java @@ -31,7 +31,8 @@ import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; -import org.apache.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.content.factory.ContentServiceFactory; import org.dspace.core.Constants; import org.dspace.core.Context; @@ -55,7 +56,7 @@ public class GoogleAsyncEventListener extends AbstractUsageEventListener { // 20 is the event max set by the GA API private static final int GA_MAX_EVENTS = 20; private static final String ANALYTICS_BATCH_ENDPOINT = "https://www.google-analytics.com/batch"; - private static Logger log = Logger.getLogger(GoogleAsyncEventListener.class); + private final static Logger log = LogManager.getLogger(); private static String analyticsKey; private static CloseableHttpClient httpclient; private static Buffer buffer; From a0438e51addb61aef44d3ced9b11ad22ea600896 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Tue, 29 Mar 2022 09:20:37 -0500 Subject: [PATCH 0118/1846] Remove old version of XMLBeans. We don't use it directly and Tika brings in a new version --- dspace-api/pom.xml | 4 ---- dspace/modules/additions/pom.xml | 4 ---- pom.xml | 5 ----- 3 files changed, 13 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 009b18d0acd8..eb0004b3effb 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -659,10 +659,6 @@ lucene-analyzers-stempel test - - org.apache.xmlbeans - xmlbeans - diff --git a/dspace/modules/additions/pom.xml b/dspace/modules/additions/pom.xml index 9df5c8ef0277..2abc12b5292c 100644 --- a/dspace/modules/additions/pom.xml +++ b/dspace/modules/additions/pom.xml @@ -273,10 +273,6 @@ lucene-analyzers-stempel test - - org.apache.xmlbeans - xmlbeans - junit diff --git a/pom.xml b/pom.xml index c53a58d10530..9442ef1889fa 100644 --- a/pom.xml +++ b/pom.xml @@ -1572,11 +1572,6 @@ poi-ooxml-full ${poi-version} - - org.apache.xmlbeans - xmlbeans - 3.1.0 - xalan xalan From 508883bec8f1a16512abdd3a120665db1d61ede7 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 28 Mar 2022 13:10:30 -0500 Subject: [PATCH 0119/1846] Replace format specific POI filters with TikaTextExtractionFilter --- dspace-api/pom.xml | 4 - .../dspace/app/mediafilter/ExcelFilter.java | 99 ---------- .../dspace/app/mediafilter/HTMLFilter.java | 82 -------- .../org/dspace/app/mediafilter/PDFFilter.java | 137 ------------- .../dspace/app/mediafilter/PoiWordFilter.java | 70 ------- .../app/mediafilter/PowerPointFilter.java | 111 ----------- .../mediafilter/TikaTextExtractionFilter.java | 178 +++++++++++++++++ .../indexobject/IndexFactoryImpl.java | 1 - .../app/mediafilter/PoiWordFilterTest.java | 181 ------------------ .../TikaTextExtractionFilterTest.java | 177 +++++++++++++++++ .../org/dspace/app/mediafilter/test.html | 53 +++++ .../org/dspace/app/mediafilter/test.pdf | Bin 0 -> 56812 bytes .../org/dspace/app/mediafilter/test.txt | 13 ++ dspace/config/dspace.cfg | 54 +++--- pom.xml | 21 -- 15 files changed, 451 insertions(+), 730 deletions(-) delete mode 100644 dspace-api/src/main/java/org/dspace/app/mediafilter/ExcelFilter.java delete mode 100644 dspace-api/src/main/java/org/dspace/app/mediafilter/HTMLFilter.java delete mode 100644 dspace-api/src/main/java/org/dspace/app/mediafilter/PDFFilter.java delete mode 100644 dspace-api/src/main/java/org/dspace/app/mediafilter/PoiWordFilter.java delete mode 100644 dspace-api/src/main/java/org/dspace/app/mediafilter/PowerPointFilter.java create mode 100644 dspace-api/src/main/java/org/dspace/app/mediafilter/TikaTextExtractionFilter.java delete mode 100644 dspace-api/src/test/java/org/dspace/app/mediafilter/PoiWordFilterTest.java create mode 100644 dspace-api/src/test/java/org/dspace/app/mediafilter/TikaTextExtractionFilterTest.java create mode 100644 dspace-api/src/test/resources/org/dspace/app/mediafilter/test.html create mode 100644 dspace-api/src/test/resources/org/dspace/app/mediafilter/test.pdf create mode 100644 dspace-api/src/test/resources/org/dspace/app/mediafilter/test.txt diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index eb0004b3effb..2c998572f47b 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -530,10 +530,6 @@ org.apache.pdfbox fontbox - - org.apache.poi - poi-scratchpad - xalan xalan diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/ExcelFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/ExcelFilter.java deleted file mode 100644 index 1ebd7b871534..000000000000 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/ExcelFilter.java +++ /dev/null @@ -1,99 +0,0 @@ -/** - * 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.mediafilter; - -import java.io.InputStream; -import java.nio.charset.StandardCharsets; - -import org.apache.commons.io.IOUtils; -import org.apache.logging.log4j.Logger; -import org.apache.poi.extractor.ExtractorFactory; -import org.apache.poi.extractor.POITextExtractor; -import org.apache.poi.hssf.extractor.ExcelExtractor; -import org.apache.poi.xssf.extractor.XSSFExcelExtractor; -import org.dspace.content.Item; - -/* - * ExcelFilter - * - * Entries you must add to dspace.cfg: - * - * filter.plugins = blah, \ - * Excel Text Extractor - * - * plugin.named.org.dspace.app.mediafilter.FormatFilter = \ - * blah = blah, \ - * org.dspace.app.mediafilter.ExcelFilter = Excel Text Extractor - * - * #Configure each filter's input Formats - * filter.org.dspace.app.mediafilter.ExcelFilter.inputFormats = Microsoft Excel, Microsoft Excel XML - * - */ -public class ExcelFilter extends MediaFilter { - - private static Logger log = org.apache.logging.log4j.LogManager.getLogger(ExcelFilter.class); - - public String getFilteredName(String oldFilename) { - return oldFilename + ".txt"; - } - - /** - * @return String bundle name - */ - public String getBundleName() { - return "TEXT"; - } - - /** - * @return String bitstream format - */ - public String getFormatString() { - return "Text"; - } - - /** - * @return String description - */ - public String getDescription() { - return "Extracted text"; - } - - /** - * @param item item - * @param source source input stream - * @param verbose verbose mode - * @return InputStream the resulting input stream - * @throws Exception if error - */ - @Override - public InputStream getDestinationStream(Item item, InputStream source, boolean verbose) - throws Exception { - String extractedText = null; - - try { - POITextExtractor theExtractor = ExtractorFactory.createExtractor(source); - if (theExtractor instanceof ExcelExtractor) { - // for xls file - extractedText = (theExtractor).getText(); - } else if (theExtractor instanceof XSSFExcelExtractor) { - // for xlsx file - extractedText = (theExtractor).getText(); - } - } catch (Exception e) { - log.error("Error filtering bitstream: " + e.getMessage(), e); - throw e; - } - - if (extractedText != null) { - // generate an input stream with the extracted text - return IOUtils.toInputStream(extractedText, StandardCharsets.UTF_8); - } - - return null; - } -} diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/HTMLFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/HTMLFilter.java deleted file mode 100644 index 5e10f2841de5..000000000000 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/HTMLFilter.java +++ /dev/null @@ -1,82 +0,0 @@ -/** - * 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.mediafilter; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import javax.swing.text.Document; -import javax.swing.text.html.HTMLEditorKit; - -import org.dspace.content.Item; - -/* - * - * to do: helpful error messages - can't find mediafilter.cfg - can't - * instantiate filter - bitstream format doesn't exist - * - */ -public class HTMLFilter extends MediaFilter { - - @Override - public String getFilteredName(String oldFilename) { - return oldFilename + ".txt"; - } - - /** - * @return String bundle name - */ - @Override - public String getBundleName() { - return "TEXT"; - } - - /** - * @return String bitstream format - */ - @Override - public String getFormatString() { - return "Text"; - } - - /** - * @return String description - */ - @Override - public String getDescription() { - return "Extracted text"; - } - - /** - * @param currentItem item - * @param source source input stream - * @param verbose verbose mode - * @return InputStream the resulting input stream - * @throws Exception if error - */ - @Override - public InputStream getDestinationStream(Item currentItem, InputStream source, boolean verbose) - throws Exception { - // try and read the document - set to ignore character set directive, - // assuming that the input stream is already set properly (I hope) - HTMLEditorKit kit = new HTMLEditorKit(); - Document doc = kit.createDefaultDocument(); - - doc.putProperty("IgnoreCharsetDirective", Boolean.TRUE); - - kit.read(source, doc, 0); - - String extractedText = doc.getText(0, doc.getLength()); - - // generate an input stream with the extracted text - byte[] textBytes = extractedText.getBytes(StandardCharsets.UTF_8); - ByteArrayInputStream bais = new ByteArrayInputStream(textBytes); - - return bais; - } -} diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/PDFFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/PDFFilter.java deleted file mode 100644 index c90d7c5a6e97..000000000000 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/PDFFilter.java +++ /dev/null @@ -1,137 +0,0 @@ -/** - * 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.mediafilter; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.io.OutputStreamWriter; -import java.io.Writer; - -import org.apache.logging.log4j.Logger; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.encryption.InvalidPasswordException; -import org.apache.pdfbox.text.PDFTextStripper; -import org.dspace.content.Item; -import org.dspace.services.ConfigurationService; -import org.dspace.services.factory.DSpaceServicesFactory; - -/* - * - * to do: helpful error messages - can't find mediafilter.cfg - can't - * instantiate filter - bitstream format doesn't exist - * - */ -public class PDFFilter extends MediaFilter { - - private static Logger log = org.apache.logging.log4j.LogManager.getLogger(PDFFilter.class); - - @Override - public String getFilteredName(String oldFilename) { - return oldFilename + ".txt"; - } - - /** - * @return String bundle name - */ - @Override - public String getBundleName() { - return "TEXT"; - } - - /** - * @return String bitstreamformat - */ - @Override - public String getFormatString() { - return "Text"; - } - - /** - * @return String description - */ - @Override - public String getDescription() { - return "Extracted text"; - } - - /** - * @param currentItem item - * @param source source input stream - * @param verbose verbose mode - * @return InputStream the resulting input stream - * @throws Exception if error - */ - @Override - public InputStream getDestinationStream(Item currentItem, InputStream source, boolean verbose) - throws Exception { - ConfigurationService configurationService - = DSpaceServicesFactory.getInstance().getConfigurationService(); - try { - boolean useTemporaryFile = configurationService.getBooleanProperty("pdffilter.largepdfs", false); - - // get input stream from bitstream - // pass to filter, get string back - PDFTextStripper pts = new PDFTextStripper(); - pts.setSortByPosition(true); - PDDocument pdfDoc = null; - Writer writer = null; - File tempTextFile = null; - ByteArrayOutputStream byteStream = null; - - if (useTemporaryFile) { - tempTextFile = File.createTempFile("dspacepdfextract" + source.hashCode(), ".txt"); - tempTextFile.deleteOnExit(); - writer = new OutputStreamWriter(new FileOutputStream(tempTextFile)); - } else { - byteStream = new ByteArrayOutputStream(); - writer = new OutputStreamWriter(byteStream); - } - - try { - pdfDoc = PDDocument.load(source); - pts.writeText(pdfDoc, writer); - } catch (InvalidPasswordException ex) { - log.error("PDF is encrypted. Cannot extract text (item: {})", - () -> currentItem.getHandle()); - return null; - } finally { - try { - if (pdfDoc != null) { - pdfDoc.close(); - } - } catch (Exception e) { - log.error("Error closing PDF file: " + e.getMessage(), e); - } - - try { - writer.close(); - } catch (Exception e) { - log.error("Error closing temporary extract file: " + e.getMessage(), e); - } - } - - if (useTemporaryFile) { - return new FileInputStream(tempTextFile); - } else { - byte[] bytes = byteStream.toByteArray(); - return new ByteArrayInputStream(bytes); - } - } catch (OutOfMemoryError oome) { - log.error("Error parsing PDF document " + oome.getMessage(), oome); - if (!configurationService.getBooleanProperty("pdffilter.skiponmemoryexception", false)) { - throw oome; - } - } - - return null; - } -} diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/PoiWordFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/PoiWordFilter.java deleted file mode 100644 index 942eafa04d25..000000000000 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/PoiWordFilter.java +++ /dev/null @@ -1,70 +0,0 @@ -/** - * 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.mediafilter; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; - -import org.apache.poi.extractor.ExtractorFactory; -import org.apache.poi.extractor.POITextExtractor; -import org.dspace.content.Item; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Extract flat text from Microsoft Word documents (.doc, .docx). - */ -public class PoiWordFilter - extends MediaFilter { - private static final Logger LOG = LoggerFactory.getLogger(PoiWordFilter.class); - - @Override - public String getFilteredName(String oldFilename) { - return oldFilename + ".txt"; - } - - @Override - public String getBundleName() { - return "TEXT"; - } - - @Override - public String getFormatString() { - return "Text"; - } - - @Override - public String getDescription() { - return "Extracted text"; - } - - @Override - public InputStream getDestinationStream(Item currentItem, InputStream source, boolean verbose) - throws Exception { - String text; - try { - // get input stream from bitstream, pass to filter, get string back - POITextExtractor extractor = ExtractorFactory.createExtractor(source); - text = extractor.getText(); - } catch (IOException e) { - System.err.format("Invalid File Format: %s%n", e.getMessage()); - LOG.error("Unable to parse the bitstream: ", e); - throw e; - } - - // if verbose flag is set, print out extracted text to STDOUT - if (verbose) { - System.out.println(text); - } - - // return the extracted text as a stream. - return new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8)); - } -} diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/PowerPointFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/PowerPointFilter.java deleted file mode 100644 index a825064353e4..000000000000 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/PowerPointFilter.java +++ /dev/null @@ -1,111 +0,0 @@ -/** - * 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.mediafilter; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; - -import org.apache.logging.log4j.Logger; -import org.apache.poi.extractor.ExtractorFactory; -import org.apache.poi.extractor.POITextExtractor; -import org.apache.poi.sl.extractor.SlideShowExtractor; -import org.apache.poi.xslf.extractor.XSLFExtractor; -import org.dspace.content.Item; - -/* - * TODO: Allow user to configure extraction of only text or only notes - * - */ -public class PowerPointFilter extends MediaFilter { - - private static Logger log = org.apache.logging.log4j.LogManager.getLogger(PowerPointFilter.class); - - @Override - public String getFilteredName(String oldFilename) { - return oldFilename + ".txt"; - } - - /** - * @return String bundle name - */ - @Override - public String getBundleName() { - return "TEXT"; - } - - /** - * @return String bitstream format - * - * TODO: Check that this is correct - */ - @Override - public String getFormatString() { - return "Text"; - } - - /** - * @return String description - */ - @Override - public String getDescription() { - return "Extracted text"; - } - - /** - * @param currentItem item - * @param source source input stream - * @param verbose verbose mode - * @return InputStream the resulting input stream - * @throws Exception if error - */ - @Override - public InputStream getDestinationStream(Item currentItem, InputStream source, boolean verbose) - throws Exception { - - try { - - String extractedText = null; - POITextExtractor pptExtractor = ExtractorFactory - .createExtractor(source); - - // PowerPoint XML files and legacy format PowerPoint files - // require different classes and APIs for text extraction - - // If this is a PowerPoint XML file, extract accordingly - if (pptExtractor instanceof XSLFExtractor) { - // Extract text from both slides and notes - ((XSLFExtractor) pptExtractor).setNotesByDefault(true); - ((XSLFExtractor) pptExtractor).setSlidesByDefault(true); - extractedText = ((XSLFExtractor) pptExtractor).getText(); - } else if (pptExtractor instanceof SlideShowExtractor) { // Legacy PowerPoint files - // Extract text from both slides and notes - ((SlideShowExtractor) pptExtractor).setNotesByDefault(true); - ((SlideShowExtractor) pptExtractor).setSlidesByDefault(true); - extractedText = ((SlideShowExtractor) pptExtractor).getText(); - } - if (extractedText != null) { - // if verbose flag is set, print out extracted text - // to STDOUT - if (verbose) { - System.out.println(extractedText); - } - - // generate an input stream with the extracted text - byte[] textBytes = extractedText.getBytes(); - ByteArrayInputStream bais = new ByteArrayInputStream(textBytes); - - return bais; - } - } catch (Exception e) { - log.error("Error filtering bitstream: " + e.getMessage(), e); - throw e; - } - - return null; - } -} diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/TikaTextExtractionFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/TikaTextExtractionFilter.java new file mode 100644 index 000000000000..909291e450fd --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/TikaTextExtractionFilter.java @@ -0,0 +1,178 @@ +/** + * 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.mediafilter; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import org.apache.commons.lang.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.tika.Tika; +import org.apache.tika.exception.TikaException; +import org.apache.tika.metadata.Metadata; +import org.apache.tika.parser.AutoDetectParser; +import org.apache.tika.sax.BodyContentHandler; +import org.apache.tika.sax.ContentHandlerDecorator; +import org.dspace.content.Item; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; +import org.xml.sax.SAXException; + +/** + * Text Extraction media filter which uses Apache Tika to extract text from a large number of file formats (including + * all Microsoft formats, PDF, HTML, Text, etc). For a more complete list of file formats supported by Tika see the + * Tika documentation: https://tika.apache.org/2.3.0/formats.html + */ +public class TikaTextExtractionFilter + extends MediaFilter { + private final static Logger log = LogManager.getLogger(); + + @Override + public String getFilteredName(String oldFilename) { + return oldFilename + ".txt"; + } + + @Override + public String getBundleName() { + return "TEXT"; + } + + @Override + public String getFormatString() { + return "Text"; + } + + @Override + public String getDescription() { + return "Extracted text"; + } + + @Override + public InputStream getDestinationStream(Item currentItem, InputStream source, boolean verbose) + throws Exception { + ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + boolean useTemporaryFile = configurationService.getBooleanProperty("textextractor.use-temp-file", false); + + if (useTemporaryFile) { + // Extract text out of source file using a temp file, returning results as InputStream + return extractUsingTempFile(source, verbose); + } + + // Not using temporary file. We'll use Tika's default in-memory parsing. + // Get maximum characters to extract. Default is 100,000 chars, which is also Tika's default setting. + String extractedText; + int maxChars = configurationService.getIntProperty("textextractor.max-chars", 100000); + try { + // Use Tika to extract text from input. Tika will automatically detect the file type. + Tika tika = new Tika(); + tika.setMaxStringLength(maxChars); // Tell Tika the maximum number of characters to extract + extractedText = tika.parseToString(source); + } catch (IOException e) { + System.err.println("Unable to extract text from bitstream in Item " + currentItem.getID().toString()); + e.printStackTrace(); + log.error("Unable to extract text from bitstream in Item {}", currentItem.getID().toString(), e); + throw e; + } catch (OutOfMemoryError oe) { + System.err.println("OutOfMemoryError occurred when extracting text from bitstream in Item " + + currentItem.getID().toString() + + "You may wish to enable 'textextractor.use-temp-file'."); + oe.printStackTrace(); + log.error("OutOfMemoryError occurred when extracting text from bitstream in Item {}. " + + "You may wish to enable 'textextractor.use-temp-file'.", currentItem.getID().toString(), oe); + throw oe; + } + + if (StringUtils.isNotEmpty(extractedText)) { + // if verbose flag is set, print out extracted text to STDOUT + if (verbose) { + System.out.println("(Verbose mode) Extracted text:"); + System.out.println(extractedText); + } + + // return the extracted text as a UTF-8 stream. + return new ByteArrayInputStream(extractedText.getBytes(StandardCharsets.UTF_8)); + } + return null; + } + + /** + * Extracts the text out of a given source InputStream, using a temporary file. This decreases the amount of memory + * necessary for text extraction, but can be slower as it requires writing extracted text to a temporary file. + * @param source source InputStream + * @param verbose verbose mode enabled/disabled + * @return InputStream for temporary file containing extracted text + * @throws IOException + * @throws SAXException + * @throws TikaException + */ + private InputStream extractUsingTempFile(InputStream source, boolean verbose) + throws IOException, TikaException, SAXException { + File tempExtractedTextFile = File.createTempFile("dspacetextextract" + source.hashCode(), ".txt"); + + if (verbose) { + System.out.println("(Verbose mode) Extracted text was written to temporary file at " + + tempExtractedTextFile.getAbsolutePath()); + } else { + tempExtractedTextFile.deleteOnExit(); + } + + // Open temp file for writing + try (FileWriter writer = new FileWriter(tempExtractedTextFile, StandardCharsets.UTF_8)) { + // Initialize a custom ContentHandlerDecorator which is a BodyContentHandler. + // This mimics the behavior of Tika().parseToString(), which only extracts text from the body of the file. + // This custom Handler writes any extracted text to the temp file. + ContentHandlerDecorator handler = new BodyContentHandler(new ContentHandlerDecorator() { + /** + * Write all extracted characters directly to the temp file. + */ + @Override + public void characters(char[] ch, int start, int length) { + try { + writer.append(new String(ch), start, length); + } catch (IOException e) { + log.error("Could not append to temporary file at {} when performing text extraction", + tempExtractedTextFile.getAbsolutePath(), e); + } + } + + /** + * Write all ignorable whitespace directly to the temp file. + * This mimics the behaviour of Tika().parseToString() which extracts ignorableWhitespace characters + * (like blank lines, indentations, etc.), so that we get the same extracted text either way. + */ + @Override + public void ignorableWhitespace(char[] ch, int start, int length) { + try { + writer.append(new String(ch), start, length); + } catch (IOException e) { + log.error("Could not append to temporary file at {} when performing text extraction", + tempExtractedTextFile.getAbsolutePath(), e); + } + } + }); + + AutoDetectParser parser = new AutoDetectParser(); + Metadata metadata = new Metadata(); + // parse our source InputStream using the above custom handler + parser.parse(source, handler, metadata); + } + + // At this point, all extracted text is written to our temp file. So, return a FileInputStream for that file + return new FileInputStream(tempExtractedTextFile); + } + + + + +} diff --git a/dspace-api/src/main/java/org/dspace/discovery/indexobject/IndexFactoryImpl.java b/dspace-api/src/main/java/org/dspace/discovery/indexobject/IndexFactoryImpl.java index 8660bbebc796..3c93e1c52277 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/indexobject/IndexFactoryImpl.java +++ b/dspace-api/src/main/java/org/dspace/discovery/indexobject/IndexFactoryImpl.java @@ -95,7 +95,6 @@ protected void writeDocument(SolrInputDocument doc, FullTextContentStreams strea 100000); // Use Tika's Text parser as the streams are always from the TEXT bundle (i.e. already extracted text) - // TODO: We may wish to consider using Tika to extract the text in the future. TextAndCSVParser tikaParser = new TextAndCSVParser(); BodyContentHandler tikaHandler = new BodyContentHandler(charLimit); Metadata tikaMetadata = new Metadata(); diff --git a/dspace-api/src/test/java/org/dspace/app/mediafilter/PoiWordFilterTest.java b/dspace-api/src/test/java/org/dspace/app/mediafilter/PoiWordFilterTest.java deleted file mode 100644 index 4d2353a29ab0..000000000000 --- a/dspace-api/src/test/java/org/dspace/app/mediafilter/PoiWordFilterTest.java +++ /dev/null @@ -1,181 +0,0 @@ -/** - * 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.mediafilter; - -import static org.junit.Assert.assertTrue; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; - -import org.dspace.content.Item; -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; - -/** - * Drive the POI-based MS Word filter. - * - * @author mwood - */ -public class PoiWordFilterTest { - - public PoiWordFilterTest() { - } - - @BeforeClass - public static void setUpClass() { - } - - @AfterClass - public static void tearDownClass() { - } - - @Before - public void setUp() { - } - - @After - public void tearDown() { - } - - /** - * Test of getFilteredName method, of class PoiWordFilter. - */ -/* - @Test - public void testGetFilteredName() - { - System.out.println("getFilteredName"); - String oldFilename = ""; - PoiWordFilter instance = new PoiWordFilter(); - String expResult = ""; - String result = instance.getFilteredName(oldFilename); - assertEquals(expResult, result); - // TODO review the generated test code and remove the default call to fail. - fail("The test case is a prototype."); - } -*/ - - /** - * Test of getBundleName method, of class PoiWordFilter. - */ -/* - @Test - public void testGetBundleName() - { - System.out.println("getBundleName"); - PoiWordFilter instance = new PoiWordFilter(); - String expResult = ""; - String result = instance.getBundleName(); - assertEquals(expResult, result); - // TODO review the generated test code and remove the default call to fail. - fail("The test case is a prototype."); - } -*/ - - /** - * Test of getFormatString method, of class PoiWordFilter. - */ -/* - @Test - public void testGetFormatString() - { - System.out.println("getFormatString"); - PoiWordFilter instance = new PoiWordFilter(); - String expResult = ""; - String result = instance.getFormatString(); - assertEquals(expResult, result); - // TODO review the generated test code and remove the default call to fail. - fail("The test case is a prototype."); - } -*/ - - /** - * Test of getDescription method, of class PoiWordFilter. - */ -/* - @Test - public void testGetDescription() - { - System.out.println("getDescription"); - PoiWordFilter instance = new PoiWordFilter(); - String expResult = ""; - String result = instance.getDescription(); - assertEquals(expResult, result); - // TODO review the generated test code and remove the default call to fail. - fail("The test case is a prototype."); - } -*/ - - /** - * Test of getDestinationStream method, of class PoiWordFilter. - * Read a constant .doc document and examine the extracted text. - * - * @throws java.lang.Exception passed through. - */ - @Test - public void testGetDestinationStreamDoc() - throws Exception { - System.out.println("getDestinationStream"); - Item currentItem = null; - InputStream source; - boolean verbose = false; - PoiWordFilter instance = new PoiWordFilter(); - InputStream result; - - source = getClass().getResourceAsStream("wordtest.doc"); - result = instance.getDestinationStream(currentItem, source, verbose); - assertTrue("Known content was not found", readAll(result).contains("quick brown fox")); - } - - /** - * Test of getDestinationStream method, of class PoiWordFilter. - * Read a constant .docx document and examine the extracted text. - * - * @throws java.lang.Exception passed through. - */ - @Test - public void testGetDestinationStreamDocx() - throws Exception { - System.out.println("getDestinationStream"); - Item currentItem = null; - InputStream source; - boolean verbose = false; - PoiWordFilter instance = new PoiWordFilter(); - InputStream result; - - source = getClass().getResourceAsStream("wordtest.docx"); - result = instance.getDestinationStream(currentItem, source, verbose); - assertTrue("Known content was not found", readAll(result).contains("quick brown fox")); - } - - /** - * Read the entire content of a stream into a String. - * - * @param stream a stream of UTF-8 characters. - * @return complete content of {@link stream} - * @throws IOException - */ - private static String readAll(InputStream stream) - throws IOException { - if (null == stream) { - return null; - } - - byte[] bytes = new byte[stream.available()]; - StringBuilder resultSb = new StringBuilder(bytes.length / 2); // Guess: average 2 bytes per character - int howmany; - while ((howmany = stream.read(bytes)) > 0) { - resultSb.append(new String(bytes, 0, howmany, StandardCharsets.UTF_8)); - } - return resultSb.toString(); - } -} diff --git a/dspace-api/src/test/java/org/dspace/app/mediafilter/TikaTextExtractionFilterTest.java b/dspace-api/src/test/java/org/dspace/app/mediafilter/TikaTextExtractionFilterTest.java new file mode 100644 index 000000000000..5ebc85c9562a --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/app/mediafilter/TikaTextExtractionFilterTest.java @@ -0,0 +1,177 @@ +/** + * 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.mediafilter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import org.apache.commons.io.IOUtils; +import org.dspace.AbstractUnitTest; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; +import org.junit.Test; + +/** + * Drive the POI-based MS Word filter. + * + * @author mwood + */ +public class TikaTextExtractionFilterTest extends AbstractUnitTest { + + private ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + + /** + * Test of getDestinationStream method using temp file for text extraction + * + * @throws java.lang.Exception passed through. + */ + @Test + public void testGetDestinationStreamWithUseTempFile() + throws Exception { + TikaTextExtractionFilter instance = new TikaTextExtractionFilter(); + + // Extract text from file with "use-temp-file=true" + configurationService.setProperty("textextractor.use-temp-file", "true"); + InputStream source = getClass().getResourceAsStream("test.pdf"); + InputStream result = instance.getDestinationStream(null, source, false); + String tempFileExtractedText = readAll(result); + + // Verify text extracted successfully + assertTrue("Known content was not found in .pdf", tempFileExtractedText.contains("quick brown fox")); + + // Now, extract text from same file using default, in-memory + configurationService.setProperty("textextractor.use-temp-file", "false"); + source = getClass().getResourceAsStream("test.pdf"); + result = instance.getDestinationStream(null, source, false); + String inMemoryExtractedText = readAll(result); + + // Verify the two results are equal + assertEquals("Extracted text via temp file is the same as in-memory.", + inMemoryExtractedText, tempFileExtractedText); + } + + /** + * Test of getDestinationStream method when max characters is less than file size + * + * @throws java.lang.Exception passed through. + */ + @Test + public void testGetDestinationStreamWithMaxChars() + throws Exception { + TikaTextExtractionFilter instance = new TikaTextExtractionFilter(); + + // Set "max-chars" to a small value of 100 chars, which is less than the text size of the file. + configurationService.setProperty("textextractor.max-chars", "100"); + InputStream source = getClass().getResourceAsStream("test.pdf"); + InputStream result = instance.getDestinationStream(null, source, false); + String extractedText = readAll(result); + + // Verify we have exactly the first 100 characters + assertEquals(100, extractedText.length()); + // Verify it has some text at the beginning of the file, but NOT text near the end + assertTrue("Known beginning content was found", extractedText.contains("This is a text.")); + assertFalse("Known ending content was not found", extractedText.contains("Emergency Broadcast System")); + } + + /** + * Test of getDestinationStream method using older Microsoft Word document. + * Read a constant .doc document and examine the extracted text. + * + * @throws java.lang.Exception passed through. + */ + @Test + public void testGetDestinationStreamWithDoc() + throws Exception { + TikaTextExtractionFilter instance = new TikaTextExtractionFilter(); + + InputStream source = getClass().getResourceAsStream("wordtest.doc"); + InputStream result = instance.getDestinationStream(null, source, false); + assertTrue("Known content was not found in .doc", readAll(result).contains("quick brown fox")); + } + + /** + * Test of getDestinationStream method using newer Microsoft Word document. + * Read a constant .docx document and examine the extracted text. + * + * @throws java.lang.Exception passed through. + */ + @Test + public void testGetDestinationStreamWithDocx() + throws Exception { + TikaTextExtractionFilter instance = new TikaTextExtractionFilter(); + + InputStream source = getClass().getResourceAsStream("wordtest.docx"); + InputStream result = instance.getDestinationStream(null, source, false); + assertTrue("Known content was not found in .docx", readAll(result).contains("quick brown fox")); + } + + /** + * Test of getDestinationStream method using a PDF document + * Read a constant .pdf document and examine the extracted text. + * + * @throws java.lang.Exception passed through. + */ + @Test + public void testGetDestinationStreamWithPDF() + throws Exception { + TikaTextExtractionFilter instance = new TikaTextExtractionFilter(); + + InputStream source = getClass().getResourceAsStream("test.pdf"); + InputStream result = instance.getDestinationStream(null, source, false); + assertTrue("Known content was not found in .pdf", readAll(result).contains("quick brown fox")); + } + + /** + * Test of getDestinationStream method using an HTML document + * Read a constant .html document and examine the extracted text. + * + * @throws java.lang.Exception passed through. + */ + @Test + public void testGetDestinationStreamWithHTML() + throws Exception { + TikaTextExtractionFilter instance = new TikaTextExtractionFilter(); + + InputStream source = getClass().getResourceAsStream("test.html"); + InputStream result = instance.getDestinationStream(null, source, false); + assertTrue("Known content was not found in .html", readAll(result).contains("quick brown fox")); + } + + /** + * Test of getDestinationStream method using a TXT document + * Read a constant .txt document and examine the extracted text. + * + * @throws java.lang.Exception passed through. + */ + @Test + public void testGetDestinationStreamWithTxt() + throws Exception { + TikaTextExtractionFilter instance = new TikaTextExtractionFilter(); + + InputStream source = getClass().getResourceAsStream("test.txt"); + InputStream result = instance.getDestinationStream(null, source, false); + assertTrue("Known content was not found in .txt", readAll(result).contains("quick brown fox")); + } + + /** + * Read the entire content of a stream into a String. + * + * @param stream a stream of UTF-8 characters. + * @return complete content of stream as a String + * @throws IOException + */ + private static String readAll(InputStream stream) + throws IOException { + return IOUtils.toString(stream, StandardCharsets.UTF_8); + } +} diff --git a/dspace-api/src/test/resources/org/dspace/app/mediafilter/test.html b/dspace-api/src/test/resources/org/dspace/app/mediafilter/test.html new file mode 100644 index 000000000000..7655f566cc35 --- /dev/null +++ b/dspace-api/src/test/resources/org/dspace/app/mediafilter/test.html @@ -0,0 +1,53 @@ + + + + +A Text Extraction Test Document for DSpace + + + + +
+ +

A Text Extraction Test Document

+ +

for

+ +

DSpace

+ +

+ +

This is a text. For the next sixty seconds this software +will conduct a test of the DSpace text extraction facility. This is only a +text.

+ +

This is a paragraph that followed the first that lived in +the document that Jack built.

+ +

Lorem ipsum dolor sit amet. The quick brown fox jumped over +the lazy dog. Yow! Are we having fun yet?

+ +

This has been a test of the DSpace text extraction system. +In the event of actual content you would care what is written here.

+ +
+ +
+ +
+ +
+ +

Tip o’ the hat to the U.S. Emergency Broadcast System for the format that I have +irreverently borrowed.

+ +
+ + + + + + diff --git a/dspace-api/src/test/resources/org/dspace/app/mediafilter/test.pdf b/dspace-api/src/test/resources/org/dspace/app/mediafilter/test.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5b3749cbff73a41baf8aa06b7f62339886bc6ca2 GIT binary patch literal 56812 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%0AGv`t_CiE@W~}d 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.4.44.v20210927 2.17.1 2.0.24 - 5.2.2 1.18.0 1.7.25 2.3.0 @@ -1552,26 +1551,6 @@ fontbox ${pdfbox-version}
- - org.apache.poi - poi - ${poi-version} - - - org.apache.poi - poi-scratchpad - ${poi-version} - - - org.apache.poi - poi-ooxml - ${poi-version} - - - org.apache.poi - poi-ooxml-full - ${poi-version} - xalan xalan From b86adf35e233940319f2fbe44b6833a3f0016e08 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 30 Mar 2022 10:55:43 -0500 Subject: [PATCH 0120/1846] Remove duplicative dependency. This was in POM twice. --- dspace-api/pom.xml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 2c998572f47b..dd8689043502 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -872,13 +872,6 @@ 2.0.0 - - com.github.stefanbirkner - system-rules - 1.19.0 - test - - org.mock-server mockserver-junit-rule From e9e4cdbaac72da5086ccc593d656b38751224c92 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 30 Mar 2022 11:20:23 -0500 Subject: [PATCH 0121/1846] Fix dependencies in overlay modules so that example ITs pass --- dspace/modules/additions/pom.xml | 68 ++++++++++++++++++++++---------- dspace/modules/server/pom.xml | 33 ++++++++++++++++ 2 files changed, 81 insertions(+), 20 deletions(-) diff --git a/dspace/modules/additions/pom.xml b/dspace/modules/additions/pom.xml index 2abc12b5292c..569ca8ce25f3 100644 --- a/dspace/modules/additions/pom.xml +++ b/dspace/modules/additions/pom.xml @@ -254,26 +254,54 @@ - - org.apache.lucene - lucene-core - - - org.apache.lucene - lucene-analyzers-icu - test - - - org.apache.lucene - lucene-analyzers-smartcn - test - - - org.apache.lucene - lucene-analyzers-stempel - test - - + + org.apache.solr + solr-solrj + ${solr.client.version} + + + + + org.apache.solr + solr-core + ${solr.client.version} + test + + + + org.apache.commons + commons-text + + + + org.eclipse.jetty + jetty-http + + + org.eclipse.jetty + jetty-io + + + org.eclipse.jetty + jetty-util + + + + + org.apache.lucene + lucene-analyzers-icu + test + + + org.apache.lucene + lucene-analyzers-smartcn + test + + + org.apache.lucene + lucene-analyzers-stempel + test + junit junit diff --git a/dspace/modules/server/pom.xml b/dspace/modules/server/pom.xml index ad47074392fa..dd854c4215ba 100644 --- a/dspace/modules/server/pom.xml +++ b/dspace/modules/server/pom.xml @@ -278,6 +278,11 @@ just adding new jar in the classloader dspace-server-webapp war + + org.apache.solr + solr-solrj + ${solr.client.version} + @@ -324,6 +329,34 @@ just adding new jar in the classloader mockito-inline test + + + + org.apache.solr + solr-core + ${solr.client.version} + test + + + + org.apache.commons + commons-text + + + + org.eclipse.jetty + jetty-http + + + org.eclipse.jetty + jetty-io + + + org.eclipse.jetty + jetty-util + + + org.apache.lucene lucene-analyzers-icu From f02d8ab04a9fa328306767de9db8e27382437ed1 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 30 Mar 2022 13:52:44 -0500 Subject: [PATCH 0122/1846] Add test files for major text-based formats & test to verify all are indexable. Also add CSV to list of formats & include it. --- .../TikaTextExtractionFilterTest.java | 152 ++++++++++- .../org/dspace/app/mediafilter/test.csv | 4 + .../mediafilter/{wordtest.doc => test.doc} | Bin .../mediafilter/{wordtest.docx => test.docx} | Bin .../org/dspace/app/mediafilter/test.odp | Bin 0 -> 25579 bytes .../org/dspace/app/mediafilter/test.ods | Bin 0 -> 2886 bytes .../org/dspace/app/mediafilter/test.odt | Bin 0 -> 5653 bytes .../org/dspace/app/mediafilter/test.ppt | Bin 0 -> 96256 bytes .../org/dspace/app/mediafilter/test.pptx | Bin 0 -> 37911 bytes .../org/dspace/app/mediafilter/test.rtf | 239 ++++++++++++++++++ .../org/dspace/app/mediafilter/test.xls | Bin 0 -> 23552 bytes .../org/dspace/app/mediafilter/test.xlsx | Bin 0 -> 8466 bytes dspace/config/dspace.cfg | 15 +- .../config/registries/bitstream-formats.xml | 9 + 14 files changed, 411 insertions(+), 8 deletions(-) create mode 100644 dspace-api/src/test/resources/org/dspace/app/mediafilter/test.csv rename dspace-api/src/test/resources/org/dspace/app/mediafilter/{wordtest.doc => test.doc} (100%) rename dspace-api/src/test/resources/org/dspace/app/mediafilter/{wordtest.docx => test.docx} (100%) create mode 100644 dspace-api/src/test/resources/org/dspace/app/mediafilter/test.odp create mode 100644 dspace-api/src/test/resources/org/dspace/app/mediafilter/test.ods create mode 100644 dspace-api/src/test/resources/org/dspace/app/mediafilter/test.odt create mode 100644 dspace-api/src/test/resources/org/dspace/app/mediafilter/test.ppt create mode 100644 dspace-api/src/test/resources/org/dspace/app/mediafilter/test.pptx create mode 100644 dspace-api/src/test/resources/org/dspace/app/mediafilter/test.rtf create mode 100644 dspace-api/src/test/resources/org/dspace/app/mediafilter/test.xls create mode 100644 dspace-api/src/test/resources/org/dspace/app/mediafilter/test.xlsx diff --git a/dspace-api/src/test/java/org/dspace/app/mediafilter/TikaTextExtractionFilterTest.java b/dspace-api/src/test/java/org/dspace/app/mediafilter/TikaTextExtractionFilterTest.java index 5ebc85c9562a..b7d0ed5a3bee 100644 --- a/dspace-api/src/test/java/org/dspace/app/mediafilter/TikaTextExtractionFilterTest.java +++ b/dspace-api/src/test/java/org/dspace/app/mediafilter/TikaTextExtractionFilterTest.java @@ -22,9 +22,11 @@ import org.junit.Test; /** - * Drive the POI-based MS Word filter. + * Test the TikaTextExtractionFilter using test files for all major formats. + * The test files used below are all located at [dspace-api]/src/main/resources/org/dspace/app/mediafilter/ * * @author mwood + * @author Tim Donohue */ public class TikaTextExtractionFilterTest extends AbstractUnitTest { @@ -94,7 +96,7 @@ public void testGetDestinationStreamWithDoc() throws Exception { TikaTextExtractionFilter instance = new TikaTextExtractionFilter(); - InputStream source = getClass().getResourceAsStream("wordtest.doc"); + InputStream source = getClass().getResourceAsStream("test.doc"); InputStream result = instance.getDestinationStream(null, source, false); assertTrue("Known content was not found in .doc", readAll(result).contains("quick brown fox")); } @@ -110,11 +112,43 @@ public void testGetDestinationStreamWithDocx() throws Exception { TikaTextExtractionFilter instance = new TikaTextExtractionFilter(); - InputStream source = getClass().getResourceAsStream("wordtest.docx"); + InputStream source = getClass().getResourceAsStream("test.docx"); InputStream result = instance.getDestinationStream(null, source, false); assertTrue("Known content was not found in .docx", readAll(result).contains("quick brown fox")); } + /** + * Test of getDestinationStream method using an ODT document + * Read a constant .odt document and examine the extracted text. + * + * @throws java.lang.Exception passed through. + */ + @Test + public void testGetDestinationStreamWithODT() + throws Exception { + TikaTextExtractionFilter instance = new TikaTextExtractionFilter(); + + InputStream source = getClass().getResourceAsStream("test.odt"); + InputStream result = instance.getDestinationStream(null, source, false); + assertTrue("Known content was not found in .odt", readAll(result).contains("quick brown fox")); + } + + /** + * Test of getDestinationStream method using an RTF document + * Read a constant .rtf document and examine the extracted text. + * + * @throws java.lang.Exception passed through. + */ + @Test + public void testGetDestinationStreamWithRTF() + throws Exception { + TikaTextExtractionFilter instance = new TikaTextExtractionFilter(); + + InputStream source = getClass().getResourceAsStream("test.rtf"); + InputStream result = instance.getDestinationStream(null, source, false); + assertTrue("Known content was not found in .rtf", readAll(result).contains("quick brown fox")); + } + /** * Test of getDestinationStream method using a PDF document * Read a constant .pdf document and examine the extracted text. @@ -163,6 +197,118 @@ public void testGetDestinationStreamWithTxt() assertTrue("Known content was not found in .txt", readAll(result).contains("quick brown fox")); } + /** + * Test of getDestinationStream method using a CSV document + * Read a constant .csv document and examine the extracted text. + * + * @throws java.lang.Exception passed through. + */ + @Test + public void testGetDestinationStreamWithCsv() + throws Exception { + TikaTextExtractionFilter instance = new TikaTextExtractionFilter(); + + InputStream source = getClass().getResourceAsStream("test.csv"); + InputStream result = instance.getDestinationStream(null, source, false); + assertTrue("Known content was not found in .csv", readAll(result).contains("data3,3")); + } + + /** + * Test of getDestinationStream method using an XLS document + * Read a constant .xls document and examine the extracted text. + * + * @throws java.lang.Exception passed through. + */ + @Test + public void testGetDestinationStreamWithXLS() + throws Exception { + TikaTextExtractionFilter instance = new TikaTextExtractionFilter(); + + InputStream source = getClass().getResourceAsStream("test.xls"); + InputStream result = instance.getDestinationStream(null, source, false); + assertTrue("Known content was not found in .xls", readAll(result).contains("data3,3")); + } + + /** + * Test of getDestinationStream method using an XLSX document + * Read a constant .xlsx document and examine the extracted text. + * + * @throws java.lang.Exception passed through. + */ + @Test + public void testGetDestinationStreamWithXLSX() + throws Exception { + TikaTextExtractionFilter instance = new TikaTextExtractionFilter(); + + InputStream source = getClass().getResourceAsStream("test.xlsx"); + InputStream result = instance.getDestinationStream(null, source, false); + assertTrue("Known content was not found in .xlsx", readAll(result).contains("data3,3")); + } + + /** + * Test of getDestinationStream method using an ODS document + * Read a constant .ods document and examine the extracted text. + * + * @throws java.lang.Exception passed through. + */ + @Test + public void testGetDestinationStreamWithODS() + throws Exception { + TikaTextExtractionFilter instance = new TikaTextExtractionFilter(); + + InputStream source = getClass().getResourceAsStream("test.ods"); + InputStream result = instance.getDestinationStream(null, source, false); + assertTrue("Known content was not found in .ods", readAll(result).contains("Data on the second sheet")); + } + + /** + * Test of getDestinationStream method using an PPT document + * Read a constant .ppt document and examine the extracted text. + * + * @throws java.lang.Exception passed through. + */ + @Test + public void testGetDestinationStreamWithPPT() + throws Exception { + TikaTextExtractionFilter instance = new TikaTextExtractionFilter(); + + InputStream source = getClass().getResourceAsStream("test.ppt"); + InputStream result = instance.getDestinationStream(null, source, false); + assertTrue("Known content was not found in .ppt", readAll(result).contains("quick brown fox")); + } + + /** + * Test of getDestinationStream method using an PPTX document + * Read a constant .pptx document and examine the extracted text. + * + * @throws java.lang.Exception passed through. + */ + @Test + public void testGetDestinationStreamWithPPTX() + throws Exception { + TikaTextExtractionFilter instance = new TikaTextExtractionFilter(); + + InputStream source = getClass().getResourceAsStream("test.pptx"); + InputStream result = instance.getDestinationStream(null, source, false); + assertTrue("Known content was not found in .pptx", readAll(result).contains("quick brown fox")); + } + + /** + * Test of getDestinationStream method using an ODP document + * Read a constant .odp document and examine the extracted text. + * + * @throws java.lang.Exception passed through. + */ + @Test + public void testGetDestinationStreamWithODP() + throws Exception { + TikaTextExtractionFilter instance = new TikaTextExtractionFilter(); + + InputStream source = getClass().getResourceAsStream("test.odp"); + InputStream result = instance.getDestinationStream(null, source, false); + assertTrue("Known content was not found in .odp", readAll(result).contains("quick brown fox")); + } + /** * Read the entire content of a stream into a String. * diff --git a/dspace-api/src/test/resources/org/dspace/app/mediafilter/test.csv b/dspace-api/src/test/resources/org/dspace/app/mediafilter/test.csv new file mode 100644 index 000000000000..07c22ff0bfb9 --- /dev/null +++ b/dspace-api/src/test/resources/org/dspace/app/mediafilter/test.csv @@ -0,0 +1,4 @@ +row1,row2,row3,row4 +"data1,2","data 2,2","data3,2","data4,2" +"data1,3","data 2,3","data3,3","data4,3" +"data1,4","data2,4","data3,4","data4,4" diff --git a/dspace-api/src/test/resources/org/dspace/app/mediafilter/wordtest.doc b/dspace-api/src/test/resources/org/dspace/app/mediafilter/test.doc similarity index 100% rename from dspace-api/src/test/resources/org/dspace/app/mediafilter/wordtest.doc rename to dspace-api/src/test/resources/org/dspace/app/mediafilter/test.doc diff --git a/dspace-api/src/test/resources/org/dspace/app/mediafilter/wordtest.docx b/dspace-api/src/test/resources/org/dspace/app/mediafilter/test.docx similarity index 100% rename from dspace-api/src/test/resources/org/dspace/app/mediafilter/wordtest.docx rename to dspace-api/src/test/resources/org/dspace/app/mediafilter/test.docx diff --git a/dspace-api/src/test/resources/org/dspace/app/mediafilter/test.odp b/dspace-api/src/test/resources/org/dspace/app/mediafilter/test.odp new file mode 100644 index 0000000000000000000000000000000000000000..4701884a8a62eb650dadce702c25c74b2e1e7448 GIT binary patch literal 25579 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 QaKgfv(u{)S`(d8{0^$hQa{vGU literal 0 HcmV?d00001 diff --git a/dspace-api/src/test/resources/org/dspace/app/mediafilter/test.odt b/dspace-api/src/test/resources/org/dspace/app/mediafilter/test.odt new file mode 100644 index 0000000000000000000000000000000000000000..3c996a1f46c41bfbc1b8e7da638a57458072b45e GIT binary patch literal 5653 zcmZ{o1yEdDwuT!IPH@-Y!D%D}2=3arTVqYrxDyC{(I6KH5+F1oL4pK#5AG1$tpkli zAOwOv?##Tp^D^^x)vj&zuc~v_Uc3JKbTu$A@c{p9EC8!MVFJ#F#b5o=Lx;LR!9IZ= zU|SClHy3+b9~YRrfS)y?N zNgCJ}+c;j3fO_gChYYev)g&!2z^t($C8>CMZIA)iswZh)Uo;Lc2(NAe$`Ptq$x?#} z{v|AHcmu;{w=1&^<&<%10*OXVCh)k&YPz=aIPp?YIxm96KqA`AEhDto5m(wx__4Qk zfj!tJC9=2VIE;2KQJA?^Z4jWg?rgX!Do4P-z+#YX%~MyT-g$tfFHvui0d9I(S){KE zp5~&Mn6XnDH07e1t!|{WbMi$$9&T#wzeS$UW~AT<`tCzEpwX`v-!9aa6Ze)fRt4f; zGZ!?)&HTL0x8`hO%oqkL&FHTD`e=CXF**D_pi#*LDyDPJ0N*{k^Xn}g(CDv?8Ex@a zX#u@|BYlB)8q7cDkouL}2?Q2HMWDC7bx8X9RFabkM{&Zks)*w}9tJ!r8PN})4?ALa zG054e$n)T-2g={Opz%RYgm~%LUwJd+)5aOnS4sL)GXNThs0HqlwKs+jiK3lh%@|3c z3ZQd5;9}^Cx|Cc$*Qc?aH0hrZc|_PlzV7ca0RK0mW5SUF3lAp_=l}r4!>FZXAjhYs ztt*KR?AsAG5={8akFkFHP~794Si_D8uJwz*icepUlQM74zh zo8g>eaPwOBL-(mg^|OtUPbqWb+iJWYb?x(sE7sPmqN&PF!da2(s2_1j1}|TVESK+ttHZ3S{h>t7+2?u zd;5(xnt}t1%}llFAi(Zg6=&SGdFRatFv4l_;-XkrtvP15J?sy6#TMy%_)xVYGFU^oleWjWa&4^wK=gr9YKbBo~zkAfoB`vH>UlOR3=67pPkzW zxzPCH4Vpip8oK+Dxunq+8mlyf6w>ey#6OnA3nUrS02~R_!4sM1;W?N6we}*0lasEF zji5xARk%G@Ce4cqQonbjcDppVI=2m2_6ZyGOuN(WQOK z0f{a{$2)wktgqgsYm4(E*ejeYg_yI}BF{Q5i6NyQJc4w78W=*`82)3Q~t-4+eD3_EaQD;%CIY4*r;^zYRgn1gOducj1If< zsy?fveA{UK(i7ekRw?AqvH_FyP;1B2XF_7AzkJF|C>+Du3LfI_I~)POg+{b>N!EKD5b`Osi(q+VvnEoh^H3Y$Y_kQ$T;E)1TS@LbWq$Bt(jP9q#!lm97qnou!tjf zC?(d&sX3zn`zNA#>AF|q!w^B;`X9u#1*($t)YyOtmX#TubOm2BS5%6tUmnAF#7L$W ztrEE4d$K_(S1en)$ncxQz7L+{qKfQ$!ZaZQ1B}ZmlR=IA<&UR62k}Jnz1!SuOC8mw)@ofuIo~$Js&iyEE_wmu<=jUgu#P;h3I{o;`XN?6kMSQuuI&+#N z6kF*Zwc{KoVAyj+Ti}1DN1;&|XQRU$%^{XgS8M?!Bm%OSl>>QN*wM_z*aCkMGm zY3BpJ9K}JcczJ73FSIfOW&2`|Y2X^Gsk~?C1nX)SOq|DGl-HtUA;pG5v0GFeiIpv+c#;MD}lOZqxc>{;;zX}_GeY1Ix)o&$;F zrIwq*d6~VwH)wkIELJ)4XzN7LE@?-iqgDpQ=GcWVoTApy*lkGo!pt+YtkkuN06A*s6Ua)b4mFl(G}glH1G?1%K(( zK$?zMQzA@Hkvg=iFZ7GsLiikZE(-xx4%c@?tHx18?NEnjE;`jNN!4yrPuz%T#v{_j z;1@J@843i$(_hrA7M;b#R zQEX1VAUR`8Em=8{EbCM$2=YfC7wwXhNyuDOt7~pt1#87kc(R(HiVh8PjOpPFG!_jS zUM59z)V*>(cxBlK>=N+(oDk!0pmHXz|2S2MwG5I48%cMLibg8h)8M6lqP@+Sem)+% zQf#+1(&_v5nI-XzDTm-xH5@H=N`p(0+j5=f698dz~pJLUo);0r-&&*wWl#F@fo5Efo zlIS?vBK>CUJt6J4%NbRCl~UxI!5Gg&mCvNCNUwe(!kXsI{1bdKm{c0rzBk+85$m6( zw;H5y2yHWZ?Q#GGRodQfo|Y`c-S7Z{luku z(z~$mc6c7M?6-YK0p*(^?E3Sog*UGcrf5SY^K z5L`U8YX_?nr`cpmwSem=J7o!8xHoRy_PU~0BV6k32#*iOGIovjgM=98%6U}up?yj+ z+w7bQz^z^9XWH-H;{gS*_t9Z0Np{y8!3&K12@MtBCPw|W-j;*Yhb;+J%7?~W=JW~u zfTw*VVkKNdHf6UjWf#X2njBLHLkLQ{nitB~uZ>r&qzW5C=?aaFGq#eLv+hG%iEb=v z-uExR5?3~^YoRb!@Fz1$#i)9e{X{{j@lD*y9`;o-dlxVEy)H>`lhn08 zdOK~pK4Ab;43+(dF0bNZpBKwS+WXsnX8_EPZn64e2V7B-QF&J@HoT;;)}M*OdAs;F ziZ^aKllM+VSz~P4z??8yxKHN?G^Mkpe?Ty>cm@3uODl zpiiU07U)Pk9^~-38MBb4E{Am^Da7J&Lw4j=wE7fzxY5AIcbSZIOoGmgFE|34RdI1qxRi3AHx_)}YNw=z zA-?$XHOO;z#Fmgq|Jgjvl(b#i$(Iwdd5t7?IK=k+{cuha8Dl#BxBdNjq~pqN zXa@0vK(F;`%5X+2%8XFlX_PSsbruJii!nUBSG2$&R$T+PTPrm~kF-OFE#4ASFU;x+Iob*0Q?A*bm`Fxe!R2BrzmZ>zl@&d*r-UcncyZNk3=Bfj4m*GovL9{ETV|&B>WbA`=dR3iD0jk-bkguK7X~ z&#~p^ENYIOmOJ=4Id8<-yfC3}hUrGP!SJ*Bl)i(5^0{!CJV!Jp`*RvOIEHr~IaWrW z(a9sCt=k2IljwBQ!6R^KO#*2ojysFZJewY$e%uqeZeh!s>MEQnGq*4HdxmJ%u+U1< zBn7t1FFCWEqdvlcP&XjgQ@vV2-Qj1vn1pLS>w~BRS1}bOeE&CSbZN5q{RRs-OSRnz zice{xyrVipYsH1*MX%Fth(axwCz;pdo7+kWD4NZUf~*yvRG@65@;Yk*7Pbw@l{xbX zzdasIU*()P88g6;YKwXy_NUgA>H7&^-z=@ZqM+B+)eDZl+N10b93dgy@J_aR6616S zMP4hs_M@ZsJV^`mgApurS^-=}T&HiE+2?4DjbmmR4G8xr&KIa__Le`>eJ+vcW6P{_ z^Vrc_$!gV>51@UNaGBjRx<_(vyRg)PppJ(QzL0&o%-y6{+*EyW*s%Yq>n+NLpUJE5 zE+jg_CW(aJJU^?}4G!`=$LvhovYWtz46w@YH)h}+%^G_<3`smL9P)XuD)R!06;QskYWP^OR5Y122- zxx}W4wYY~bjk9+&v}q8fdl@`y>N8=*d4n8-^yl;IgBTQ;k+c^~PdOp7 z=ybeF{$Z?4;m(lqr!{wY+c12jsJX>!{OIwZs*?Cx9xyM_Cg|l$ZIQ88l__Q#t^L}{ zsl!(d{`iiwQPa8?o^(0Vest%g3g2pitqs&7PIInWgyg1s=4X>Tx}Z9Ny>DfzPiA^Y zOgonLMkn?nPo&27sZ2L$4{gHc``+x(%goO(-M4IpjtjP{D;AeKS{5-pM!**>eHguA ztdnw-E4bSeDF3s8Z$skW^aebHTPxD_~F^o>XQ<($? zN1um@{7M?c3F~@UP`ya!kh>La6wUH}JNKh<%~iIDG0@pOYK-aUfbb|sF7m@7@vG7I zD|`6oo!VcU6U;5zIg`?kt?Xh3KCuwTsX%aj=m@u|e?3}iKE{0Y=Qno9Nmiv_Ic@F& zm)>aYyOru{CDIMHT0Ux3YXYUr8=E|r=qU~{_tSXiyVPp?)9UO4LF6>*?h_9Vvkoh= z9=2B;O2GW=9>q0|O+7}U-z&>W>hm*y;~fKEuS|o{Yew0L@QH?C!^VtJob5Q(oVoS1 z{=?J?%Iys-!?CmGHK7!ArXnhc-X=qNask)ZCuNtoKW7#edY{UAldDw-(PF%1P$Cpx zjX@G_cB$nhY5EOz!1G7njtNw7Az1V2h^d!He<0S^ou(epmgD4*pgdJ^+P(0|~zie|N2a3&|c3 d#NQrPR|D&T&H^5oEjGaHfz=i~FbV+Re*ixBRN(*s literal 0 HcmV?d00001 diff --git a/dspace-api/src/test/resources/org/dspace/app/mediafilter/test.ppt b/dspace-api/src/test/resources/org/dspace/app/mediafilter/test.ppt new file mode 100644 index 0000000000000000000000000000000000000000..bb3a3d6b41e2b2123eddf7183c1906369a89ed7c GIT binary patch literal 96256 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 literal 0 HcmV?d00001 diff --git a/dspace-api/src/test/resources/org/dspace/app/mediafilter/test.pptx b/dspace-api/src/test/resources/org/dspace/app/mediafilter/test.pptx new file mode 100644 index 0000000000000000000000000000000000000000..2c27ad1630b914d4f731dcbc905b83e21c583755 GIT binary patch literal 37911 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 ui`P$m@OP!uLlnGHY#`~m>iJRpJJfBPSmtfpE3 literal 0 HcmV?d00001 diff --git a/dspace-api/src/test/resources/org/dspace/app/mediafilter/test.rtf b/dspace-api/src/test/resources/org/dspace/app/mediafilter/test.rtf new file mode 100644 index 000000000000..3b841917b27b --- /dev/null +++ b/dspace-api/src/test/resources/org/dspace/app/mediafilter/test.rtf @@ -0,0 +1,239 @@ +{\rtf1\adeflang1025\ansi\ansicpg1252\uc1\adeff46\deff0\stshfdbch45\stshfloch43\stshfhich43\stshfbi46\deflang1033\deflangfe1033\themelang1033\themelangfe0\themelangcs0{\fonttbl{\f34\fbidi \froman\fcharset0\fprq2{\*\panose 02040503050406030204}Cambria Math;}{\f43\fbidi \froman\fcharset0\fprq2 Liberation Serif{\*\falt Times New Roman};} +{\f44\fbidi \fswiss\fcharset0\fprq2 Liberation Sans{\*\falt Arial};}{\f45\fbidi \froman\fcharset0\fprq0{\*\panose 00000000000000000000}AR PL SungtiL GB;}{\f46\fbidi \froman\fcharset0\fprq0{\*\panose 00000000000000000000}Lohit Hindi;} +{\flomajor\f31500\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\fdbmajor\f31501\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;} +{\fhimajor\f31502\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0302020204030204}Calibri Light;}{\fbimajor\f31503\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;} +{\flominor\f31504\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\fdbminor\f31505\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;} +{\fhiminor\f31506\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0502020204030204}Calibri;}{\fbiminor\f31507\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f1504\fbidi \froman\fcharset238\fprq2 Cambria Math CE;} +{\f1505\fbidi \froman\fcharset204\fprq2 Cambria Math Cyr;}{\f1507\fbidi \froman\fcharset161\fprq2 Cambria Math Greek;}{\f1508\fbidi \froman\fcharset162\fprq2 Cambria Math Tur;}{\f1511\fbidi \froman\fcharset186\fprq2 Cambria Math Baltic;} +{\f1512\fbidi \froman\fcharset163\fprq2 Cambria Math (Vietnamese);}{\flomajor\f31508\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\flomajor\f31509\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;} +{\flomajor\f31511\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\flomajor\f31512\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\flomajor\f31513\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);} +{\flomajor\f31514\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\flomajor\f31515\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\flomajor\f31516\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);} +{\fdbmajor\f31518\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fdbmajor\f31519\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fdbmajor\f31521\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;} +{\fdbmajor\f31522\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fdbmajor\f31523\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fdbmajor\f31524\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);} +{\fdbmajor\f31525\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fdbmajor\f31526\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fhimajor\f31528\fbidi \fswiss\fcharset238\fprq2 Calibri Light CE;} +{\fhimajor\f31529\fbidi \fswiss\fcharset204\fprq2 Calibri Light Cyr;}{\fhimajor\f31531\fbidi \fswiss\fcharset161\fprq2 Calibri Light Greek;}{\fhimajor\f31532\fbidi \fswiss\fcharset162\fprq2 Calibri Light Tur;} +{\fhimajor\f31533\fbidi \fswiss\fcharset177\fprq2 Calibri Light (Hebrew);}{\fhimajor\f31534\fbidi \fswiss\fcharset178\fprq2 Calibri Light (Arabic);}{\fhimajor\f31535\fbidi \fswiss\fcharset186\fprq2 Calibri Light Baltic;} +{\fhimajor\f31536\fbidi \fswiss\fcharset163\fprq2 Calibri Light (Vietnamese);}{\fbimajor\f31538\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fbimajor\f31539\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;} +{\fbimajor\f31541\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fbimajor\f31542\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fbimajor\f31543\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);} +{\fbimajor\f31544\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fbimajor\f31545\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fbimajor\f31546\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);} +{\flominor\f31548\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\flominor\f31549\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\flominor\f31551\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;} +{\flominor\f31552\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\flominor\f31553\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\flominor\f31554\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);} +{\flominor\f31555\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\flominor\f31556\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fdbminor\f31558\fbidi \froman\fcharset238\fprq2 Times New Roman CE;} +{\fdbminor\f31559\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fdbminor\f31561\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fdbminor\f31562\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;} +{\fdbminor\f31563\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fdbminor\f31564\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fdbminor\f31565\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;} +{\fdbminor\f31566\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fhiminor\f31568\fbidi \fswiss\fcharset238\fprq2 Calibri CE;}{\fhiminor\f31569\fbidi \fswiss\fcharset204\fprq2 Calibri Cyr;} +{\fhiminor\f31571\fbidi \fswiss\fcharset161\fprq2 Calibri Greek;}{\fhiminor\f31572\fbidi \fswiss\fcharset162\fprq2 Calibri Tur;}{\fhiminor\f31573\fbidi \fswiss\fcharset177\fprq2 Calibri (Hebrew);} +{\fhiminor\f31574\fbidi \fswiss\fcharset178\fprq2 Calibri (Arabic);}{\fhiminor\f31575\fbidi \fswiss\fcharset186\fprq2 Calibri Baltic;}{\fhiminor\f31576\fbidi \fswiss\fcharset163\fprq2 Calibri (Vietnamese);} +{\fbiminor\f31578\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fbiminor\f31579\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fbiminor\f31581\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;} +{\fbiminor\f31582\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fbiminor\f31583\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fbiminor\f31584\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);} +{\fbiminor\f31585\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fbiminor\f31586\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\f1164\fbidi \froman\fcharset238\fprq2 Times New Roman CE;} +{\f1165\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\f1167\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\f1168\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\f1169\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);} +{\f1170\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\f1171\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\f1172\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}}{\colortbl;\red0\green0\blue0;\red0\green0\blue255; +\red0\green255\blue255;\red0\green255\blue0;\red255\green0\blue255;\red255\green0\blue0;\red255\green255\blue0;\red255\green255\blue255;\red0\green0\blue128;\red0\green128\blue128;\red0\green128\blue0;\red128\green0\blue128;\red128\green0\blue0; +\red128\green128\blue0;\red128\green128\blue128;\red192\green192\blue192;\red0\green0\blue0;\red0\green0\blue0;}{\*\defchp \fs24\lang1033\langfe2052\loch\af43\hich\af43\dbch\af45\langfenp2052 }{\*\defpap +\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 }\noqfpromote {\stylesheet{\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af46\afs24\alang1081 \ltrch\fcs0 +\fs24\lang1033\langfe2052\loch\f43\hich\af43\dbch\af45\cgrid\langnp1033\langfenp2052 \snext0 \sqformat \spriority0 Normal;}{\*\cs10 \additive \ssemihidden \sunhideused \spriority1 Default Paragraph Font;}{\* +\ts11\tsrowd\trftsWidthB3\trpaddl108\trpaddr108\trpaddfl3\trpaddft3\trpaddfb3\trpaddfr3\trcbpat1\trcfpat1\tblind0\tblindtype3\tsvertalt\tsbrdrt\tsbrdrl\tsbrdrb\tsbrdrr\tsbrdrdgl\tsbrdrdgr\tsbrdrh\tsbrdrv +\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af46\afs24\alang1081 \ltrch\fcs0 \fs24\lang1033\langfe2052\loch\f43\hich\af43\dbch\af45\cgrid\langnp1033\langfenp2052 \snext11 \ssemihidden \sunhideused +Normal Table;}{\*\cs15 \additive \sqformat \spriority0 Footnote Characters;}{\*\cs16 \additive \super \spriority0 Footnote Anchor;}{\*\cs17 \additive \super \spriority0 Endnote Anchor;}{\*\cs18 \additive \sqformat \spriority0 Endnote Characters;}{ +\s19\ql \li0\ri0\sb240\sa120\keepn\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af46\afs28\alang1081 \ltrch\fcs0 \fs28\lang1033\langfe2052\loch\f44\hich\af44\dbch\af45\cgrid\langnp1033\langfenp2052 +\sbasedon0 \snext20 \sqformat \spriority0 Heading;}{\s20\ql \li0\ri0\sa140\sl288\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af46\afs24\alang1081 \ltrch\fcs0 +\fs24\lang1033\langfe2052\loch\f43\hich\af43\dbch\af45\cgrid\langnp1033\langfenp2052 \sbasedon0 \snext20 \spriority0 Body Text;}{\s21\ql \li0\ri0\sa140\sl288\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 +\af46\afs24\alang1081 \ltrch\fcs0 \fs24\lang1033\langfe2052\loch\f43\hich\af43\dbch\af45\cgrid\langnp1033\langfenp2052 \sbasedon20 \snext21 \spriority0 List;}{ +\s22\ql \li0\ri0\sb120\sa120\widctlpar\noline\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \ai\af46\afs24\alang1081 \ltrch\fcs0 \i\fs24\lang1033\langfe2052\loch\f43\hich\af43\dbch\af45\cgrid\langnp1033\langfenp2052 +\sbasedon0 \snext22 \sqformat \spriority0 caption;}{\s23\ql \li0\ri0\widctlpar\noline\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af46\afs24\alang1081 \ltrch\fcs0 +\fs24\lang1033\langfe2052\loch\f43\hich\af43\dbch\af45\cgrid\langnp1033\langfenp2052 \sbasedon0 \snext23 \sqformat \spriority0 Index;}{\s24\ql \fi-339\li339\ri0\widctlpar\noline\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin339\itap0 \rtlch\fcs1 +\af46\afs20\alang1081 \ltrch\fcs0 \fs20\lang1033\langfe2052\loch\f43\hich\af43\dbch\af45\cgrid\langnp1033\langfenp2052 \sbasedon0 \snext24 \spriority0 footnote text;}}{\*\rsidtbl \rsid6097384\rsid16590483\rsid16671749}{\mmathPr\mmathFont34\mbrkBin0 +\mbrkBinSub0\msmallFrac0\mdispDef1\mlMargin0\mrMargin0\mdefJc1\mwrapIndent1440\mintLim0\mnaryLim1}{\info{\title A Text Extraction Test Document for DSpace}{\author Mark Wood}{\operator Tim Donohue}{\creatim\yr2022\mo3\dy30\hr13\min54} +{\revtim\yr2022\mo3\dy30\hr13\min54}{\version2}{\edmins0}{\nofpages1}{\nofwords75}{\nofchars433}{\nofcharsws507}{\vern43}}{\*\xmlnstbl {\xmlns1 http://schemas.microsoft.com/office/word/2003/wordml}} +\paperw12240\paperh15840\margl1134\margr1134\margt1134\margb1134\gutter0\ltrsect +\deftab709\widowctrl\ftnbj\aenddoc\trackmoves0\trackformatting1\donotembedsysfont1\relyonvml0\donotembedlingdata0\grfdocevents0\validatexml1\showplaceholdtext0\ignoremixedcontent0\saveinvalidxml0\showxmlerrors1 +\noxlattoyen\expshrtn\noultrlspc\dntblnsbdb\nospaceforul\formshade\horzdoc\dgmargin\dghspace180\dgvspace180\dghorigin450\dgvorigin0\dghshow1\dgvshow1 +\jexpand\viewkind5\viewscale100\pgbrdrhead\pgbrdrfoot\splytwnine\ftnlytwnine\htmautsp\nolnhtadjtbl\useltbaln\alntblind\lytcalctblwd\lyttblrtgr\lnbrkrule\nobrkwrptbl\snaptogridincell\allowfieldendsel\wrppunct +\asianbrkrule\rsidroot6097384\newtblstyruls\nogrowautofit\usenormstyforlist\noindnmbrts\felnbrelev\nocxsptable\indrlsweleven\noafcnsttbl\afelev\utinl\hwelev\spltpgpar\notcvasp\notbrkcnstfrctbl\notvatxbx\krnprsnet\cachedcolbal \nouicompat \fet0 +{\*\wgrffmtfilter 2450}\nofeaturethrottle1\ilfomacatclnup0{\*\ftnsep \ltrpar \pard\plain \ltrpar\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af46\afs24\alang1081 \ltrch\fcs0 +\fs24\lang1033\langfe2052\loch\af43\hich\af43\dbch\af45\cgrid\langnp1033\langfenp2052 {\rtlch\fcs1 \af46 \ltrch\fcs0 \insrsid16671749 \chftnsep +\par }}{\*\ftnsepc \ltrpar \pard\plain \ltrpar\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af46\afs24\alang1081 \ltrch\fcs0 +\fs24\lang1033\langfe2052\loch\af43\hich\af43\dbch\af45\cgrid\langnp1033\langfenp2052 {\rtlch\fcs1 \af46 \ltrch\fcs0 \insrsid16671749 \chftnsepc +\par }}{\*\aftnsep \ltrpar \pard\plain \ltrpar\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af46\afs24\alang1081 \ltrch\fcs0 +\fs24\lang1033\langfe2052\loch\af43\hich\af43\dbch\af45\cgrid\langnp1033\langfenp2052 {\rtlch\fcs1 \af46 \ltrch\fcs0 \insrsid16671749 \chftnsep +\par }}{\*\aftnsepc \ltrpar \pard\plain \ltrpar\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af46\afs24\alang1081 \ltrch\fcs0 +\fs24\lang1033\langfe2052\loch\af43\hich\af43\dbch\af45\cgrid\langnp1033\langfenp2052 {\rtlch\fcs1 \af46 \ltrch\fcs0 \insrsid16671749 \chftnsepc +\par }}\ltrpar \sectd \ltrsect\linex0\headery0\footery0\endnhere\sectunlocked1\sectdefaultcl\sftnbj {\*\pnseclvl1\pnucrm\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl2\pnucltr\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl3 +\pndec\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl4\pnlcltr\pnstart1\pnindent720\pnhang {\pntxta )}}{\*\pnseclvl5\pndec\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl6\pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}} +{\*\pnseclvl7\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl8\pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl9\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}\pard\plain \ltrpar +\qc \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af46\afs24\alang1081 \ltrch\fcs0 \fs24\lang1033\langfe2052\loch\af43\hich\af43\dbch\af45\cgrid\langnp1033\langfenp2052 {\rtlch\fcs1 \af46\afs30 \ltrch\fcs0 +\fs30\insrsid16671749 \hich\af43\dbch\af45\loch\f43 A Text Extraction Test Document}{\rtlch\fcs1 \af46\afs30 \ltrch\fcs0 \fs30\insrsid6097384 +\par }{\rtlch\fcs1 \af46\afs20 \ltrch\fcs0 \fs20\insrsid16671749 \hich\af43\dbch\af45\loch\f43 for}{\rtlch\fcs1 \af46\afs20 \ltrch\fcs0 \fs20\insrsid6097384 +\par }{\rtlch\fcs1 \af46\afs30 \ltrch\fcs0 \fs30\insrsid16671749 \hich\af43\dbch\af45\loch\f43 DSpace}{\rtlch\fcs1 \af46\afs30 \ltrch\fcs0 \fs30\insrsid6097384 +\par +\par }\pard \ltrpar\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 {\rtlch\fcs1 \af46 \ltrch\fcs0 \insrsid16671749 \hich\af43\dbch\af45\loch\f43 +This is a text. For the next sixty seconds this software will conduct a test of the DSpace text extraction facility. This is only a text.}{\rtlch\fcs1 \af46 \ltrch\fcs0 \insrsid6097384 +\par +\par }{\rtlch\fcs1 \af46 \ltrch\fcs0 \insrsid16671749 \hich\af43\dbch\af45\loch\f43 This is a paragraph that followed the first that lived in the \hich\af43\dbch\af45\loch\f43 document that Jack built.}{\rtlch\fcs1 \af46 \ltrch\fcs0 \insrsid6097384 +\par +\par }{\rtlch\fcs1 \af46 \ltrch\fcs0 \insrsid16671749 \hich\af43\dbch\af45\loch\f43 Lorem ipsum dolor sit amet. The quick brown fox jumped over the lazy dog. Yow! Are we having fun yet?}{\rtlch\fcs1 \af46 \ltrch\fcs0 \insrsid6097384 +\par +\par }{\rtlch\fcs1 \af46 \ltrch\fcs0 \insrsid16671749 \hich\af43\dbch\af45\loch\f43 This has been a test of the DSpace text extraction system. In the event of actual content you would care what is written he\hich\af43\dbch\af45\loch\f43 re.}{\rtlch\fcs1 +\af46 \ltrch\fcs0 \cs16\super\insrsid16671749 \chftn {\footnote \ltrpar \pard\plain \ltrpar\s24\ql \fi-339\li339\ri0\widctlpar\noline\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin339\itap0 \rtlch\fcs1 \af46\afs20\alang1081 \ltrch\fcs0 +\fs20\lang1033\langfe2052\loch\af43\hich\af43\dbch\af45\cgrid\langnp1033\langfenp2052 {\rtlch\fcs1 \af46 \ltrch\fcs0 \insrsid16671749 \chftn \tab \hich\af43\dbch\af45\loch\f43 Tip o\hich\f43 \rquote \loch\f43 + the hat to the U.S. Emergency Broadcast System for the format that I have irreverently borrowed.}}}{\rtlch\fcs1 \af46 \ltrch\fcs0 \insrsid6097384 +\par }{\*\themedata 504b030414000600080000002100e9de0fbfff0000001c020000130000005b436f6e74656e745f54797065735d2e786d6cac91cb4ec3301045f748fc83e52d4a +9cb2400825e982c78ec7a27cc0c8992416c9d8b2a755fbf74cd25442a820166c2cd933f79e3be372bd1f07b5c3989ca74aaff2422b24eb1b475da5df374fd9ad +5689811a183c61a50f98f4babebc2837878049899a52a57be670674cb23d8e90721f90a4d2fa3802cb35762680fd800ecd7551dc18eb899138e3c943d7e503b6 +b01d583deee5f99824e290b4ba3f364eac4a430883b3c092d4eca8f946c916422ecab927f52ea42b89a1cd59c254f919b0e85e6535d135a8de20f20b8c12c3b0 +0c895fcf6720192de6bf3b9e89ecdbd6596cbcdd8eb28e7c365ecc4ec1ff1460f53fe813d3cc7f5b7f020000ffff0300504b030414000600080000002100a5d6 +a7e7c0000000360100000b0000005f72656c732f2e72656c73848fcf6ac3300c87ef85bd83d17d51d2c31825762fa590432fa37d00e1287f68221bdb1bebdb4f +c7060abb0884a4eff7a93dfeae8bf9e194e720169aaa06c3e2433fcb68e1763dbf7f82c985a4a725085b787086a37bdbb55fbc50d1a33ccd311ba548b6309512 +0f88d94fbc52ae4264d1c910d24a45db3462247fa791715fd71f989e19e0364cd3f51652d73760ae8fa8c9ffb3c330cc9e4fc17faf2ce545046e37944c69e462 +a1a82fe353bd90a865aad41ed0b5b8f9d6fd010000ffff0300504b0304140006000800000021006b799616830000008a0000001c0000007468656d652f746865 +6d652f7468656d654d616e616765722e786d6c0ccc4d0ac3201040e17da17790d93763bb284562b2cbaebbf600439c1a41c7a0d29fdbd7e5e38337cedf14d59b +4b0d592c9c070d8a65cd2e88b7f07c2ca71ba8da481cc52c6ce1c715e6e97818c9b48d13df49c873517d23d59085adb5dd20d6b52bd521ef2cdd5eb9246a3d8b +4757e8d3f729e245eb2b260a0238fd010000ffff0300504b030414000600080000002100b6f4679893070000c9200000160000007468656d652f7468656d652f +7468656d65312e786d6cec59cd8b1bc915bf07f23f347d97f5d5ad8fc1f2a24fcfda33b6b164873dd648a5eef2547789aad28cc56208de532e81c026e49085bd +ed21842cecc22eb9e48f31d8249b3f22afaa5bdd5552c99e191c3061463074977eefd5afde7bf5de53d5ddcf5e26d4bbc05c1096f6fcfa9d9aefe174ce16248d +7afeb3d9a4d2f13d2151ba4094a5b8e76fb0f03fbbf7eb5fdd454732c609f6403e1547a8e7c752ae8eaa5531876124eeb0154ee1bb25e30992f0caa3ea82a34b +d09bd06aa3566b55134452df4b51026a1f2f97648ebd9952e9dfdb2a1f53784da5500373caa74a35b6243476715e5708b11143cabd0b447b3eccb3609733fc52 +fa1e4542c2173dbfa6fffceabdbb5574940b517940d6909be8bf5c2e17589c37f49c3c3a2b260d823068f50bfd1a40e53e6edc1eb7c6ad429f06a0f91c569a71 +b175b61bc320c71aa0ecd1a17bd41e35eb16ded0dfdce3dc0fd5c7c26b50a63fd8c34f2643b0a285d7a00c1feee1c3417730b2f56b50866fede1dbb5fe28685b +fa3528a6243ddf43d7c25673b85d6d0159327aec8477c360d26ee4ca4b144443115d6a8a254be5a1584bd00bc6270050408a24493db959e1259a43140f112567 +9c7827248a21f056286502866b8ddaa4d684ffea13e827ed5174849121ad780113b137a4f87862cec94af6fc07a0d537206f7ffef9cdeb1fdfbcfee9cd575fbd +79fdf77c6eadca923b466964cafdf2dd1ffef3cd6fbd7ffff0ed2f5fff319b7a172f4cfcbbbffdeedd3ffef93ef5b0e2d2146ffff4fdbb1fbf7ffbe7dfffebaf +5f3bb4f7393a33e1339260e13dc297de5396c0021dfcf119bf9ec42c46c494e8a791402952b338f48f656ca11f6d10450edc00db767cce21d5b880f7d72f2cc2 +d398af2571687c182716f094313a60dc6985876a2ec3ccb3751ab927e76b13f714a10bd7dc43945a5e1eaf579063894be530c616cd2714a5124538c5d253dfb1 +738c1dabfb8210cbaea764ce99604be97d41bc01224e93ccc899154da5d03149c02f1b1741f0b7659bd3e7de8051d7aa47f8c246c2de40d4417e86a965c6fb68 +2d51e252394309350d7e8264ec2239ddf0b9891b0b099e8e3065de78818570c93ce6b05ec3e90f21cdb8dd7e4a37898de4929cbb749e20c64ce4889d0f6394ac +5cd829496313fbb938871045de13265df05366ef10f50e7e40e941773f27d872f787b3c133c8b026a53240d4376beef0e57dccacf89d6ee8126157aae9f3c44a +b17d4e9cd131584756689f604cd1255a60ec3dfbdcc160c05696cd4bd20f62c82ac7d815580f901dabea3dc5027a25d5dcece7c91322ac909de2881de073bad9 +493c1b9426881fd2fc08bc6eda7c0ca52e7105c0633a3f37818f08f480102f4ea33c16a0c308ee835a9fc4c82a60ea5db8e375c32dff5d658fc1be7c61d1b8c2 +be04197c6d1948eca6cc7b6d3343d49aa00c9819822ec3956e41c4727f29a28aab165b3be596f6a62ddd00dd91d5f42424fd6007b4d3fb84ffbbde073a8cb77f +f9c6b10f3e4ebfe3566c25ab6b763a8792c9f14e7f7308b7dbd50c195f904fbfa919a175fa04431dd9cf58b73dcd6d4fe3ffdff73487f6f36d2773a8dfb8ed64 +7ce8306e3b99fc70e5e3743265f3027d8d3af0c80e7af4b14f72f0d46749289dca0dc527421ffc08f83db398c0a092d3279eb838055cc5f0a8ca1c4c60e1228e +b48cc799fc0d91f134462b381daafb4a492472d591f0564cc0a1911e76ea5678ba4e4ed9223becacd7d5c16656590592e5782d2cc6e1a04a66e856bb3cc02bd4 +6bb6913e68dd1250b2d721614c6693683a48b4b783ca48fa58178ce620a157f65158741d2c3a4afdd6557b2c805ae115f8c1edc1cff49e1f06200242701e07cd +f942f92973f5d6bbda991fd3d3878c69450034d8db08283ddd555c0f2e4fad2e0bb52b78da2261849b4d425b46377822869fc17974aad1abd0b8aeafbba54b2d +7aca147a3e08ad9246bbf33e1637f535c8ede6069a9a9982a6de65cf6f35430899395af5fc251c1ac363b282d811ea3717a211dcbccc25cf36fc4d32cb8a0b39 +4222ce0cae934e960d122231f728497abe5a7ee1069aea1ca2b9d51b90103e59725d482b9f1a3970baed64bc5ce2b934dd6e8c284b67af90e1b35ce1fc568bdf +1cac24d91adc3d8d1797de195df3a708422c6cd795011744c0dd413db3e682c0655891c8caf8db294c79da356fa3740c65e388ae62945714339967709dca0b3a +faadb081f196af190c6a98242f8467912ab0a651ad6a5a548d8cc3c1aafb6121653923699635d3ca2aaa6abab39835c3b60cecd8f26645de60b53531e434b3c2 +67a97b37e576b7b96ea74f28aa0418bcb09fa3ea5ea12018d4cac92c6a8af17e1a56393b1fb56bc776811fa07695226164fdd656ed8edd8a1ae19c0e066f54f9 +416e376a6168b9ed2bb5a5f5adb979b1cdce5e40f2184197bba6526857c2c92e47d0104d754f92a50dd8222f65be35e0c95b73d2f3bfac85fd60d80887955a27 +1c57826650ab74c27eb3d20fc3667d1cd66ba341e31514161927f530bbb19fc00506dde4f7f67a7cefee3ed9ded1dc99b3a4caf4dd7c5513d777f7f5c6e1bb7b +8f40d2f9b2d598749bdd41abd26df627956034e854bac3d6a0326a0ddba3c9681876ba9357be77a1c141bf390c5ae34ea5551f0e2b41aba6e877ba9576d068f4 +8376bf330efaaff23606569ea58fdc16605ecdebde7f010000ffff0300504b0304140006000800000021000dd1909fb60000001b010000270000007468656d65 +2f7468656d652f5f72656c732f7468656d654d616e616765722e786d6c2e72656c73848f4d0ac2301484f78277086f6fd3ba109126dd88d0add40384e4350d36 +3f2451eced0dae2c082e8761be9969bb979dc9136332de3168aa1a083ae995719ac16db8ec8e4052164e89d93b64b060828e6f37ed1567914b284d262452282e +3198720e274a939cd08a54f980ae38a38f56e422a3a641c8bbd048f7757da0f19b017cc524bd62107bd5001996509affb3fd381a89672f1f165dfe514173d985 +0528a2c6cce0239baa4c04ca5bbabac4df000000ffff0300504b01022d0014000600080000002100e9de0fbfff0000001c020000130000000000000000000000 +0000000000005b436f6e74656e745f54797065735d2e786d6c504b01022d0014000600080000002100a5d6a7e7c0000000360100000b00000000000000000000 +000000300100005f72656c732f2e72656c73504b01022d00140006000800000021006b799616830000008a0000001c0000000000000000000000000019020000 +7468656d652f7468656d652f7468656d654d616e616765722e786d6c504b01022d0014000600080000002100b6f4679893070000c92000001600000000000000 +000000000000d60200007468656d652f7468656d652f7468656d65312e786d6c504b01022d00140006000800000021000dd1909fb60000001b01000027000000 +000000000000000000009d0a00007468656d652f7468656d652f5f72656c732f7468656d654d616e616765722e786d6c2e72656c73504b050600000000050005005d010000980b00000000} +{\*\colorschememapping 3c3f786d6c2076657273696f6e3d22312e302220656e636f64696e673d225554462d3822207374616e64616c6f6e653d22796573223f3e0d0a3c613a636c724d +617020786d6c6e733a613d22687474703a2f2f736368656d61732e6f70656e786d6c666f726d6174732e6f72672f64726177696e676d6c2f323030362f6d6169 +6e22206267313d226c743122207478313d22646b3122206267323d226c743222207478323d22646b322220616363656e74313d22616363656e74312220616363 +656e74323d22616363656e74322220616363656e74333d22616363656e74332220616363656e74343d22616363656e74342220616363656e74353d22616363656e74352220616363656e74363d22616363656e74362220686c696e6b3d22686c696e6b2220666f6c486c696e6b3d22666f6c486c696e6b222f3e} +{\*\latentstyles\lsdstimax376\lsdlockeddef0\lsdsemihiddendef0\lsdunhideuseddef0\lsdqformatdef0\lsdprioritydef99{\lsdlockedexcept \lsdqformat1 \lsdpriority0 \lsdlocked0 Normal;\lsdqformat1 \lsdpriority9 \lsdlocked0 heading 1; +\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 2;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 3;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 4; +\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 5;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 6;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 7; +\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 8;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 9;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 1; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 5; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 7;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 8;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 9; +\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 1;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 2;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 3; +\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 4;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 5;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 6; +\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 7;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 8;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 9;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal Indent; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footnote text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 header;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footer; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index heading;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority35 \lsdlocked0 caption;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 table of figures; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 envelope address;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 envelope return;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footnote reference;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation reference; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 line number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 page number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 endnote reference;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 endnote text; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 table of authorities;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 macro;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 toa heading;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 3; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 3; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 3; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 5;\lsdqformat1 \lsdpriority10 \lsdlocked0 Title;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Closing; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Signature;\lsdsemihidden1 \lsdunhideused1 \lsdpriority1 \lsdlocked0 Default Paragraph Font;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 4; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Message Header;\lsdqformat1 \lsdpriority11 \lsdlocked0 Subtitle;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Salutation; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Date;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text First Indent;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text First Indent 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Note Heading; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent 3; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Block Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 FollowedHyperlink;\lsdqformat1 \lsdpriority22 \lsdlocked0 Strong; +\lsdqformat1 \lsdpriority20 \lsdlocked0 Emphasis;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Document Map;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Plain Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 E-mail Signature; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Top of Form;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Bottom of Form;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal (Web);\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Acronym; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Address;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Cite;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Code;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Definition; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Keyboard;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Preformatted;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Sample;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Typewriter; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Variable;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal Table;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation subject;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 No List; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Simple 1; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Simple 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Simple 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Classic 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Classic 2; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Classic 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Classic 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Colorful 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Colorful 2; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Colorful 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 3; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 2; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 6; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 7;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 8;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 2; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 6; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 7;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 8;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table 3D effects 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table 3D effects 2; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table 3D effects 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Contemporary;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Elegant;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Professional; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Subtle 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Subtle 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Web 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Web 2; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Web 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Balloon Text;\lsdpriority39 \lsdlocked0 Table Grid;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Theme;\lsdsemihidden1 \lsdlocked0 Placeholder Text; +\lsdqformat1 \lsdpriority1 \lsdlocked0 No Spacing;\lsdpriority60 \lsdlocked0 Light Shading;\lsdpriority61 \lsdlocked0 Light List;\lsdpriority62 \lsdlocked0 Light Grid;\lsdpriority63 \lsdlocked0 Medium Shading 1;\lsdpriority64 \lsdlocked0 Medium Shading 2; +\lsdpriority65 \lsdlocked0 Medium List 1;\lsdpriority66 \lsdlocked0 Medium List 2;\lsdpriority67 \lsdlocked0 Medium Grid 1;\lsdpriority68 \lsdlocked0 Medium Grid 2;\lsdpriority69 \lsdlocked0 Medium Grid 3;\lsdpriority70 \lsdlocked0 Dark List; +\lsdpriority71 \lsdlocked0 Colorful Shading;\lsdpriority72 \lsdlocked0 Colorful List;\lsdpriority73 \lsdlocked0 Colorful Grid;\lsdpriority60 \lsdlocked0 Light Shading Accent 1;\lsdpriority61 \lsdlocked0 Light List Accent 1; +\lsdpriority62 \lsdlocked0 Light Grid Accent 1;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 1;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 1;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 1;\lsdsemihidden1 \lsdlocked0 Revision; +\lsdqformat1 \lsdpriority34 \lsdlocked0 List Paragraph;\lsdqformat1 \lsdpriority29 \lsdlocked0 Quote;\lsdqformat1 \lsdpriority30 \lsdlocked0 Intense Quote;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 1;\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 1; +\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 1;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 1;\lsdpriority70 \lsdlocked0 Dark List Accent 1;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 1;\lsdpriority72 \lsdlocked0 Colorful List Accent 1; +\lsdpriority73 \lsdlocked0 Colorful Grid Accent 1;\lsdpriority60 \lsdlocked0 Light Shading Accent 2;\lsdpriority61 \lsdlocked0 Light List Accent 2;\lsdpriority62 \lsdlocked0 Light Grid Accent 2;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 2; +\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 2;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 2;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 2;\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 2;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 2; +\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 2;\lsdpriority70 \lsdlocked0 Dark List Accent 2;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 2;\lsdpriority72 \lsdlocked0 Colorful List Accent 2;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 2; +\lsdpriority60 \lsdlocked0 Light Shading Accent 3;\lsdpriority61 \lsdlocked0 Light List Accent 3;\lsdpriority62 \lsdlocked0 Light Grid Accent 3;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 3;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 3; +\lsdpriority65 \lsdlocked0 Medium List 1 Accent 3;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 3;\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 3;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 3;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 3; +\lsdpriority70 \lsdlocked0 Dark List Accent 3;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 3;\lsdpriority72 \lsdlocked0 Colorful List Accent 3;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 3;\lsdpriority60 \lsdlocked0 Light Shading Accent 4; +\lsdpriority61 \lsdlocked0 Light List Accent 4;\lsdpriority62 \lsdlocked0 Light Grid Accent 4;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 4;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 4;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 4; +\lsdpriority66 \lsdlocked0 Medium List 2 Accent 4;\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 4;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 4;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 4;\lsdpriority70 \lsdlocked0 Dark List Accent 4; +\lsdpriority71 \lsdlocked0 Colorful Shading Accent 4;\lsdpriority72 \lsdlocked0 Colorful List Accent 4;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 4;\lsdpriority60 \lsdlocked0 Light Shading Accent 5;\lsdpriority61 \lsdlocked0 Light List Accent 5; +\lsdpriority62 \lsdlocked0 Light Grid Accent 5;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 5;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 5;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 5;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 5; +\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 5;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 5;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 5;\lsdpriority70 \lsdlocked0 Dark List Accent 5;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 5; +\lsdpriority72 \lsdlocked0 Colorful List Accent 5;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 5;\lsdpriority60 \lsdlocked0 Light Shading Accent 6;\lsdpriority61 \lsdlocked0 Light List Accent 6;\lsdpriority62 \lsdlocked0 Light Grid Accent 6; +\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 6;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 6;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 6;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 6; +\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 6;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 6;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 6;\lsdpriority70 \lsdlocked0 Dark List Accent 6;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 6; +\lsdpriority72 \lsdlocked0 Colorful List Accent 6;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 6;\lsdqformat1 \lsdpriority19 \lsdlocked0 Subtle Emphasis;\lsdqformat1 \lsdpriority21 \lsdlocked0 Intense Emphasis; +\lsdqformat1 \lsdpriority31 \lsdlocked0 Subtle Reference;\lsdqformat1 \lsdpriority32 \lsdlocked0 Intense Reference;\lsdqformat1 \lsdpriority33 \lsdlocked0 Book Title;\lsdsemihidden1 \lsdunhideused1 \lsdpriority37 \lsdlocked0 Bibliography; +\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority39 \lsdlocked0 TOC Heading;\lsdpriority41 \lsdlocked0 Plain Table 1;\lsdpriority42 \lsdlocked0 Plain Table 2;\lsdpriority43 \lsdlocked0 Plain Table 3;\lsdpriority44 \lsdlocked0 Plain Table 4; +\lsdpriority45 \lsdlocked0 Plain Table 5;\lsdpriority40 \lsdlocked0 Grid Table Light;\lsdpriority46 \lsdlocked0 Grid Table 1 Light;\lsdpriority47 \lsdlocked0 Grid Table 2;\lsdpriority48 \lsdlocked0 Grid Table 3;\lsdpriority49 \lsdlocked0 Grid Table 4; +\lsdpriority50 \lsdlocked0 Grid Table 5 Dark;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 1;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 1; +\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 1;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 1;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 1;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 1; +\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 1;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 2;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 2;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 2; +\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 2;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 2;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 2;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 2; +\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 3;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 3;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 3;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 3; +\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 3;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 3;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 3;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 4; +\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 4;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 4;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 4;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 4; +\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 4;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 4;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 5;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 5; +\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 5;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 5;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 5; +\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 5;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 6;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 6; +\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 6;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 6; +\lsdpriority46 \lsdlocked0 List Table 1 Light;\lsdpriority47 \lsdlocked0 List Table 2;\lsdpriority48 \lsdlocked0 List Table 3;\lsdpriority49 \lsdlocked0 List Table 4;\lsdpriority50 \lsdlocked0 List Table 5 Dark; +\lsdpriority51 \lsdlocked0 List Table 6 Colorful;\lsdpriority52 \lsdlocked0 List Table 7 Colorful;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 1;\lsdpriority47 \lsdlocked0 List Table 2 Accent 1;\lsdpriority48 \lsdlocked0 List Table 3 Accent 1; +\lsdpriority49 \lsdlocked0 List Table 4 Accent 1;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 1;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 1;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 1; +\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 2;\lsdpriority47 \lsdlocked0 List Table 2 Accent 2;\lsdpriority48 \lsdlocked0 List Table 3 Accent 2;\lsdpriority49 \lsdlocked0 List Table 4 Accent 2; +\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 2;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 2;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 2;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 3; +\lsdpriority47 \lsdlocked0 List Table 2 Accent 3;\lsdpriority48 \lsdlocked0 List Table 3 Accent 3;\lsdpriority49 \lsdlocked0 List Table 4 Accent 3;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 3; +\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 3;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 3;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 4;\lsdpriority47 \lsdlocked0 List Table 2 Accent 4; +\lsdpriority48 \lsdlocked0 List Table 3 Accent 4;\lsdpriority49 \lsdlocked0 List Table 4 Accent 4;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 4;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 4; +\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 4;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 5;\lsdpriority47 \lsdlocked0 List Table 2 Accent 5;\lsdpriority48 \lsdlocked0 List Table 3 Accent 5; +\lsdpriority49 \lsdlocked0 List Table 4 Accent 5;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 5;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 5; +\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 List Table 2 Accent 6;\lsdpriority48 \lsdlocked0 List Table 3 Accent 6;\lsdpriority49 \lsdlocked0 List Table 4 Accent 6; +\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Mention; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hashtag;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Unresolved Mention;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Link;}}{\*\datastore 01050000 +02000000180000004d73786d6c322e534158584d4c5265616465722e362e3000000000000000000000060000 +d0cf11e0a1b11ae1000000000000000000000000000000003e000300feff090006000000000000000000000001000000010000000000000000100000feffffff00000000feffffff0000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +fffffffffffffffffdfffffffeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffff52006f006f007400200045006e00740072007900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016000500ffffffffffffffffffffffff0c6ad98892f1d411a65f0040963251e5000000000000000000000000d0af +77916744d801feffffff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000 +00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000 +000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff000000000000000000000000000000000000000000000000 +0000000000000000000000000000000000000000000000000105000000000000}} \ No newline at end of file diff --git a/dspace-api/src/test/resources/org/dspace/app/mediafilter/test.xls b/dspace-api/src/test/resources/org/dspace/app/mediafilter/test.xls new file mode 100644 index 0000000000000000000000000000000000000000..1ebc20bc3810a239e20fdb8d221b8830594bce07 GIT binary patch literal 23552 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 + + text/csv + CSV + Comma-Separated Values + 1 + false + csv + + application/msword Microsoft Word From 476fe7220dd2ed47b4a2ec808a3e37d53d41e154 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 30 Mar 2022 14:43:29 -0500 Subject: [PATCH 0123/1846] Minor fix to BitstreamFormatRestRepositoryIT after adding new format (CSV) --- .../org/dspace/app/rest/BitstreamFormatRestRepositoryIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamFormatRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamFormatRestRepositoryIT.java index 48ad410d0035..1a6cc29ca75c 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamFormatRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamFormatRestRepositoryIT.java @@ -56,7 +56,7 @@ public class BitstreamFormatRestRepositoryIT extends AbstractControllerIntegrati @Autowired private BitstreamFormatConverter bitstreamFormatConverter; - private final int DEFAULT_AMOUNT_FORMATS = 80; + private final int DEFAULT_AMOUNT_FORMATS = 81; @Test public void findAllPaginationTest() throws Exception { From 9ef561a5d01189f37b6dcc37bb350a3981b40f58 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 30 Mar 2022 16:52:39 -0500 Subject: [PATCH 0124/1846] Switch to System.err.format & correct spacing in error messages. --- .../dspace/app/mediafilter/TikaTextExtractionFilter.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/TikaTextExtractionFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/TikaTextExtractionFilter.java index 909291e450fd..07b1576086c8 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/TikaTextExtractionFilter.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/TikaTextExtractionFilter.java @@ -79,14 +79,13 @@ public InputStream getDestinationStream(Item currentItem, InputStream source, bo tika.setMaxStringLength(maxChars); // Tell Tika the maximum number of characters to extract extractedText = tika.parseToString(source); } catch (IOException e) { - System.err.println("Unable to extract text from bitstream in Item " + currentItem.getID().toString()); + System.err.format("Unable to extract text from bitstream in Item %s%n", currentItem.getID().toString()); e.printStackTrace(); log.error("Unable to extract text from bitstream in Item {}", currentItem.getID().toString(), e); throw e; } catch (OutOfMemoryError oe) { - System.err.println("OutOfMemoryError occurred when extracting text from bitstream in Item " + - currentItem.getID().toString() + - "You may wish to enable 'textextractor.use-temp-file'."); + System.err.format("OutOfMemoryError occurred when extracting text from bitstream in Item %s. " + + "You may wish to enable 'textextractor.use-temp-file'.%n", currentItem.getID().toString()); oe.printStackTrace(); log.error("OutOfMemoryError occurred when extracting text from bitstream in Item {}. " + "You may wish to enable 'textextractor.use-temp-file'.", currentItem.getID().toString(), oe); From b5876852d90b7f07f25d9d1eb0228460b1686182 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 4 Apr 2022 10:28:04 -0500 Subject: [PATCH 0125/1846] Resolve feedback. Ensure errors are re-thrown & fix comment. --- .../mediafilter/TikaTextExtractionFilter.java | 18 ++++++++++++------ .../TikaTextExtractionFilterTest.java | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/TikaTextExtractionFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/TikaTextExtractionFilter.java index 07b1576086c8..e83bf706ed02 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/TikaTextExtractionFilter.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/TikaTextExtractionFilter.java @@ -136,12 +136,15 @@ private InputStream extractUsingTempFile(InputStream source, boolean verbose) * Write all extracted characters directly to the temp file. */ @Override - public void characters(char[] ch, int start, int length) { + public void characters(char[] ch, int start, int length) throws SAXException { try { writer.append(new String(ch), start, length); } catch (IOException e) { - log.error("Could not append to temporary file at {} when performing text extraction", - tempExtractedTextFile.getAbsolutePath(), e); + String errorMsg = String.format("Could not append to temporary file at %s " + + "when performing text extraction", + tempExtractedTextFile.getAbsolutePath()); + log.error(errorMsg, e); + throw new SAXException(errorMsg, e); } } @@ -151,12 +154,15 @@ public void characters(char[] ch, int start, int length) { * (like blank lines, indentations, etc.), so that we get the same extracted text either way. */ @Override - public void ignorableWhitespace(char[] ch, int start, int length) { + public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException { try { writer.append(new String(ch), start, length); } catch (IOException e) { - log.error("Could not append to temporary file at {} when performing text extraction", - tempExtractedTextFile.getAbsolutePath(), e); + String errorMsg = String.format("Could not append to temporary file at %s " + + "when performing text extraction", + tempExtractedTextFile.getAbsolutePath()); + log.error(errorMsg, e); + throw new SAXException(errorMsg, e); } } }); diff --git a/dspace-api/src/test/java/org/dspace/app/mediafilter/TikaTextExtractionFilterTest.java b/dspace-api/src/test/java/org/dspace/app/mediafilter/TikaTextExtractionFilterTest.java index b7d0ed5a3bee..9db1ef77768b 100644 --- a/dspace-api/src/test/java/org/dspace/app/mediafilter/TikaTextExtractionFilterTest.java +++ b/dspace-api/src/test/java/org/dspace/app/mediafilter/TikaTextExtractionFilterTest.java @@ -23,7 +23,7 @@ /** * Test the TikaTextExtractionFilter using test files for all major formats. - * The test files used below are all located at [dspace-api]/src/main/resources/org/dspace/app/mediafilter/ + * The test files used below are all located at [dspace-api]/src/test/resources/org/dspace/app/mediafilter/ * * @author mwood * @author Tim Donohue From 3a01dbe0a2b1f4ca6bfe30f2f785b9c6c55ee6fb Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 20 Dec 2021 16:50:00 -0600 Subject: [PATCH 0126/1846] Upgrade to latest Spring Boot, Solr & Postgres. Minor code fixes/dependency updates to get everything to compile --- dspace-server-webapp/pom.xml | 8 ++++-- .../rest/AuthenticationRestController.java | 2 +- .../app/rest/DiscoveryRestController.java | 2 +- .../app/rest/IdentifierRestController.java | 4 +-- .../dspace/app/rest/OidcRestController.java | 2 +- .../app/rest/RestResourceController.java | 4 +-- .../app/rest/StatisticsRestController.java | 2 +- .../SubmissionCCLicenseUrlRepository.java | 2 +- .../app/rest/UUIDLookupRestController.java | 4 +-- .../dspace/app/rest/link/HalLinkFactory.java | 4 +-- .../repository/ClaimedTaskRestRepository.java | 2 +- .../rest/repository/DSpaceRestRepository.java | 8 ++++++ .../repository/EPersonRestRepository.java | 2 +- .../repository/PoolTaskRestRepository.java | 2 +- .../ResourcePolicyRestRepository.java | 2 +- .../VocabularyEntryDetailsRestRepository.java | 2 +- pom.xml | 28 ++++++++++++++----- 17 files changed, 52 insertions(+), 28 deletions(-) diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 4677366a2c57..6c34e11a81c7 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -298,6 +298,12 @@ org.springframework.data spring-data-rest-hal-browser ${spring-hal-browser.version} + + + org.springframework.data + spring-data-rest-webmvc + + @@ -463,13 +469,11 @@ com.jayway.jsonpath json-path - ${json-path.version} test com.jayway.jsonpath json-path-assert - ${json-path.version} test diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/AuthenticationRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/AuthenticationRestController.java index 9660d0af5677..7d9cb470f9c1 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/AuthenticationRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/AuthenticationRestController.java @@ -80,7 +80,7 @@ public class AuthenticationRestController implements InitializingBean { @Override public void afterPropertiesSet() { discoverableEndpointsService - .register(this, Arrays.asList(new Link("/api/" + AuthnRest.CATEGORY, AuthnRest.NAME))); + .register(this, Arrays.asList(Link.of("/api/" + AuthnRest.CATEGORY, AuthnRest.NAME))); } @RequestMapping(method = RequestMethod.GET) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/DiscoveryRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/DiscoveryRestController.java index 5ecbe191761b..947515ca54c5 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/DiscoveryRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/DiscoveryRestController.java @@ -73,7 +73,7 @@ public class DiscoveryRestController implements InitializingBean { @Override public void afterPropertiesSet() throws Exception { discoverableEndpointsService - .register(this, Arrays.asList(new Link("/api/" + SearchResultsRest.CATEGORY, SearchResultsRest.CATEGORY))); + .register(this, Arrays.asList(Link.of("/api/" + SearchResultsRest.CATEGORY, SearchResultsRest.CATEGORY))); } @RequestMapping(method = RequestMethod.GET) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/IdentifierRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/IdentifierRestController.java index d3a6ef981d8c..5f7a473b910f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/IdentifierRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/IdentifierRestController.java @@ -66,8 +66,8 @@ public void afterPropertiesSet() throws Exception { discoverableEndpointsService .register(this, Arrays.asList( - new Link( - new UriTemplate("/api/" + CATEGORY + "/" + ACTION, + Link.of( + UriTemplate.of("/api/" + CATEGORY + "/" + ACTION, new TemplateVariables( new TemplateVariable(PARAM, VariableType.REQUEST_PARAM))), CATEGORY))); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/OidcRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/OidcRestController.java index ab34a72a8ddf..db04b3b7cd95 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/OidcRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/OidcRestController.java @@ -45,7 +45,7 @@ public class OidcRestController { @PostConstruct public void afterPropertiesSet() { - discoverableEndpointsService.register(this, List.of(new Link("/api/" + AuthnRest.CATEGORY, "oidc"))); + discoverableEndpointsService.register(this, List.of(Link.of("/api/" + AuthnRest.CATEGORY, "oidc"))); } @RequestMapping(method = RequestMethod.GET) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/RestResourceController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/RestResourceController.java index 7c79a857017b..b82b4830753c 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/RestResourceController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/RestResourceController.java @@ -127,7 +127,7 @@ public void afterPropertiesSet() { // Link l = linkTo(this.getClass(), r).withRel(r); String[] split = r.split("\\.", 2); String plural = English.plural(split[1]); - Link l = new Link("/api/" + split[0] + "/" + plural, plural); + Link l = Link.of("/api/" + split[0] + "/" + plural, plural); links.add(l); log.debug(l.getRel().value() + " " + l.getHref()); } @@ -821,7 +821,7 @@ private RepresentationModel findRelInternal(HttpServle link = linkTo(this.getClass(), apiCategory, model).slash(uuid).slash(subpath).withSelfRel(); } - return new EntityModel(new EmbeddedPage(link.getHref(), + return EntityModel.of(new EmbeddedPage(link.getHref(), pageResult.map(converter::toResource), null, subpath)); } else { RestModel object = (RestModel) linkMethod.invoke(linkRepository, request, diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/StatisticsRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/StatisticsRestController.java index 4c2dc545745f..a491157f533a 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/StatisticsRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/StatisticsRestController.java @@ -57,7 +57,7 @@ public class StatisticsRestController implements InitializingBean { public void afterPropertiesSet() throws Exception { discoverableEndpointsService .register(this, Arrays - .asList(new Link("/api/" + RestAddressableModel.STATISTICS, RestAddressableModel.STATISTICS))); + .asList(Link.of("/api/" + RestAddressableModel.STATISTICS, RestAddressableModel.STATISTICS))); } @RequestMapping(method = RequestMethod.GET) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseUrlRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseUrlRepository.java index 78e71d91fdb2..c32d551cbee3 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseUrlRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseUrlRepository.java @@ -132,7 +132,7 @@ public Class getDomainClass() { @Override public void afterPropertiesSet() { discoverableEndpointsService.register(this, Arrays.asList( - new Link("/api/" + SubmissionCCLicenseUrlRest.CATEGORY + "/" + + Link.of("/api/" + SubmissionCCLicenseUrlRest.CATEGORY + "/" + SubmissionCCLicenseUrlRest.PLURAL + "/search", SubmissionCCLicenseUrlRest.PLURAL + "-search"))); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/UUIDLookupRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/UUIDLookupRestController.java index 01f77a8ddc3f..40c0a79b97be 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/UUIDLookupRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/UUIDLookupRestController.java @@ -73,8 +73,8 @@ public void afterPropertiesSet() throws Exception { discoverableEndpointsService .register(this, Arrays.asList( - new Link( - new UriTemplate("/api/" + CATEGORY + "/" + ACTION, + Link.of( + UriTemplate.of("/api/" + CATEGORY + "/" + ACTION, new TemplateVariables( new TemplateVariable(PARAM, VariableType.REQUEST_PARAM))), CATEGORY))); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/HalLinkFactory.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/HalLinkFactory.java index 093c93a182cf..659c0be517de 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/HalLinkFactory.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/HalLinkFactory.java @@ -56,9 +56,7 @@ protected UriComponentsBuilder uriBuilder(T data) { } protected Link buildLink(String rel, String href) { - Link link = new Link(href, rel); - - return link; + return Link.of(href, rel); } protected CONTROLLER getMethodOn(Object... parameters) { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ClaimedTaskRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ClaimedTaskRestRepository.java index 3c0c81a88a83..dbd37093a9fc 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ClaimedTaskRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ClaimedTaskRestRepository.java @@ -287,7 +287,7 @@ protected ClaimedTaskRest createAndReturn(Context context, List list) @Override public void afterPropertiesSet() throws Exception { discoverableEndpointsService.register(this, Arrays.asList( - new Link("/api/" + ClaimedTaskRest.CATEGORY + "/" + ClaimedTaskRest.PLURAL_NAME + "/search", + Link.of("/api/" + ClaimedTaskRest.CATEGORY + "/" + ClaimedTaskRest.PLURAL_NAME + "/search", ClaimedTaskRest.PLURAL_NAME + "-search"))); } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/DSpaceRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/DSpaceRestRepository.java index 5fa86824b100..a6cec2601546 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/DSpaceRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/DSpaceRestRepository.java @@ -212,6 +212,14 @@ public void deleteAll(Iterable entities) { } + @Override + /** + * Deletes all instances of the type T with the given IDs. + */ + public void deleteAllById(Iterable ids) { + // TODO Auto-generated method stub + } + @Override /** * Method to implement to support bulk delete of ALL entity instances diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java index a1cd6dd25fb3..cfae12584ff7 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java @@ -336,6 +336,6 @@ public Class getDomainClass() { @Override public void afterPropertiesSet() throws Exception { discoverableEndpointsService.register(this, Arrays.asList( - new Link("/api/" + EPersonRest.CATEGORY + "/registrations", EPersonRest.NAME + "-registration"))); + Link.of("/api/" + EPersonRest.CATEGORY + "/registrations", EPersonRest.NAME + "-registration"))); } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/PoolTaskRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/PoolTaskRestRepository.java index 22ebfe1610e3..da5253608e65 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/PoolTaskRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/PoolTaskRestRepository.java @@ -131,7 +131,7 @@ public Page findAll(Context context, Pageable pageable) { @Override public void afterPropertiesSet() throws Exception { discoverableEndpointsService.register(this, Arrays.asList( - new Link("/api/" + PoolTaskRest.CATEGORY + "/" + PoolTaskRest.PLURAL_NAME + "/search", + Link.of("/api/" + PoolTaskRest.CATEGORY + "/" + PoolTaskRest.PLURAL_NAME + "/search", PoolTaskRest.PLURAL_NAME + "-search"))); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResourcePolicyRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResourcePolicyRestRepository.java index 7e0f3c24583a..0b77f96b9b5f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResourcePolicyRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResourcePolicyRestRepository.java @@ -324,7 +324,7 @@ protected void patch(Context context, HttpServletRequest request, String apiCate @Override public void afterPropertiesSet() throws Exception { discoverableEndpointsService.register(this, Arrays.asList( - new Link("/api/" + ResourcePolicyRest.CATEGORY + "/" + ResourcePolicyRest.PLURAL_NAME + "/search", + Link.of("/api/" + ResourcePolicyRest.CATEGORY + "/" + ResourcePolicyRest.PLURAL_NAME + "/search", ResourcePolicyRest.PLURAL_NAME + "-search"))); } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/VocabularyEntryDetailsRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/VocabularyEntryDetailsRestRepository.java index c206721361e0..bc7c60d62b77 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/VocabularyEntryDetailsRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/VocabularyEntryDetailsRestRepository.java @@ -56,7 +56,7 @@ public class VocabularyEntryDetailsRestRepository extends DSpaceRestRepository LYRASIS - http://www.dspace.org + https://dspace.org 11 - 5.2.5.RELEASE - 2.2.6.RELEASE - 5.2.2.RELEASE + 5.3.15 + 2.6.3 + 5.6.1 5.6.5.Final 6.0.23.Final 42.3.3 @@ -47,9 +47,9 @@ - 3.2.6.RELEASE + 3.3.9.RELEASE - 2.4.0 + 2.6.0 7.9 @@ -1738,7 +1738,7 @@ com.google.guava guava - 30.1.1-jre + 31.0.1-jre @@ -1752,6 +1752,20 @@ xom 1.2.5 + + + com.jayway.jsonpath + json-path + ${json-path.version} + test + + + com.jayway.jsonpath + json-path-assert + ${json-path.version} + test + + javax.xml.bind From d8e4a569d6dbba9502ac4083abaca067dd24a407 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Tue, 21 Dec 2021 12:02:00 -0600 Subject: [PATCH 0127/1846] Fix circular reference errors in beans. Use @Lazy annotation for beans which must be loaded later / on first use. --- .../converter/AInprogressItemConverter.java | 3 + .../rest/converter/BitstreamConverter.java | 4 -- .../rest/converter/ClaimedTaskConverter.java | 3 + .../app/rest/converter/ConverterService.java | 6 +- .../rest/converter/DSpaceObjectConverter.java | 3 + .../HarvestedCollectionConverter.java | 3 + .../app/rest/converter/MetadataConverter.java | 3 + .../converter/MetadataFieldConverter.java | 3 + .../app/rest/converter/PoolTaskConverter.java | 3 + .../app/rest/converter/ProcessConverter.java | 3 + .../rest/converter/RelationshipConverter.java | 3 + .../converter/RelationshipTypeConverter.java | 3 + .../converter/ResourcePolicyConverter.java | 3 + .../SubmissionCCLicenseConverter.java | 3 + .../SubmissionCCLicenseFieldConverter.java | 3 + .../SubmissionDefinitionConverter.java | 3 + .../rest/converter/TemplateItemConverter.java | 3 + .../WorkflowDefinitionConverter.java | 3 + .../rest/converter/WorkflowStepConverter.java | 3 + .../exception/DSpaceAccessDeniedHandler.java | 8 +-- .../AbstractDSpaceRestRepository.java | 2 + .../rest/repository/DSpaceRestRepository.java | 57 ++++++++++++++----- .../security/WebSecurityConfiguration.java | 10 +++- ...JWTTokenRestAuthenticationServiceImpl.java | 8 +-- .../app/rest/submit/SubmissionService.java | 2 + .../dspace/app/rest/utils/AuthorityUtils.java | 4 ++ .../java/org/dspace/app/rest/utils/Utils.java | 3 + 27 files changed, 122 insertions(+), 33 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/AInprogressItemConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/AInprogressItemConverter.java index 1fb2bffc5819..ce7ca349180d 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/AInprogressItemConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/AInprogressItemConverter.java @@ -27,6 +27,7 @@ import org.dspace.content.Item; import org.dspace.eperson.EPerson; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; /** * Abstract implementation providing the common functionalities for all the inprogressSubmission Converter @@ -44,6 +45,8 @@ public abstract class AInprogressItemConverter { - @Autowired - ConverterService converter; - @Override public BitstreamRest convert(org.dspace.content.Bitstream obj, Projection projection) { BitstreamRest b = super.convert(obj, projection); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ClaimedTaskConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ClaimedTaskConverter.java index cc7459955f53..5238b8888a2c 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ClaimedTaskConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ClaimedTaskConverter.java @@ -14,6 +14,7 @@ import org.dspace.xmlworkflow.storedcomponents.ClaimedTask; import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; /** @@ -26,6 +27,8 @@ public class ClaimedTaskConverter implements IndexableObjectConverter { + // Must be loaded @Lazy, as ConverterService autowires all DSpaceConverter components + @Lazy @Autowired private ConverterService converter; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ConverterService.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ConverterService.java index 18fc119eefa7..0f7b47239e3f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ConverterService.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ConverterService.java @@ -38,6 +38,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.context.annotation.Lazy; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.type.filter.AssignableTypeFilter; import org.springframework.data.domain.Page; @@ -52,9 +53,12 @@ /** * Converts domain objects from the DSpace service layer to rest objects, and from rest objects to resource * objects, applying {@link Projection}s where applicable. - * + *

+ * MUST be loaded @Lazy, as this service requires other services to be preloaded (especially DSpaceConverter components) + * and that can result in circular references if those services need this ConverterService (and many do). * @author Luca Giamminonni (luca.giamminonni at 4science dot it) */ +@Lazy @Service public class ConverterService { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/DSpaceObjectConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/DSpaceObjectConverter.java index 0c730265ef0e..a89d5ec4e9b4 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/DSpaceObjectConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/DSpaceObjectConverter.java @@ -24,6 +24,7 @@ import org.dspace.core.Context; import org.dspace.services.RequestService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; /** * This is the base converter from/to objects in the DSpace API data model and @@ -38,6 +39,8 @@ public abstract class DSpaceObjectConverter { + // Must be loaded @Lazy, as ConverterService autowires all DSpaceConverter components + @Lazy @Autowired private ConverterService converter; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/MetadataConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/MetadataConverter.java index 14189a440108..76aca4be231d 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/MetadataConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/MetadataConverter.java @@ -28,6 +28,7 @@ import org.dspace.content.service.DSpaceObjectService; import org.dspace.core.Context; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; /** @@ -39,6 +40,8 @@ public class MetadataConverter implements DSpaceConverter { + // Must be loaded @Lazy, as ConverterService autowires all DSpaceConverter components + @Lazy @Autowired private ConverterService converter; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/PoolTaskConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/PoolTaskConverter.java index 48299dd362ee..548dc637533d 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/PoolTaskConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/PoolTaskConverter.java @@ -13,6 +13,7 @@ import org.dspace.xmlworkflow.storedcomponents.PoolTask; import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; /** @@ -25,6 +26,8 @@ public class PoolTaskConverter implements IndexableObjectConverter { + // Must be loaded @Lazy, as ConverterService autowires all DSpaceConverter components + @Lazy @Autowired private ConverterService converter; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ProcessConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ProcessConverter.java index 4a794c9b8533..de03d6063019 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ProcessConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ProcessConverter.java @@ -15,6 +15,7 @@ import org.dspace.scripts.Process; import org.dspace.scripts.service.ProcessService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; /** @@ -23,6 +24,8 @@ @Component public class ProcessConverter implements DSpaceConverter { + // Must be loaded @Lazy, as ConverterService autowires all DSpaceConverter components + @Lazy @Autowired private ConverterService converter; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/RelationshipConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/RelationshipConverter.java index f9d0cf52ec31..1459db1a9459 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/RelationshipConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/RelationshipConverter.java @@ -11,6 +11,7 @@ import org.dspace.app.rest.projection.Projection; import org.dspace.content.Relationship; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; /** @@ -20,6 +21,8 @@ @Component public class RelationshipConverter implements DSpaceConverter { + // Must be loaded @Lazy, as ConverterService autowires all DSpaceConverter components + @Lazy @Autowired private ConverterService converter; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/RelationshipTypeConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/RelationshipTypeConverter.java index c20249c025d2..44a5eb3d136c 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/RelationshipTypeConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/RelationshipTypeConverter.java @@ -11,6 +11,7 @@ import org.dspace.app.rest.projection.Projection; import org.dspace.content.RelationshipType; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; /** @@ -20,6 +21,8 @@ @Component public class RelationshipTypeConverter implements DSpaceConverter { + // Must be loaded @Lazy, as ConverterService autowires all DSpaceConverter components + @Lazy @Autowired private ConverterService converter; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ResourcePolicyConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ResourcePolicyConverter.java index ab8694874c61..6b233fed8bdf 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ResourcePolicyConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ResourcePolicyConverter.java @@ -12,6 +12,7 @@ import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.service.ResourcePolicyService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; /** @@ -26,6 +27,8 @@ public class ResourcePolicyConverter implements DSpaceConverter { + // Must be loaded @Lazy, as ConverterService autowires all DSpaceConverter components + @Lazy @Autowired private ConverterService converter; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseFieldConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseFieldConverter.java index 782056dc1c85..70a1a4d76c34 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseFieldConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseFieldConverter.java @@ -16,6 +16,7 @@ import org.dspace.license.CCLicenseField; import org.dspace.license.CCLicenseFieldEnum; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; /** @@ -27,6 +28,8 @@ public class SubmissionCCLicenseFieldConverter implements DSpaceConverter { + // Must be loaded @Lazy, as ConverterService autowires all DSpaceConverter components + @Lazy @Autowired private ConverterService converter; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionDefinitionConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionDefinitionConverter.java index f380a6695ff3..eccd9cba41eb 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionDefinitionConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionDefinitionConverter.java @@ -27,6 +27,7 @@ import org.dspace.core.Context; import org.dspace.services.RequestService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; /** @@ -47,6 +48,8 @@ public class SubmissionDefinitionConverter implements DSpaceConverter { + // Must be loaded @Lazy, as ConverterService autowires all DSpaceConverter components + @Lazy @Autowired ConverterService converter; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/WorkflowDefinitionConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/WorkflowDefinitionConverter.java index 04af851e8b6c..e7f322a20c57 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/WorkflowDefinitionConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/WorkflowDefinitionConverter.java @@ -15,6 +15,7 @@ import org.dspace.xmlworkflow.factory.XmlWorkflowFactory; import org.dspace.xmlworkflow.state.Workflow; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; /** @@ -28,6 +29,8 @@ public class WorkflowDefinitionConverter implements DSpaceConverter { + // Must be loaded @Lazy, as ConverterService autowires all DSpaceConverter components + @Lazy @Autowired ConverterService converter; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceAccessDeniedHandler.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceAccessDeniedHandler.java index 9e20eca4c287..c2842d33d2bc 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceAccessDeniedHandler.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceAccessDeniedHandler.java @@ -12,9 +12,9 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.dspace.app.rest.security.WebSecurityConfiguration; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Lazy; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.csrf.CsrfToken; @@ -40,8 +40,9 @@ @Component public class DSpaceAccessDeniedHandler implements AccessDeniedHandler { + @Lazy @Autowired - private WebSecurityConfiguration webSecurityConfiguration; + private CsrfTokenRepository csrfTokenRepository; @Autowired @Qualifier("handlerExceptionResolver") @@ -69,9 +70,6 @@ public void handle(HttpServletRequest request, HttpServletResponse response, Acc // switched clients (from HAL Browser to UI or visa versa) and has an out-of-sync token. // NOTE: this logic is tested in AuthenticationRestControllerIT.testRefreshTokenWithInvalidCSRF() if (ex instanceof InvalidCsrfTokenException) { - // Get access to our enabled CSRF token repository - CsrfTokenRepository csrfTokenRepository = webSecurityConfiguration.getCsrfTokenRepository(); - // Remove current token & generate a new one csrfTokenRepository.saveToken(null, request, response); CsrfToken newToken = csrfTokenRepository.generateToken(request); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AbstractDSpaceRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AbstractDSpaceRestRepository.java index f835098d55d9..0ccfa5249f89 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AbstractDSpaceRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AbstractDSpaceRestRepository.java @@ -14,6 +14,7 @@ import org.dspace.services.RequestService; import org.dspace.utils.DSpace; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; /** * This is the base class for any Rest Repository. It provides utility method to @@ -26,6 +27,7 @@ public abstract class AbstractDSpaceRestRepository { @Autowired protected Utils utils; + @Lazy @Autowired protected ConverterService converter; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/DSpaceRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/DSpaceRestRepository.java index a6cec2601546..01f127eca5ac 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/DSpaceRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/DSpaceRestRepository.java @@ -22,12 +22,15 @@ import org.dspace.app.rest.exception.RESTAuthorizationException; import org.dspace.app.rest.exception.RepositoryMethodNotImplementedException; import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.ItemRest; import org.dspace.app.rest.model.RestAddressableModel; import org.dspace.app.rest.model.patch.Patch; import org.dspace.authorize.AuthorizeException; import org.dspace.content.service.MetadataFieldService; import org.dspace.core.Context; +import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -45,20 +48,44 @@ */ public abstract class DSpaceRestRepository extends AbstractDSpaceRestRepository - implements PagingAndSortingRepository { + implements PagingAndSortingRepository, BeanNameAware { private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(DSpaceRestRepository.class); - //Trick to make inner-calls to ourselves that are checked by Spring security - //See: - // https://stackoverflow.com/questions/13564627/spring-aop-not-working-for-method-call-inside-another-method - // https://docs.spring.io/spring/docs/4.3.18.RELEASE/spring-framework-reference/htmlsingle/#aop-understanding-aop-proxies - @Autowired + private String thisRepositoryBeanName; private DSpaceRestRepository thisRepository; + @Autowired + private ApplicationContext applicationContext; + @Autowired private MetadataFieldService metadataFieldService; + /** + * From BeanNameAware. Allows us to capture the name of the bean, so we can load it into thisRepository. + * See getThisRepository() method. + * @param beanName name of ourselves + */ + @Override + public void setBeanName(String beanName) { + this.thisRepositoryBeanName = beanName; + } + + /** + * Get access to our current DSpaceRestRepository bean. This is a trick to make inner-calls to ourselves that are + * checked by Spring Security + * See: + * https://stackoverflow.com/questions/13564627/spring-aop-not-working-for-method-call-inside-another-method + * https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop-understanding-aop-proxies + * @return current DSpaceRestRepository + */ + private DSpaceRestRepository getThisRepository() { + if (thisRepository == null) { + thisRepository = (DSpaceRestRepository) applicationContext.getBean(thisRepositoryBeanName); + } + return thisRepository; + } + @Override public S save(S entity) { Context context = null; @@ -108,7 +135,7 @@ public Iterable saveAll(Iterable entities) { */ public Optional findById(ID id) { Context context = obtainContext(); - final T object = thisRepository.findOne(context, id); + final T object = getThisRepository().findOne(context, id); if (object == null) { return Optional.empty(); } else { @@ -171,7 +198,7 @@ public long count() { public void deleteById(ID id) { Context context = obtainContext(); try { - thisRepository.delete(context, id); + getThisRepository().delete(context, id); context.commit(); } catch (AuthorizeException e) { throw new RESTAuthorizationException(e); @@ -243,7 +270,7 @@ public final Iterable findAll(Sort sort) { */ public Page findAll(Pageable pageable) { Context context = obtainContext(); - return thisRepository.findAll(context, pageable); + return getThisRepository().findAll(context, pageable); } /** @@ -271,7 +298,7 @@ public T createAndReturn() { Context context = null; try { context = obtainContext(); - T entity = thisRepository.createAndReturn(context); + T entity = getThisRepository().createAndReturn(context); context.commit(); return entity; } catch (AuthorizeException e) { @@ -292,7 +319,7 @@ public T createAndReturn(UUID uuid) { Context context = null; try { context = obtainContext(); - T entity = thisRepository.createAndReturn(context, uuid); + T entity = getThisRepository().createAndReturn(context, uuid); context.commit(); return entity; } catch (AuthorizeException e) { @@ -313,7 +340,7 @@ public T createAndReturn(List list) { Context context = null; try { context = obtainContext(); - T entity = thisRepository.createAndReturn(context, list); + T entity = getThisRepository().createAndReturn(context, list); context.commit(); return entity; } catch (AuthorizeException e) { @@ -414,7 +441,7 @@ public T patch(HttpServletRequest request, String apiCategory, String model, ID throws UnprocessableEntityException, DSpaceBadRequestException { Context context = obtainContext(); try { - thisRepository.patch(context, request, apiCategory, model, id, patch); + getThisRepository().patch(context, request, apiCategory, model, id, patch); context.commit(); } catch (AuthorizeException ae) { throw new RESTAuthorizationException(ae); @@ -515,7 +542,7 @@ protected Iterable upload(Context context, HttpServletRequest request, public T put(HttpServletRequest request, String apiCategory, String model, ID uuid, JsonNode jsonNode) { Context context = obtainContext(); try { - thisRepository.put(context, request, apiCategory, model, uuid, jsonNode); + getThisRepository().put(context, request, apiCategory, model, uuid, jsonNode); context.commit(); } catch (SQLException e) { throw new RuntimeException("Unable to update DSpace object " + model + " with id=" + uuid, e); @@ -540,7 +567,7 @@ public T put(HttpServletRequest request, String apiCategory, String model, ID id List stringList) { Context context = obtainContext(); try { - thisRepository.put(context, request, apiCategory, model, id, stringList); + getThisRepository().put(context, request, apiCategory, model, id, stringList); context.commit(); } catch (SQLException | AuthorizeException e) { throw new RuntimeException(e.getMessage(), e); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java index 38e8c1d7f4db..23e6356216b3 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java @@ -13,7 +13,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; @@ -102,7 +104,7 @@ protected void configure(HttpSecurity http) throws Exception { // (both are defined below as methods). // While we primarily use JWT in headers, CSRF protection is needed because we also support JWT via Cookies .csrf() - .csrfTokenRepository(this.getCsrfTokenRepository()) + .csrfTokenRepository(this.csrfTokenRepository()) .sessionAuthenticationStrategy(this.sessionAuthenticationStrategy()) .and() .exceptionHandling() @@ -168,7 +170,9 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception { * * @return CsrfTokenRepository as described above */ - public CsrfTokenRepository getCsrfTokenRepository() { + @Lazy + @Bean + public CsrfTokenRepository csrfTokenRepository() { return new DSpaceCsrfTokenRepository(); } @@ -177,7 +181,7 @@ public CsrfTokenRepository getCsrfTokenRepository() { * is only refreshed when it is used (or attempted to be used) by the client. */ private SessionAuthenticationStrategy sessionAuthenticationStrategy() { - return new DSpaceCsrfAuthenticationStrategy(getCsrfTokenRepository()); + return new DSpaceCsrfAuthenticationStrategy(csrfTokenRepository()); } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java index 5c685f61afed..c28729ff83a8 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java @@ -21,7 +21,6 @@ import org.dspace.app.rest.model.wrapper.AuthenticationToken; import org.dspace.app.rest.security.DSpaceAuthentication; import org.dspace.app.rest.security.RestAuthenticationService; -import org.dspace.app.rest.security.WebSecurityConfiguration; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.authenticate.AuthenticationMethod; import org.dspace.authenticate.service.AuthenticationService; @@ -32,6 +31,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.http.ResponseCookie; import org.springframework.security.web.csrf.CsrfToken; import org.springframework.security.web.csrf.CsrfTokenRepository; @@ -65,8 +65,9 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication @Autowired private AuthenticationService authenticationService; + @Lazy @Autowired - private WebSecurityConfiguration webSecurityConfiguration; + private CsrfTokenRepository csrfTokenRepository; @Override public void afterPropertiesSet() throws Exception { @@ -331,9 +332,6 @@ private boolean hasAuthorizationCookie(HttpServletRequest request) { * @param response current response */ private void resetCSRFToken(HttpServletRequest request, HttpServletResponse response) { - // Get access to our enabled CSRF token repository - CsrfTokenRepository csrfTokenRepository = webSecurityConfiguration.getCsrfTokenRepository(); - // Remove current CSRF token & generate a new one // We do this as we want the token to change anytime you login or logout csrfTokenRepository.saveToken(null, request, response); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/SubmissionService.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/SubmissionService.java index 8eb1c1c11965..6f3cd62203e2 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/SubmissionService.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/SubmissionService.java @@ -63,6 +63,7 @@ import org.dspace.workflow.WorkflowService; import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.data.rest.webmvc.json.patch.PatchException; import org.springframework.jdbc.datasource.init.UncategorizedScriptException; import org.springframework.stereotype.Component; @@ -94,6 +95,7 @@ public class SubmissionService { protected CreativeCommonsService creativeCommonsService; @Autowired private RequestService requestService; + @Lazy @Autowired private ConverterService converter; @Autowired diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/AuthorityUtils.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/AuthorityUtils.java index 1a2a071fe1b0..dabb0448a9bf 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/AuthorityUtils.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/AuthorityUtils.java @@ -17,6 +17,7 @@ import org.dspace.content.authority.ChoiceAuthority; import org.dspace.content.authority.service.ChoiceAuthorityService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; /** @@ -38,6 +39,9 @@ public class AuthorityUtils { @Autowired private ChoiceAuthorityService cas; + // Lazy load required so that AuthorityUtils can be used from DSpaceConverter components + // (because ConverterService autowires all DSpaceConverter components) + @Lazy @Autowired private ConverterService converter; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/Utils.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/Utils.java index 99c350214c83..8e1bb37005f6 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/Utils.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/Utils.java @@ -84,6 +84,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Lazy; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.convert.ConversionService; import org.springframework.data.domain.Page; @@ -131,6 +132,8 @@ public class Utils { @Autowired private BitstreamFormatService bitstreamFormatService; + // Must be loaded @Lazy, as ConverterService also autowires Utils + @Lazy @Autowired private ConverterService converter; From 24f674d92b24d1e42c3bae83701fdf88ebc5e5e9 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Tue, 21 Dec 2021 12:03:26 -0600 Subject: [PATCH 0128/1846] Minor fix to test. ContentType now includes charset. --- .../src/test/java/org/dspace/app/oai/OAIpmhIT.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/oai/OAIpmhIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/oai/OAIpmhIT.java index 052c363771c4..2c50257ec9c7 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/oai/OAIpmhIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/oai/OAIpmhIT.java @@ -153,8 +153,8 @@ public void requestForIdentifyShouldReturnTheConfiguredValues() throws Exception getClient().perform(get(DEFAULT_CONTEXT).param("verb", "Identify")) // Expect a 200 response code .andExpect(status().isOk()) - // Expect the content type to be "text/xml" - .andExpect(content().contentType("text/xml")) + // Expect the content type to be "text/xml;charset=UTF-8" + .andExpect(content().contentType("text/xml;charset=UTF-8")) // Expect oai .andExpect(xpath("OAI-PMH/Identify/description/oai-identifier/scheme").string("oai")) // Expect protocol version 2.0 From 7d396434c271c678d616152d82eb05bdd1189449 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 16 Feb 2022 13:44:49 -0600 Subject: [PATCH 0129/1846] Fix UTF-8 encoding issues in Spring Boot by updating config names --- .../src/main/resources/application.properties | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/dspace-server-webapp/src/main/resources/application.properties b/dspace-server-webapp/src/main/resources/application.properties index 91e2aa73ae9a..5c1790f0310e 100644 --- a/dspace-server-webapp/src/main/resources/application.properties +++ b/dspace-server-webapp/src/main/resources/application.properties @@ -60,11 +60,9 @@ spring.messages.encoding=UTF-8 # # # Charset of HTTP requests and responses. Added to the "Content-Type" header if not set explicitly. -spring.http.encoding.charset=UTF-8 -# Enable http encoding support. -spring.http.encoding.enabled=true +server.servlet.encoding.charset=UTF-8 # Force the encoding to the configured charset on HTTP requests and responses. -spring.http.encoding.force=true +server.servlet.encoding.force=true ########################### # Server Properties From 5ea14a86370d16522f11fc7e86f1f89365915c5d Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 16 Feb 2022 17:08:14 -0600 Subject: [PATCH 0130/1846] Add missing UTF-8 charset to tests --- .../app/opensearch/OpenSearchControllerDisabledIT.java | 6 +++--- .../org/dspace/app/rest/BitstreamRestControllerIT.java | 8 ++++---- .../java/org/dspace/app/rest/SitemapRestControllerIT.java | 8 ++++---- .../dspace/app/rest/WorkspaceItemRestRepositoryIT.java | 4 ++-- .../app/rest/test/AbstractControllerIntegrationTest.java | 4 +++- 5 files changed, 16 insertions(+), 14 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/opensearch/OpenSearchControllerDisabledIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/opensearch/OpenSearchControllerDisabledIT.java index a757ecbca39b..6132544c61e6 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/opensearch/OpenSearchControllerDisabledIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/opensearch/OpenSearchControllerDisabledIT.java @@ -40,8 +40,8 @@ public void searchTest() throws Exception { .param("query", "dog")) //The status has to be 404 Not Found .andExpect(status().isNotFound()) - //We expect the content type to be "application/html" - .andExpect(content().contentType("text/html")) + //We expect the content type to be "text/html" + .andExpect(content().contentType("text/html;charset=UTF-8")) .andExpect(content().string("OpenSearch Service is disabled")) ; } @@ -52,7 +52,7 @@ public void serviceDocumentTest() throws Exception { getClient().perform(get("/opensearch/service")) //The status has to be 404 Not Found .andExpect(status().isNotFound()) - .andExpect(content().contentType("text/html")) + .andExpect(content().contentType("text/html;charset=UTF-8")) .andExpect(content().string("OpenSearch Service is disabled")) ; } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java index efb7d1776dd4..ac9c0d2ed617 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java @@ -203,7 +203,7 @@ public void retrieveFullBitstream() throws Exception { // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag .andExpect(header().string("ETag", "\"" + bitstream.getChecksum() + "\"")) //We expect the content type to match the bitstream mime type - .andExpect(content().contentType("text/plain")) + .andExpect(content().contentType("text/plain;charset=UTF-8")) //THe bytes of the content must match the original content .andExpect(content().bytes(bitstreamContent.getBytes())); @@ -265,7 +265,7 @@ public void retrieveRangeBitstream() throws Exception { //The response should give us details about the range .andExpect(header().string("Content-Range", "bytes 1-3/10")) //We expect the content type to match the bitstream mime type - .andExpect(content().contentType("text/plain")) + .andExpect(content().contentType("text/plain;charset=UTF-8")) //We only expect the bytes 1, 2 and 3 .andExpect(content().bytes("123".getBytes())); @@ -286,7 +286,7 @@ public void retrieveRangeBitstream() throws Exception { //The response should give us details about the range .andExpect(header().string("Content-Range", "bytes 4-9/10")) //We expect the content type to match the bitstream mime type - .andExpect(content().contentType("text/plain")) + .andExpect(content().contentType("text/plain;charset=UTF-8")) //We all remaining bytes, starting at byte 4 .andExpect(content().bytes("456789".getBytes())); @@ -827,7 +827,7 @@ public void retrieveCitationCoverpageOfBitstream() throws Exception { //The ETag has to be based on the checksum .andExpect(header().string("ETag", "\"" + bitstream.getChecksum() + "\"")) //We expect the content type to match the bitstream mime type - .andExpect(content().contentType("application/pdf")) + .andExpect(content().contentType("application/pdf;charset=UTF-8")) //THe bytes of the content must match the original content .andReturn().getResponse().getContentAsByteArray(); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SitemapRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SitemapRestControllerIT.java index fd84aa023b9b..cbcf970547f7 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SitemapRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SitemapRestControllerIT.java @@ -107,7 +107,7 @@ public void testSitemap_sitemapIndexHtml() throws Exception { //** THEN ** .andExpect(status().isOk()) //We expect the content type to match - .andExpect(content().contentType("text/html")) + .andExpect(content().contentType("text/html;charset=UTF-8")) .andReturn(); String response = result.getResponse().getContentAsString(); @@ -123,7 +123,7 @@ public void testSitemap_sitemap0Html() throws Exception { //** THEN ** .andExpect(status().isOk()) //We expect the content type to match - .andExpect(content().contentType("text/html")) + .andExpect(content().contentType("text/html;charset=UTF-8")) .andReturn(); String response = result.getResponse().getContentAsString(); @@ -140,7 +140,7 @@ public void testSitemap_sitemapIndexXml() throws Exception { //** THEN ** .andExpect(status().isOk()) //We expect the content type to match - .andExpect(content().contentType("application/xml")) + .andExpect(content().contentType("application/xml;charset=UTF-8")) .andReturn(); String response = result.getResponse().getContentAsString(); @@ -156,7 +156,7 @@ public void testSitemap_sitemap0Xml() throws Exception { //** THEN ** .andExpect(status().isOk()) //We expect the content type to match - .andExpect(content().contentType("application/xml")) + .andExpect(content().contentType("application/xml;charset=UTF-8")) .andReturn(); String response = result.getResponse().getContentAsString(); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java index 6fa73ddc9e02..199313a31441 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java @@ -6976,7 +6976,7 @@ public void affterAddingAccessConditionBitstreamMustBeDownloadableTest() throws .andExpect(status().isOk()) .andExpect(header().string("Accept-Ranges", "bytes")) .andExpect(header().string("ETag", "\"" + bitstream.getChecksum() + "\"")) - .andExpect(content().contentType("text/plain")) + .andExpect(content().contentType("text/plain;charset=UTF-8")) .andExpect(content().bytes(bitstreamContent.getBytes())); // others can't download the bitstream @@ -7014,7 +7014,7 @@ public void affterAddingAccessConditionBitstreamMustBeDownloadableTest() throws .andExpect(status().isOk()) .andExpect(header().string("Accept-Ranges", "bytes")) .andExpect(header().string("ETag", "\"" + bitstream.getChecksum() + "\"")) - .andExpect(content().contentType("text/plain")) + .andExpect(content().contentType("text/plain;charset=UTF-8")) .andExpect(content().bytes(bitstreamContent.getBytes())); // others can't download the bitstream diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/test/AbstractControllerIntegrationTest.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/test/AbstractControllerIntegrationTest.java index a08fd625c49a..00339ba2e482 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/test/AbstractControllerIntegrationTest.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/test/AbstractControllerIntegrationTest.java @@ -13,6 +13,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; +import java.nio.charset.StandardCharsets; import java.sql.SQLException; import java.util.Arrays; import java.util.List; @@ -89,8 +90,9 @@ public class AbstractControllerIntegrationTest extends AbstractIntegrationTestWi public static final String REST_SERVER_URL = "http://localhost/api/"; public static final String BASE_REST_SERVER_URL = "http://localhost"; + // Our standard/expected content type protected MediaType contentType = new MediaType(MediaTypes.HAL_JSON.getType(), - MediaTypes.HAL_JSON.getSubtype()); + MediaTypes.HAL_JSON.getSubtype(), StandardCharsets.UTF_8); protected MediaType textUriContentType = RestMediaTypes.TEXT_URI_LIST; From 9e25e1d43e215c208f1ccb0d0c24856615530b9c Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 18 Feb 2022 14:04:40 -0600 Subject: [PATCH 0131/1846] Remove duplicative slash "/" in RequestMapping which caused test failures --- .../org/dspace/app/rest/BundleUploadBitstreamController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/BundleUploadBitstreamController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/BundleUploadBitstreamController.java index a0f5d5f71e40..0cb6bc47e033 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/BundleUploadBitstreamController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/BundleUploadBitstreamController.java @@ -59,7 +59,7 @@ * */ @RestController -@RequestMapping("/api/" + BundleRest.CATEGORY + "/" + BundleRest.PLURAL_NAME + "/" +@RequestMapping("/api/" + BundleRest.CATEGORY + "/" + BundleRest.PLURAL_NAME + REGEX_REQUESTMAPPING_IDENTIFIER_AS_UUID + "/" + BitstreamRest.PLURAL_NAME) public class BundleUploadBitstreamController { From 0311aeb45c3561a37e2248cc98e81da76b815f37 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 18 Feb 2022 16:28:32 -0600 Subject: [PATCH 0132/1846] Ensure files are not required for scripts endpoint. Tests already prove they are not required, but not set that way in controller. --- .../java/org/dspace/app/rest/ScriptProcessesController.java | 6 ++++-- .../dspace/app/rest/repository/ScriptRestRepository.java | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/ScriptProcessesController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ScriptProcessesController.java index 1197e13c98c3..196cade5dd51 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/ScriptProcessesController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ScriptProcessesController.java @@ -55,13 +55,15 @@ public class ScriptProcessesController { * This method can be called by sending a POST request to the system/scripts/{name}/processes endpoint * This will start a process for the script that matches the given name * @param scriptName The name of the script that we want to start a process for + * @param files (Optional) any files that need to be passed to the script for it to run * @return The ProcessResource object for the created process * @throws Exception If something goes wrong */ @RequestMapping(method = RequestMethod.POST) @PreAuthorize("hasAuthority('ADMIN')") - public ResponseEntity> startProcess(@PathVariable(name = "name") String scriptName, - @RequestParam(name = "file") List files) + public ResponseEntity> startProcess( + @PathVariable(name = "name") String scriptName, + @RequestParam(name = "file", required = false) List files) throws Exception { if (log.isTraceEnabled()) { log.trace("Starting Process for Script with name: " + scriptName); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ScriptRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ScriptRestRepository.java index eb2afc0e5490..096eeedb9eb9 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ScriptRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ScriptRestRepository.java @@ -147,8 +147,10 @@ private void runDSpaceScript(List files, Context context, ScriptC DSpaceRunnable dSpaceRunnable = scriptService.createDSpaceRunnableForScriptConfiguration(scriptToExecute); try { dSpaceRunnable.initialize(args.toArray(new String[0]), restDSpaceRunnableHandler, context.getCurrentUser()); - checkFileNames(dSpaceRunnable, files); - processFiles(context, restDSpaceRunnableHandler, files); + if (files != null && !files.isEmpty()) { + checkFileNames(dSpaceRunnable, files); + processFiles(context, restDSpaceRunnableHandler, files); + } restDSpaceRunnableHandler.schedule(dSpaceRunnable); } catch (ParseException e) { dSpaceRunnable.printHelp(); From 51591047d2640d1a94ff2a7138c2719e5979443f Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 18 Feb 2022 16:41:30 -0600 Subject: [PATCH 0133/1846] All multipart/form-data ITs should use multipart(), not post() or fileUpload() --- .../BundleUploadBitstreamControllerIT.java | 14 +++--- .../app/rest/CollectionLogoControllerIT.java | 12 ++--- .../app/rest/CommunityLogoControllerIT.java | 12 ++--- .../app/rest/ScriptRestRepositoryIT.java | 36 ++++++------- .../app/rest/TaskRestRepositoriesIT.java | 8 +-- .../rest/WorkspaceItemRestRepositoryIT.java | 50 +++++++++---------- .../org/dspace/app/rest/csv/CsvExportIT.java | 12 ++--- .../org/dspace/app/rest/csv/CsvImportIT.java | 8 +-- .../org/dspace/curate/CurationScriptIT.java | 47 +++++++---------- 9 files changed, 91 insertions(+), 108 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BundleUploadBitstreamControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BundleUploadBitstreamControllerIT.java index eefcb81656df..f80b194ca6ec 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BundleUploadBitstreamControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BundleUploadBitstreamControllerIT.java @@ -103,7 +103,7 @@ public void uploadBitstreamAllPossibleFieldsProperties() throws Exception { context.restoreAuthSystemState(); MvcResult mvcResult = getClient(token).perform( - MockMvcRequestBuilders.fileUpload("/api/core/bundles/" + bundle.getID() + "/bitstreams") + MockMvcRequestBuilders.multipart("/api/core/bundles/" + bundle.getID() + "/bitstreams") .file(file) .param("properties", mapper .writeValueAsString(bitstreamRest))) @@ -167,7 +167,7 @@ public void uploadBitstreamNoProperties() throws Exception { input.getBytes()); context.restoreAuthSystemState(); MvcResult mvcResult = getClient(token) - .perform(MockMvcRequestBuilders.fileUpload("/api/core/bundles/" + bundle.getID() + "/bitstreams") + .perform(MockMvcRequestBuilders.multipart("/api/core/bundles/" + bundle.getID() + "/bitstreams") .file(file)) .andExpect(status().isCreated()) .andExpect(jsonPath("$.bundleName", is("TESTINGBUNDLE"))) @@ -223,7 +223,7 @@ public void uploadBitstreamNoPropertiesUserWithItemAddAndWriteRights() throws Ex context.restoreAuthSystemState(); MvcResult mvcResult = getClient(token) - .perform(MockMvcRequestBuilders.fileUpload("/api/core/bundles/" + bundle.getID() + "/bitstreams") + .perform(MockMvcRequestBuilders.multipart("/api/core/bundles/" + bundle.getID() + "/bitstreams") .file(file)) .andExpect(status().isCreated()) .andExpect(jsonPath("$.uuid", notNullValue())).andReturn(); @@ -273,7 +273,7 @@ public void uploadBitstreamNoRights() throws Exception { context.restoreAuthSystemState(); getClient(token).perform(MockMvcRequestBuilders - .fileUpload("/api/core/bundles/" + bundle.getID() + "/bitstreams") + .multipart("/api/core/bundles/" + bundle.getID() + "/bitstreams") .file(file)) .andExpect(status().isForbidden()); @@ -315,7 +315,7 @@ public void uploadBitstreamAnonymous() throws Exception { input.getBytes()); context.restoreAuthSystemState(); - getClient().perform(MockMvcRequestBuilders.fileUpload("/api/core/bundles/" + bundle.getID() + "/bitstreams") + getClient().perform(MockMvcRequestBuilders.multipart("/api/core/bundles/" + bundle.getID() + "/bitstreams") .file(file)) .andExpect(status().isUnauthorized()); @@ -367,7 +367,7 @@ public void uploadBitstreamMinimalProperties() throws Exception { context.restoreAuthSystemState(); MvcResult mvcResult = getClient(token) - .perform(MockMvcRequestBuilders.fileUpload("/api/core/bundles/" + bundle.getID() + "/bitstreams") + .perform(MockMvcRequestBuilders.multipart("/api/core/bundles/" + bundle.getID() + "/bitstreams") .file(file) .param("properties", mapper .writeValueAsString(bitstreamRest))) @@ -422,7 +422,7 @@ public void uploadBitstreamMultipleFiles() throws Exception { input.getBytes()); context.restoreAuthSystemState(); getClient(token) - .perform(MockMvcRequestBuilders.fileUpload("/api/core/bundles/" + bundle.getID() + "/bitstreams") + .perform(MockMvcRequestBuilders.multipart("/api/core/bundles/" + bundle.getID() + "/bitstreams") .file(file).file(file2)) .andExpect(status().isUnprocessableEntity()); } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CollectionLogoControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CollectionLogoControllerIT.java index f093156000b5..fa0732f6b774 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CollectionLogoControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CollectionLogoControllerIT.java @@ -51,7 +51,7 @@ public void createStructure() throws Exception { private String createLogoInternal() throws Exception { MvcResult mvcPostResult = getClient(adminAuthToken).perform( - MockMvcRequestBuilders.fileUpload(getLogoUrlTemplate(childCollection.getID().toString())) + MockMvcRequestBuilders.multipart(getLogoUrlTemplate(childCollection.getID().toString())) .file(bitstreamFile)) .andExpect(status().isCreated()) .andReturn(); @@ -64,7 +64,7 @@ private String createLogoInternal() throws Exception { @Test public void createLogoNotLoggedIn() throws Exception { getClient().perform( - MockMvcRequestBuilders.fileUpload(getLogoUrlTemplate(childCollection.getID().toString())) + MockMvcRequestBuilders.multipart(getLogoUrlTemplate(childCollection.getID().toString())) .file(bitstreamFile)) .andExpect(status().isUnauthorized()); } @@ -88,7 +88,7 @@ public void createLogo() throws Exception { public void createLogoNoRights() throws Exception { String userToken = getAuthToken(eperson.getEmail(), password); getClient(userToken).perform( - MockMvcRequestBuilders.fileUpload(getLogoUrlTemplate(childCollection.getID().toString())) + MockMvcRequestBuilders.multipart(getLogoUrlTemplate(childCollection.getID().toString())) .file(bitstreamFile)) .andExpect(status().isForbidden()); } @@ -96,12 +96,12 @@ public void createLogoNoRights() throws Exception { @Test public void createDuplicateLogo() throws Exception { getClient(adminAuthToken).perform( - MockMvcRequestBuilders.fileUpload(getLogoUrlTemplate(childCollection.getID().toString())) + MockMvcRequestBuilders.multipart(getLogoUrlTemplate(childCollection.getID().toString())) .file(bitstreamFile)) .andExpect(status().isCreated()); getClient(adminAuthToken).perform( - MockMvcRequestBuilders.fileUpload(getLogoUrlTemplate(childCollection.getID().toString())) + MockMvcRequestBuilders.multipart(getLogoUrlTemplate(childCollection.getID().toString())) .file(bitstreamFile)) .andExpect(status().isUnprocessableEntity()); } @@ -109,7 +109,7 @@ public void createDuplicateLogo() throws Exception { @Test public void createLogoForNonexisting() throws Exception { getClient(adminAuthToken).perform( - MockMvcRequestBuilders.fileUpload(getLogoUrlTemplate("16a4b65b-3b3f-4ef5-8058-ef6f5a653ef9")) + MockMvcRequestBuilders.multipart(getLogoUrlTemplate("16a4b65b-3b3f-4ef5-8058-ef6f5a653ef9")) .file(bitstreamFile)) .andExpect(status().isNotFound()); } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CommunityLogoControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CommunityLogoControllerIT.java index 1d34a99dd955..3a0edc931049 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CommunityLogoControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CommunityLogoControllerIT.java @@ -46,7 +46,7 @@ public void createStructure() throws Exception { private String createLogoInternal() throws Exception { MvcResult mvcPostResult = getClient(adminAuthToken).perform( - MockMvcRequestBuilders.fileUpload(getLogoUrlTemplate(parentCommunity.getID().toString())) + MockMvcRequestBuilders.multipart(getLogoUrlTemplate(parentCommunity.getID().toString())) .file(bitstreamFile)) .andExpect(status().isCreated()) .andReturn(); @@ -59,7 +59,7 @@ private String createLogoInternal() throws Exception { @Test public void createLogoNotLoggedIn() throws Exception { getClient().perform( - MockMvcRequestBuilders.fileUpload(getLogoUrlTemplate(parentCommunity.getID().toString())) + MockMvcRequestBuilders.multipart(getLogoUrlTemplate(parentCommunity.getID().toString())) .file(bitstreamFile)) .andExpect(status().isUnauthorized()); } @@ -83,7 +83,7 @@ public void createLogo() throws Exception { public void createLogoNoRights() throws Exception { String userToken = getAuthToken(eperson.getEmail(), password); getClient(userToken).perform( - MockMvcRequestBuilders.fileUpload(getLogoUrlTemplate(parentCommunity.getID().toString())) + MockMvcRequestBuilders.multipart(getLogoUrlTemplate(parentCommunity.getID().toString())) .file(bitstreamFile)) .andExpect(status().isForbidden()); } @@ -91,12 +91,12 @@ public void createLogoNoRights() throws Exception { @Test public void createDuplicateLogo() throws Exception { getClient(adminAuthToken).perform( - MockMvcRequestBuilders.fileUpload(getLogoUrlTemplate(parentCommunity.getID().toString())) + MockMvcRequestBuilders.multipart(getLogoUrlTemplate(parentCommunity.getID().toString())) .file(bitstreamFile)) .andExpect(status().isCreated()); getClient(adminAuthToken).perform( - MockMvcRequestBuilders.fileUpload(getLogoUrlTemplate(parentCommunity.getID().toString())) + MockMvcRequestBuilders.multipart(getLogoUrlTemplate(parentCommunity.getID().toString())) .file(bitstreamFile)) .andExpect(status().isUnprocessableEntity()); } @@ -104,7 +104,7 @@ public void createDuplicateLogo() throws Exception { @Test public void createLogoForNonexisting() throws Exception { getClient(adminAuthToken).perform( - MockMvcRequestBuilders.fileUpload(getLogoUrlTemplate("16a4b65b-3b3f-4ef5-8058-ef6f5a653ef9")) + MockMvcRequestBuilders.multipart(getLogoUrlTemplate("16a4b65b-3b3f-4ef5-8058-ef6f5a653ef9")) .file(bitstreamFile)) .andExpect(status().isNotFound()); } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScriptRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScriptRestRepositoryIT.java index 9976fb4be19b..da1b5d5c0a19 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScriptRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScriptRestRepositoryIT.java @@ -12,8 +12,8 @@ import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -211,29 +211,26 @@ public void findOneScriptByInvalidNameBadRequestExceptionTest() throws Exception @Test public void postProcessNonAdminAuthorizeException() throws Exception { - String token = getAuthToken(eperson.getEmail(), password); - getClient(token).perform(post("/api/system/scripts/mock-script/processes").contentType("multipart/form-data")) + getClient(token).perform(multipart("/api/system/scripts/mock-script/processes")) .andExpect(status().isForbidden()); } @Test public void postProcessAnonymousAuthorizeException() throws Exception { - getClient().perform(post("/api/system/scripts/mock-script/processes").contentType("multipart/form-data")) + getClient().perform(multipart("/api/system/scripts/mock-script/processes")) .andExpect(status().isUnauthorized()); } @Test public void postProcessAdminWrongOptionsException() throws Exception { - - String token = getAuthToken(admin.getEmail(), password); AtomicReference idRef = new AtomicReference<>(); try { getClient(token) - .perform(post("/api/system/scripts/mock-script/processes").contentType("multipart/form-data")) + .perform(multipart("/api/system/scripts/mock-script/processes")) .andExpect(status().isAccepted()) .andExpect(jsonPath("$", is( ProcessMatcher.matchProcess("mock-script", @@ -277,9 +274,8 @@ public void postProcessAdminNoOptionsFailedStatus() throws Exception { try { getClient(token) - .perform(post("/api/system/scripts/mock-script/processes").contentType("multipart/form-data") - .param("properties", - new Gson().toJson(list))) + .perform(multipart("/api/system/scripts/mock-script/processes") + .param("properties", new Gson().toJson(list))) .andExpect(status().isAccepted()) .andExpect(jsonPath("$", is( ProcessMatcher.matchProcess("mock-script", @@ -296,8 +292,7 @@ public void postProcessAdminNoOptionsFailedStatus() throws Exception { public void postProcessNonExistingScriptNameException() throws Exception { String token = getAuthToken(admin.getEmail(), password); - getClient(token).perform(post("/api/system/scripts/mock-script-invalid/processes") - .contentType("multipart/form-data")) + getClient(token).perform(multipart("/api/system/scripts/mock-script-invalid/processes")) .andExpect(status().isBadRequest()); } @@ -323,9 +318,8 @@ public void postProcessAdminWithOptionsSuccess() throws Exception { try { getClient(token) - .perform(post("/api/system/scripts/mock-script/processes").contentType("multipart/form-data") - .param("properties", - new Gson().toJson(list))) + .perform(multipart("/api/system/scripts/mock-script/processes") + .param("properties", new Gson().toJson(list))) .andExpect(status().isAccepted()) .andExpect(jsonPath("$", is( ProcessMatcher.matchProcess("mock-script", @@ -361,9 +355,8 @@ public void postProcessAndVerifyOutput() throws Exception { try { getClient(token) - .perform(post("/api/system/scripts/mock-script/processes").contentType("multipart/form-data") - .param("properties", - new Gson().toJson(list))) + .perform(multipart("/api/system/scripts/mock-script/processes") + .param("properties", new Gson().toJson(list))) .andExpect(status().isAccepted()) .andExpect(jsonPath("$", is( ProcessMatcher.matchProcess("mock-script", @@ -468,9 +461,10 @@ public void postProcessAdminWithFileSuccess() throws Exception { try { getClient(token) - .perform(fileUpload("/api/system/scripts/mock-script/processes").file(bitstreamFile) - .param("properties", - new Gson().toJson(list))) + .perform(multipart("/api/system/scripts/mock-script/processes") + .file(bitstreamFile) + .characterEncoding("UTF-8") + .param("properties", new Gson().toJson(list))) .andExpect(status().isAccepted()) .andExpect(jsonPath("$", is( ProcessMatcher.matchProcess("mock-script", diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/TaskRestRepositoriesIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/TaskRestRepositoriesIT.java index d94a16a15dd1..613ca56fb0dd 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/TaskRestRepositoriesIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/TaskRestRepositoriesIT.java @@ -15,8 +15,8 @@ import static org.springframework.data.rest.webmvc.RestMediaTypes.TEXT_URI_LIST_VALUE; import static org.springframework.http.MediaType.parseMediaType; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; @@ -2650,7 +2650,7 @@ public void unclaimedTaskTest_Upload_EditMetadataOptionAllowed() throws Exceptio InputStream bibtex = getClass().getResourceAsStream("bibtex-test.bib"); final MockMultipartFile bibtexFile = new MockMultipartFile("file", "bibtex-test.bib", "application/x-bibtex", bibtex); - getClient(reviewer1Token).perform(fileUpload("/api/workflow/workflowitems/" + witem.getID()) + getClient(reviewer1Token).perform(multipart("/api/workflow/workflowitems/" + witem.getID()) .file(bibtexFile)) .andExpect(status().isUnprocessableEntity()); @@ -2791,7 +2791,7 @@ public void uploadTest_ClaimedTask_EditMetadataOptionNotAllowed() throws Excepti InputStream bibtex = getClass().getResourceAsStream("bibtex-test.bib"); final MockMultipartFile bibtexFile = new MockMultipartFile("file", "bibtex-test.bib", "application/x-bibtex", bibtex); - getClient(authToken).perform(fileUpload("/api/workflow/workflowitems/" + witem.getID()) + getClient(authToken).perform(multipart("/api/workflow/workflowitems/" + witem.getID()) .file(bibtexFile)) .andExpect(status().isUnprocessableEntity()); @@ -2944,7 +2944,7 @@ public void claimedTaskTest_Upload_EditMetadataOptionAllowed() throws Exception InputStream bibtex = getClass().getResourceAsStream("bibtex-test.bib"); final MockMultipartFile bibtexFile = new MockMultipartFile("file", "bibtex-test.bib", "application/x-bibtex", bibtex); - getClient(reviewer1Token).perform(fileUpload("/api/workflow/workflowitems/" + witem.getID()) + getClient(reviewer1Token).perform(multipart("/api/workflow/workflowitems/" + witem.getID()) .file(bibtexFile)) .andExpect(status().isCreated()); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java index 199313a31441..91cd50ead34e 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java @@ -19,8 +19,8 @@ import static org.springframework.data.rest.webmvc.RestMediaTypes.TEXT_URI_LIST_VALUE; import static org.springframework.http.MediaType.parseMediaType; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; @@ -934,7 +934,7 @@ public void createSingleWorkspaceItemFromBibtexFileWithOneEntryTest() throws Exc String authToken = getAuthToken(eperson.getEmail(), password); try { // create a workspaceitem from a single bibliographic entry file explicitly in the default collection (col1) - getClient(authToken).perform(fileUpload("/api/submission/workspaceitems") + getClient(authToken).perform(multipart("/api/submission/workspaceitems") .file(bibtexFile)) // create should return 200, 201 (created) is better for single resource .andExpect(status().isOk()) @@ -962,7 +962,7 @@ public void createSingleWorkspaceItemFromBibtexFileWithOneEntryTest() throws Exc // create a workspaceitem from a single bibliographic entry file explicitly in the col2 try { - getClient(authToken).perform(fileUpload("/api/submission/workspaceitems") + getClient(authToken).perform(multipart("/api/submission/workspaceitems") .file(bibtexFile) .param("owningCollection", col2.getID().toString())) .andExpect(status().isOk()) @@ -1026,7 +1026,7 @@ public void createSingleWorkspaceItemFromCSVWithOneEntryTest() throws Exception // create workspaceitems in the default collection (col1) AtomicReference> idRef = new AtomicReference<>(); try { - getClient(authToken).perform(fileUpload("/api/submission/workspaceitems") + getClient(authToken).perform(multipart("/api/submission/workspaceitems") .file(csvFile)) // create should return 200, 201 (created) is better for single resource .andExpect(status().isOk()) @@ -1065,7 +1065,7 @@ public void createSingleWorkspaceItemFromCSVWithOneEntryTest() throws Exception // create workspaceitems explicitly in the col2 try { - getClient(authToken).perform(fileUpload("/api/submission/workspaceitems") + getClient(authToken).perform(multipart("/api/submission/workspaceitems") .file(csvFile) .param("owningCollection", col2.getID().toString())) .andExpect(status().isOk()) @@ -1143,7 +1143,7 @@ public void createSingleWorkspaceItemFromCSVWithOneEntryAndMissingDataTest() thr // create workspaceitems in the default collection (col1) try { - getClient(authToken).perform(fileUpload("/api/submission/workspaceitems") + getClient(authToken).perform(multipart("/api/submission/workspaceitems") .file(csvFile)) // create should return 200, 201 (created) is better for single resource .andExpect(status().isOk()) @@ -1221,7 +1221,7 @@ public void createSingleWorkspaceItemFromTSVWithOneEntryTest() throws Exception // create workspaceitems in the default collection (col1) try { - getClient(authToken).perform(fileUpload("/api/submission/workspaceitems") + getClient(authToken).perform(multipart("/api/submission/workspaceitems") .file(tsvFile)) // create should return 200, 201 (created) is better for single resource .andExpect(status().isOk()) @@ -1297,7 +1297,7 @@ public void createSingleWorkspaceItemFromRISWithOneEntryTest() throws Exception // create workspaceitems in the default collection (col1) try { - getClient(authToken).perform(fileUpload("/api/submission/workspaceitems") + getClient(authToken).perform(multipart("/api/submission/workspaceitems") .file(tsvFile)) // create should return 200, 201 (created) is better for single resource .andExpect(status().isOk()) @@ -1374,7 +1374,7 @@ public void createSingleWorkspaceItemFromEndnoteWithOneEntryTest() throws Except AtomicReference> idRef = new AtomicReference<>(); // create workspaceitems in the default collection (col1) try { - getClient(authToken).perform(fileUpload("/api/submission/workspaceitems") + getClient(authToken).perform(multipart("/api/submission/workspaceitems") .file(endnoteFile)) // create should return 200, 201 (created) is better for single resource .andExpect(status().isOk()) @@ -1453,7 +1453,7 @@ public void createSingleWorkspaceItemFromTSVWithOneEntryAndMissingDataTest() thr // create workspaceitems in the default collection (col1) try { - getClient(authToken).perform(fileUpload("/api/submission/workspaceitems") + getClient(authToken).perform(multipart("/api/submission/workspaceitems") .file(csvFile)) // create should return 200, 201 (created) is better for single resource .andExpect(status().isOk()) @@ -1534,7 +1534,7 @@ public void createSingleWorkspaceItemFromMultipleFilesWithOneEntryTest() throws // create a workspaceitem from a single bibliographic entry file explicitly in the default collection (col1) try { - getClient(authToken).perform(fileUpload("/api/submission/workspaceitems") + getClient(authToken).perform(multipart("/api/submission/workspaceitems") .file(bibtexFile).file(pubmedFile)) // create should return 200, 201 (created) is better for single resource .andExpect(status().isOk()) @@ -1568,7 +1568,7 @@ public void createSingleWorkspaceItemFromMultipleFilesWithOneEntryTest() throws // create a workspaceitem from a single bibliographic entry file explicitly in the col2 try { - getClient(authToken).perform(fileUpload("/api/submission/workspaceitems") + getClient(authToken).perform(multipart("/api/submission/workspaceitems") .file(bibtexFile).file(pubmedFile) .param("owningCollection", col2.getID().toString())) .andExpect(status().isOk()) @@ -1640,7 +1640,7 @@ public void createSingleWorkspaceItemsFromSingleFileWithMultipleEntriesTest() th String authToken = getAuthToken(eperson.getEmail(), password); // create a workspaceitem from a single bibliographic entry file explicitly in the default collection (col1) - getClient(authToken).perform(fileUpload("/api/submission/workspaceitems") + getClient(authToken).perform(multipart("/api/submission/workspaceitems") .file(bibtexFile)) // create should return return a 422 because we don't allow/support bibliographic files // that have multiple metadata records @@ -1685,7 +1685,7 @@ public void createPubmedWorkspaceItemFromFileTest() throws Exception { // create a workspaceitem from a single bibliographic entry file explicitly in the default collection (col1) try { - getClient(authToken).perform(fileUpload("/api/submission/workspaceitems") + getClient(authToken).perform(multipart("/api/submission/workspaceitems") .file(pubmedFile)) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.workspaceitems[0].sections.traditionalpageone['dc.title'][0].value", @@ -1716,7 +1716,7 @@ public void createPubmedWorkspaceItemFromFileTest() throws Exception { // create a workspaceitem from a single bibliographic entry file explicitly in the col2 try { - getClient(authToken).perform(fileUpload("/api/submission/workspaceitems") + getClient(authToken).perform(multipart("/api/submission/workspaceitems") .file(pubmedFile) .param("owningCollection", col2.getID().toString())) .andExpect(status().isOk()) @@ -1776,7 +1776,7 @@ public void createWorkspaceItemFromPDFFileTest() throws Exception { context.restoreAuthSystemState(); // create a workspaceitem - getClient(authToken).perform(fileUpload("/api/submission/workspaceitems") + getClient(authToken).perform(multipart("/api/submission/workspaceitems") .file(pdfFile)) // create should return 200, 201 (created) is better for single resource .andExpect(status().isOk()) @@ -3491,7 +3491,7 @@ public void uploadTest() throws Exception { context.restoreAuthSystemState(); // upload the file in our workspaceitem - getClient(authToken).perform(fileUpload("/api/submission/workspaceitems/" + witem.getID()) + getClient(authToken).perform(multipart("/api/submission/workspaceitems/" + witem.getID()) .file(pdfFile)) .andExpect(status().isCreated()) .andExpect(jsonPath("$.sections.upload.files[0].metadata['dc.title'][0].value", @@ -3534,7 +3534,7 @@ public void uploadUnAuthenticatedTest() throws Exception { context.restoreAuthSystemState(); // upload the file in our workspaceitem - getClient().perform(fileUpload("/api/submission/workspaceitems/" + witem.getID()) + getClient().perform(multipart("/api/submission/workspaceitems/" + witem.getID()) .file(pdfFile)) .andExpect(status().isUnauthorized()); @@ -3580,7 +3580,7 @@ public void uploadForbiddenTest() throws Exception { // upload the file in our workspaceitem String authToken = getAuthToken(eperson2.getEmail(), "qwerty02"); - getClient(authToken).perform(fileUpload("/api/submission/workspaceitems/" + witem.getID()) + getClient(authToken).perform(multipart("/api/submission/workspaceitems/" + witem.getID()) .file(pdfFile)) .andExpect(status().isForbidden()); @@ -3617,7 +3617,7 @@ public void createWorkspaceWithFiles_UploadRequired() throws Exception { context.restoreAuthSystemState(); // upload the file in our workspaceitem - getClient(authToken).perform(fileUpload("/api/submission/workspaceitems/" + witem.getID()) + getClient(authToken).perform(multipart("/api/submission/workspaceitems/" + witem.getID()) .file(pdfFile)) .andExpect(status().isCreated()) .andExpect(jsonPath("$.sections.upload.files[0].metadata['dc.title'][0].value", @@ -4386,7 +4386,7 @@ public void uploadBibtexFileOnExistingSubmissionTest() throws Exception { try { // adding a bibtex file with a single entry should automatically put the metadata in the bibtex file into // the item - getClient(authToken).perform(fileUpload("/api/submission/workspaceitems/" + witem.getID()) + getClient(authToken).perform(multipart("/api/submission/workspaceitems/" + witem.getID()) .file(bibtexFile)) .andExpect(status().isCreated()) .andExpect(jsonPath("$.sections.traditionalpageone['dc.title'][0].value", @@ -4403,7 +4403,7 @@ public void uploadBibtexFileOnExistingSubmissionTest() throws Exception { is("bibtex-test.bib"))); // do again over a submission that already has a title, the manual input should be preserved - getClient(authToken).perform(fileUpload("/api/submission/workspaceitems/" + witem2.getID()) + getClient(authToken).perform(multipart("/api/submission/workspaceitems/" + witem2.getID()) .file(bibtexFile)) .andExpect(status().isCreated()) .andExpect(jsonPath("$.sections.traditionalpageone['dc.title'][0].value", @@ -4944,7 +4944,7 @@ public void uploadBitstreamWithoutAccessConditions() throws Exception { // upload file and verify response getClient(authToken) - .perform(fileUpload("/api/submission/workspaceitems/" + wItem.getID()).file(pdfFile)) + .perform(multipart("/api/submission/workspaceitems/" + wItem.getID()).file(pdfFile)) .andExpect(status().isCreated()) .andExpect(jsonPath("$.sections.upload.files[0].accessConditions", empty())); @@ -5415,7 +5415,7 @@ public void invalidCollectionConfigurationPreventItemCreationTest() throws Excep context.restoreAuthSystemState(); try { - getClient(authToken).perform(fileUpload("/api/submission/workspaceitems") + getClient(authToken).perform(multipart("/api/submission/workspaceitems") .file(pdfFile)) .andExpect(status().is(500)); } finally { @@ -7171,7 +7171,7 @@ public void verifyCorrectInheritanceOfAccessConditionTest() throws Exception { .andExpect(jsonPath("$.sections.upload.files[1]").doesNotExist()); // upload second file - getClient(tokenEPerson).perform(fileUpload("/api/submission/workspaceitems/" + wItem.getID()) + getClient(tokenEPerson).perform(multipart("/api/submission/workspaceitems/" + wItem.getID()) .file(xmlFile)) .andExpect(status().isCreated()); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/csv/CsvExportIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/csv/CsvExportIT.java index 7439edb498dd..debd7af20d9a 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/csv/CsvExportIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/csv/CsvExportIT.java @@ -9,7 +9,7 @@ import static com.jayway.jsonpath.JsonPath.read; import static org.hamcrest.Matchers.is; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -76,9 +76,8 @@ public void metadataExportTestWithoutFileParameterSucceeds() throws Exception { String token = getAuthToken(admin.getEmail(), password); getClient(token) - .perform(fileUpload("/api/system/scripts/metadata-export/processes") - .param("properties", - new Gson().toJson(list))) + .perform(multipart("/api/system/scripts/metadata-export/processes") + .param("properties", new Gson().toJson(list))) .andExpect(status().isAccepted()) .andExpect(jsonPath("$", is( ProcessMatcher.matchProcess("metadata-export", @@ -128,9 +127,8 @@ public void metadataExportTestWithFileParameterFails() throws Exception { String token = getAuthToken(admin.getEmail(), password); getClient(token) - .perform(fileUpload("/api/system/scripts/metadata-export/processes") - .param("properties", - new Gson().toJson(list))) + .perform(multipart("/api/system/scripts/metadata-export/processes") + .param("properties", new Gson().toJson(list))) .andExpect(status().isAccepted()) .andExpect(jsonPath("$", is( ProcessMatcher.matchProcess("metadata-export", diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/csv/CsvImportIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/csv/CsvImportIT.java index b2eb36215a28..59e2eab42408 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/csv/CsvImportIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/csv/CsvImportIT.java @@ -13,8 +13,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -284,8 +284,8 @@ private void performImportScript(String[] csv) throws Exception { String token = getAuthToken(admin.getEmail(), password); getClient(token) - .perform(fileUpload("/api/system/scripts/metadata-import/processes").file(bitstreamFile) - .param("properties", + .perform(multipart("/api/system/scripts/metadata-import/processes").file(bitstreamFile) + .param("properties", new Gson().toJson(list))) .andExpect(status().isAccepted()) .andDo(result -> idRef @@ -344,7 +344,7 @@ public void csvImportWithSpecifiedEPersonParameterTestShouldFailProcess() throws String token = getAuthToken(admin.getEmail(), password); getClient(token) - .perform(fileUpload("/api/system/scripts/metadata-import/processes").file(bitstreamFile) + .perform(multipart("/api/system/scripts/metadata-import/processes").file(bitstreamFile) .param("properties", new Gson().toJson(list))) .andExpect(status().isAccepted()) diff --git a/dspace-server-webapp/src/test/java/org/dspace/curate/CurationScriptIT.java b/dspace-server-webapp/src/test/java/org/dspace/curate/CurationScriptIT.java index 66c031985752..0390136af7b2 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/curate/CurationScriptIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/curate/CurationScriptIT.java @@ -9,7 +9,7 @@ import static com.jayway.jsonpath.JsonPath.read; import static org.hamcrest.Matchers.is; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -87,9 +87,8 @@ public void curateScript_invalidTaskOption() throws Exception { // Request with -t getClient(token) - .perform(post(CURATE_SCRIPT_ENDPOINT).contentType("multipart/form-data") - .param("properties", - new Gson().toJson(list))) + .perform(multipart(CURATE_SCRIPT_ENDPOINT) + .param("properties", new Gson().toJson(list))) // Illegal Argument Exception .andExpect(status().isBadRequest()); } @@ -109,9 +108,8 @@ public void curateScript_MissingHandle() throws Exception { // Request with missing required -i getClient(token) - .perform(post(CURATE_SCRIPT_ENDPOINT).contentType("multipart/form-data") - .param("properties", - new Gson().toJson(list))) + .perform(multipart(CURATE_SCRIPT_ENDPOINT) + .param("properties", new Gson().toJson(list))) // Illegal Argument Exception .andExpect(status().isBadRequest()); } @@ -132,9 +130,8 @@ public void curateScript_invalidHandle() throws Exception { // Request with missing required -i getClient(token) - .perform(post(CURATE_SCRIPT_ENDPOINT).contentType("multipart/form-data") - .param("properties", - new Gson().toJson(list))) + .perform(multipart(CURATE_SCRIPT_ENDPOINT) + .param("properties", new Gson().toJson(list))) // Illegal Argument Exception .andExpect(status().isBadRequest()); } @@ -173,9 +170,8 @@ public void curateScript_MissingTaskOrTaskFile() throws Exception { // Request without -t or -T (and no -q ) getClient(token) - .perform(post(CURATE_SCRIPT_ENDPOINT).contentType("multipart/form-data") - .param("properties", - new Gson().toJson(list))) + .perform(multipart(CURATE_SCRIPT_ENDPOINT) + .param("properties", new Gson().toJson(list))) // Illegal Argument Exception .andExpect(status().isBadRequest()); } @@ -196,9 +192,8 @@ public void curateScript_InvalidScope() throws Exception { // Request with invalid -s ; must be object, curation or open getClient(token) - .perform(post(CURATE_SCRIPT_ENDPOINT).contentType("multipart/form-data") - .param("properties", - new Gson().toJson(list))) + .perform(multipart(CURATE_SCRIPT_ENDPOINT) + .param("properties", new Gson().toJson(list))) // Illegal Argument Exception .andExpect(status().isBadRequest()); } @@ -219,9 +214,8 @@ public void curateScript_InvalidTaskFile() throws Exception { // Request with invalid -s ; must be object, curation or open getClient(token) - .perform(post(CURATE_SCRIPT_ENDPOINT).contentType("multipart/form-data") - .param("properties", - new Gson().toJson(list))) + .perform(multipart(CURATE_SCRIPT_ENDPOINT) + .param("properties", new Gson().toJson(list))) // Illegal Argument Exception .andExpect(status().isBadRequest()); } @@ -262,9 +256,8 @@ public void curateScript_validRequest_Task() throws Exception { try { getClient(token) - .perform(post(CURATE_SCRIPT_ENDPOINT).contentType("multipart/form-data") - .param("properties", - new Gson().toJson(list))) + .perform(multipart(CURATE_SCRIPT_ENDPOINT) + .param("properties", new Gson().toJson(list))) .andExpect(status().isAccepted()) .andExpect(jsonPath("$", is( ProcessMatcher.matchProcess("curate", @@ -314,9 +307,8 @@ public void curateScript_validRequest_TaskFile() throws Exception { try { getClient(token) - .perform(post(CURATE_SCRIPT_ENDPOINT).contentType("multipart/form-data") - .param("properties", - new Gson().toJson(list))) + .perform(multipart(CURATE_SCRIPT_ENDPOINT) + .param("properties", new Gson().toJson(list))) .andExpect(status().isAccepted()) .andExpect(jsonPath("$", is( ProcessMatcher.matchProcess("curate", @@ -366,9 +358,8 @@ public void curateScript_EPersonInParametersFails() throws Exception { try { getClient(token) - .perform(post(CURATE_SCRIPT_ENDPOINT).contentType("multipart/form-data") - .param("properties", - new Gson().toJson(list))) + .perform(multipart(CURATE_SCRIPT_ENDPOINT) + .param("properties", new Gson().toJson(list))) .andExpect(jsonPath("$", is( ProcessMatcher.matchProcess("curate", String.valueOf(admin.getID()), parameters, From 88cea74e43bf905cc5f1aa6add322eb53d7ad8a7 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 7 Mar 2022 17:01:02 -0600 Subject: [PATCH 0134/1846] Fix test by ensuring InputStream is closed immediately after usage. Refactor try/finally to support that. --- .../rest/WorkflowItemRestRepositoryIT.java | 88 ++++++++++--------- 1 file changed, 45 insertions(+), 43 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowItemRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowItemRestRepositoryIT.java index 660881279183..b44e134d82ed 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowItemRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowItemRestRepositoryIT.java @@ -1862,7 +1862,7 @@ public void findOneFullProjectionTest() throws Exception { } @Test - public void whenWorkspaceitemBecomeWorkflowitemWithAccessConditionsTheBitstremMustBeDownloableTest() + public void whenWorkspaceitemBecomeWorkflowitemWithAccessConditionsTheBitstreamMustBeDownloableTest() throws Exception { context.turnOffAuthorisationSystem(); @@ -1890,8 +1890,6 @@ public void whenWorkspaceitemBecomeWorkflowitemWithAccessConditionsTheBitstremMu String bitstreamContent = "0123456789"; - AtomicReference idRef = new AtomicReference(); - try (InputStream is = IOUtils.toInputStream(bitstreamContent, Charset.defaultCharset())) { context.setCurrentUser(submitter); @@ -1905,56 +1903,60 @@ public void whenWorkspaceitemBecomeWorkflowitemWithAccessConditionsTheBitstremMu .withDescription("This is a bitstream to test range requests") .withMimeType("text/plain") .build(); + } - context.restoreAuthSystemState(); + context.restoreAuthSystemState(); - String tokenEPerson = getAuthToken(eperson.getEmail(), password); - String tokenSubmitter = getAuthToken(submitter.getEmail(), password); - String tokenReviewer1 = getAuthToken(reviewer1.getEmail(), password); + String tokenEPerson = getAuthToken(eperson.getEmail(), password); + String tokenSubmitter = getAuthToken(submitter.getEmail(), password); + String tokenReviewer1 = getAuthToken(reviewer1.getEmail(), password); - // submitter can download the bitstream - getClient(tokenSubmitter).perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content")) - .andExpect(status().isOk()) - .andExpect(header().string("Accept-Ranges", "bytes")) - .andExpect(header().string("ETag", "\"" + bitstream.getChecksum() + "\"")) - .andExpect(content().contentType("text/plain")) - .andExpect(content().bytes(bitstreamContent.getBytes())); + // submitter can download the bitstream + getClient(tokenSubmitter).perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content")) + .andExpect(status().isOk()) + .andExpect(header().string("Accept-Ranges", "bytes")) + .andExpect(header().string("ETag", "\"" + bitstream.getChecksum() + "\"")) + .andExpect(content().contentType("text/plain;charset=UTF-8")) + .andExpect(content().bytes(bitstreamContent.getBytes())); - // reviewer can't still download the bitstream - getClient(tokenReviewer1).perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content")) - .andExpect(status().isForbidden()); + // reviewer can't still download the bitstream + getClient(tokenReviewer1).perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content")) + .andExpect(status().isForbidden()); - // others can't download the bitstream - getClient(tokenEPerson).perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content")) - .andExpect(status().isForbidden()); + // others can't download the bitstream + getClient(tokenEPerson).perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content")) + .andExpect(status().isForbidden()); - // create a list of values to use in add operation - List addAccessCondition = new ArrayList<>(); - List> accessConditions = new ArrayList>(); + // create a list of values to use in add operation + List addAccessCondition = new ArrayList<>(); + List> accessConditions = new ArrayList>(); - Map value = new HashMap<>(); - value.put("name", "administrator"); + Map value = new HashMap<>(); + value.put("name", "administrator"); - accessConditions.add(value); + accessConditions.add(value); - addAccessCondition.add(new AddOperation("/sections/upload/files/0/accessConditions", accessConditions)); + addAccessCondition.add(new AddOperation("/sections/upload/files/0/accessConditions", accessConditions)); - String patchBody = getPatchContent(addAccessCondition); - getClient(tokenSubmitter).perform(patch("/api/submission/workspaceitems/" + witem.getID()) - .content(patchBody) - .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.sections.upload.files[0].accessConditions[0].name",is("administrator"))) - .andExpect(jsonPath("$.sections.upload.files[0].accessConditions[0].startDate",nullValue())) - .andExpect(jsonPath("$.sections.upload.files[0].accessConditions[0].endDate", nullValue())); + String patchBody = getPatchContent(addAccessCondition); + getClient(tokenSubmitter).perform(patch("/api/submission/workspaceitems/" + witem.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.sections.upload.files[0].accessConditions[0].name",is("administrator"))) + .andExpect(jsonPath("$.sections.upload.files[0].accessConditions[0].startDate",nullValue())) + .andExpect(jsonPath("$.sections.upload.files[0].accessConditions[0].endDate", nullValue())); - // verify that the patch changes have been persisted - getClient(tokenSubmitter).perform(get("/api/submission/workspaceitems/" + witem.getID())) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.sections.upload.files[0].accessConditions[0].name",is("administrator"))) - .andExpect(jsonPath("$.sections.upload.files[0].accessConditions[0].startDate",nullValue())) - .andExpect(jsonPath("$.sections.upload.files[0].accessConditions[0].endDate", nullValue())); + // verify that the patch changes have been persisted + getClient(tokenSubmitter).perform(get("/api/submission/workspaceitems/" + witem.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.sections.upload.files[0].accessConditions[0].name",is("administrator"))) + .andExpect(jsonPath("$.sections.upload.files[0].accessConditions[0].startDate",nullValue())) + .andExpect(jsonPath("$.sections.upload.files[0].accessConditions[0].endDate", nullValue())); + + AtomicReference idRef = new AtomicReference(); + try { // submit the workspaceitem to start the workflow getClient(tokenSubmitter).perform(post(BASE_REST_SERVER_URL + "/api/workflow/workflowitems") .content("/api/submission/workspaceitems/" + witem.getID()) @@ -1976,7 +1978,7 @@ public void whenWorkspaceitemBecomeWorkflowitemWithAccessConditionsTheBitstremMu .andExpect(status().isOk()) .andExpect(header().string("Accept-Ranges", "bytes")) .andExpect(header().string("ETag", "\"" + bitstream.getChecksum() + "\"")) - .andExpect(content().contentType("text/plain")) + .andExpect(content().contentType("text/plain;charset=UTF-8")) .andExpect(content().bytes(bitstreamContent.getBytes())); // submitter can download the bitstream @@ -1984,7 +1986,7 @@ public void whenWorkspaceitemBecomeWorkflowitemWithAccessConditionsTheBitstremMu .andExpect(status().isOk()) .andExpect(header().string("Accept-Ranges", "bytes")) .andExpect(header().string("ETag", "\"" + bitstream.getChecksum() + "\"")) - .andExpect(content().contentType("text/plain")) + .andExpect(content().contentType("text/plain;charset=UTF-8")) .andExpect(content().bytes(bitstreamContent.getBytes())); // others can't download the bitstream From 54f2c5c661d30c972643c9fd0b7bd43a702f25ed Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Tue, 8 Mar 2022 09:39:17 -0600 Subject: [PATCH 0135/1846] Minor updates to Spring dependencies --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index a99306b6aaf6..7aa5fc9dad67 100644 --- a/pom.xml +++ b/pom.xml @@ -19,9 +19,9 @@ 11 - 5.3.15 - 2.6.3 - 5.6.1 + 5.3.16 + 2.6.4 + 5.6.2 5.6.5.Final 6.0.23.Final 42.3.3 From 7a11d0d6051b46fc84aecdde58b2154fcec875c5 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 11 Mar 2022 12:16:11 -0600 Subject: [PATCH 0136/1846] Replace Spring Data REST HAL Browser with HAL Browser from WebJars --- dspace-server-webapp/README.md | 5 ++- dspace-server-webapp/pom.xml | 37 +++++-------------- .../java/org/dspace/app/rest/Application.java | 21 ++++++++++- pom.xml | 5 +-- 4 files changed, 35 insertions(+), 33 deletions(-) diff --git a/dspace-server-webapp/README.md b/dspace-server-webapp/README.md index e71039308a76..8d3853e8ccc7 100644 --- a/dspace-server-webapp/README.md +++ b/dspace-server-webapp/README.md @@ -22,7 +22,10 @@ The only tested way right now is to run this webapp inside your IDE (Eclipse). J > dspace.dir = d:/install/dspace7 ## HAL Browser -The modified version of the HAL Browser from the Spring Data REST project is included, the index.html file is overriden locally to support the /api baseURL (see [DATAREST-971](https://jira.spring.io/browse/DATAREST-971)) + +The modified version of the HAL Browser from https://github.com/mikekelly/hal-browser + +We've updated/customized the HAL Browser to integrate better with our authentication system, provide CSRF support, and use a more recent version of its dependencies. ## Packages and main classes *[org.dspace.app.rest.Application](src/main/java/org/dspace/app/rest/Application.java)* is the spring boot main class it initializes diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 6c34e11a81c7..e83e411d3938 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -238,24 +238,7 @@ - - - - - - + org.springframework.boot spring-boot-starter-web @@ -293,17 +276,15 @@ 0.4.6 - + + + - org.springframework.data - spring-data-rest-hal-browser - ${spring-hal-browser.version} - - - org.springframework.data - spring-data-rest-webmvc - - + org.webjars + hal-browser + ad9b865 diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/Application.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/Application.java index e0ea0cccb0af..459cfe0dee6c 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/Application.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/Application.java @@ -40,6 +40,7 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** @@ -192,13 +193,31 @@ public void addCorsMappings(@NonNull CorsRegistry registry) { } } + /** + * Add a ViewController for the root path, to load HAL Browser + * @param registry ViewControllerRegistry + */ + @Override + public void addViewControllers(ViewControllerRegistry registry) { + // Ensure accessing the root path will load the index.html of the HAL Browser + registry.addViewController("/").setViewName("forward:/index.html"); + } + /** * Add a new ResourceHandler to allow us to use WebJars.org to pull in web dependencies - * dynamically for HAL Browser, and access them off the /webjars path. + * dynamically for HAL Browser, etc. * @param registry ResourceHandlerRegistry */ @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { + // First, "mount" the Hal Browser resources at the /browser path + // NOTE: the hal-browser directory uses the version of the Hal browser, so this needs to be synced + // with the org.webjars.hal-browser version in the POM + registry + .addResourceHandler("/browser/**") + .addResourceLocations("/webjars/hal-browser/ad9b865/"); + + // Make all other Webjars available off the /webjars path registry .addResourceHandler("/webjars/**") .addResourceLocations("/webjars/"); diff --git a/pom.xml b/pom.xml index 7aa5fc9dad67..9da639b710fe 100644 --- a/pom.xml +++ b/pom.xml @@ -46,8 +46,6 @@ 1.70 - - 3.3.9.RELEASE 2.6.0 com.jayway.jsonpath json-path ${json-path.version} - test + com.jayway.jsonpath json-path-assert From 92687252ce11f892e13828ba627b86efdc8e11d2 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 11 Mar 2022 12:16:44 -0600 Subject: [PATCH 0137/1846] Remove unnecessary config / annotation after Spring upgrades --- .../java/org/dspace/app/rest/utils/ApplicationConfig.java | 2 -- .../src/main/resources/application.properties | 5 ----- 2 files changed, 7 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/ApplicationConfig.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/ApplicationConfig.java index 7170072e13b0..c2136781f927 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/ApplicationConfig.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/ApplicationConfig.java @@ -11,7 +11,6 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; -import org.springframework.data.web.config.EnableSpringDataWebSupport; /** * This class provides extra configuration for our Spring Boot Application @@ -23,7 +22,6 @@ * @author Tim Donohue */ @Configuration -@EnableSpringDataWebSupport @ComponentScan( {"org.dspace.app.rest.converter", "org.dspace.app.rest.repository", "org.dspace.app.rest.utils", "org.dspace.app.configuration", "org.dspace.iiif", "org.dspace.app.iiif"}) public class ApplicationConfig { diff --git a/dspace-server-webapp/src/main/resources/application.properties b/dspace-server-webapp/src/main/resources/application.properties index 5c1790f0310e..5992ded04008 100644 --- a/dspace-server-webapp/src/main/resources/application.properties +++ b/dspace-server-webapp/src/main/resources/application.properties @@ -38,11 +38,6 @@ # interact with or read its configuration from dspace.cfg. dspace.dir=${dspace.dir} -######################## -# Spring DATA Rest settings -# -spring.data.rest.basePath= - ######################## # Jackson serialization settings # From cfa9f01a47f0e8648ea17a7b940fc476846cac62 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 14 Mar 2022 12:18:30 -0500 Subject: [PATCH 0138/1846] Upgrade Hal Browser to Bootstrap 4 based on https://github.com/mikekelly/hal-browser/pull/102 . Also add CustomPostForm script from Spring Data REST v3.3.x --- dspace-server-webapp/pom.xml | 7 +- .../src/main/webapp/index.html | 241 ++++++++++-------- .../main/webapp/js/vendor/CustomPostForm.js | 207 +++++++++++++++ .../src/main/webapp/js/vendor/README | 11 + .../src/main/webapp/js/vendor/jsoneditor.js | 14 + .../src/main/webapp/login.html | 26 +- .../src/main/webapp/styles.css | 104 ++++++++ 7 files changed, 488 insertions(+), 122 deletions(-) create mode 100644 dspace-server-webapp/src/main/webapp/js/vendor/CustomPostForm.js create mode 100644 dspace-server-webapp/src/main/webapp/js/vendor/README create mode 100644 dspace-server-webapp/src/main/webapp/js/vendor/jsoneditor.js create mode 100644 dspace-server-webapp/src/main/webapp/styles.css diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index e83e411d3938..3d5a922e556d 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -68,7 +68,9 @@ src/main/webapp/index.html src/main/webapp/login.html + src/main/webapp/styles.css src/main/webapp/js/hal/** + src/main/webapp/js/vendor/** @@ -302,8 +304,8 @@ toastr 2.1.4 - @@ -312,7 +314,6 @@ 4.5.2 - org.springframework.boot diff --git a/dspace-server-webapp/src/main/webapp/index.html b/dspace-server-webapp/src/main/webapp/index.html index 829c61439388..fbab9d93b924 100644 --- a/dspace-server-webapp/src/main/webapp/index.html +++ b/dspace-server-webapp/src/main/webapp/index.html @@ -7,50 +7,53 @@ * * Download file functionality (see new downloadFile() method) * * Improved AuthorizationHeader parsing (see new getAuthorizationHeader() method) * * Upgraded third party dependencies (JQuery) +* * Updated to use Bootstrap 4, based loosely on this PR to HAL Browser: + https://github.com/mikekelly/hal-browser/pull/102 --> - The HAL Browser (customized for DSpace Server Webapp) - + The HAL Browser (customized for DSpace) + - - + -