This commit is contained in:
Paulo Graça
2023-06-04 23:01:31 +01:00
committed by GitHub
16 changed files with 232 additions and 14 deletions

View File

@@ -23,6 +23,7 @@ import java.util.UUID;
import org.apache.commons.cli.ParseException;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.tika.Tika;
import org.dspace.app.itemimport.factory.ItemImportServiceFactory;
import org.dspace.app.itemimport.service.ItemImportService;
import org.dspace.authorize.AuthorizeException;
@@ -77,6 +78,7 @@ public class ItemImport extends DSpaceRunnable<ItemImportScriptConfiguration> {
protected boolean zip = false;
protected boolean remoteUrl = false;
protected String zipfilename = null;
protected boolean zipvalid = false;
protected boolean help = false;
protected File workDir = null;
protected File workFile = null;
@@ -235,11 +237,19 @@ public class ItemImport extends DSpaceRunnable<ItemImportScriptConfiguration> {
handler.logInfo("***End of Test Run***");
}
} finally {
// clean work dir
if (zip) {
FileUtils.deleteDirectory(new File(sourcedir));
FileUtils.deleteDirectory(workDir);
if (remoteUrl && workFile != null && workFile.exists()) {
// if zip file was valid then clean sourcedir
if (zipvalid && sourcedir != null && new File(sourcedir).exists()) {
FileUtils.deleteDirectory(new File(sourcedir));
}
// clean workdir
if (workDir != null && workDir.exists()) {
FileUtils.deleteDirectory(workDir);
}
// conditionally clean workFile if import was done in the UI or via a URL and it still exists
if (workFile != null && workFile.exists()) {
workFile.delete();
}
}
@@ -329,7 +339,14 @@ public class ItemImport extends DSpaceRunnable<ItemImportScriptConfiguration> {
// manage zip via remote url
optionalFileStream = Optional.ofNullable(new URL(zipfilename).openStream());
}
if (optionalFileStream.isPresent()) {
// validate zip file
Optional<InputStream> validationFileStream = handler.getFileStream(context, zipfilename);
if (validationFileStream.isPresent()) {
validateZip(validationFileStream.get());
}
workFile = new File(itemImportService.getTempWorkDir() + File.separator
+ zipfilename + "-" + context.getCurrentUser().getID());
FileUtils.copyInputStreamToFile(optionalFileStream.get(), workFile);
@@ -337,10 +354,32 @@ public class ItemImport extends DSpaceRunnable<ItemImportScriptConfiguration> {
throw new IllegalArgumentException(
"Error reading file, the file couldn't be found for filename: " + zipfilename);
}
workDir = new File(itemImportService.getTempWorkDir() + File.separator + TEMP_DIR);
workDir = new File(itemImportService.getTempWorkDir() + File.separator + TEMP_DIR
+ File.separator + context.getCurrentUser().getID());
sourcedir = itemImportService.unzip(workFile, workDir.getAbsolutePath());
}
/**
* Confirm that the zip file has the correct MIME type
* @param inputStream
*/
protected void validateZip(InputStream inputStream) {
Tika tika = new Tika();
try {
String mimeType = tika.detect(inputStream);
if (mimeType.equals("application/zip")) {
zipvalid = true;
} else {
handler.logError("A valid zip file must be supplied. The provided file has mimetype: " + mimeType);
throw new UnsupportedOperationException("A valid zip file must be supplied");
}
} catch (IOException e) {
throw new IllegalArgumentException(
"There was an error while reading the zip file: " + zipfilename);
}
}
/**
* Read the mapfile
* @param context

View File

@@ -8,6 +8,7 @@
package org.dspace.app.itemimport;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.URL;
import java.sql.SQLException;
@@ -101,6 +102,17 @@ public class ItemImportCLI extends ItemImport {
// If this is a zip archive, unzip it first
if (zip) {
if (!remoteUrl) {
// confirm zip file exists
File myZipFile = new File(sourcedir + File.separator + zipfilename);
if ((!myZipFile.exists()) || (!myZipFile.isFile())) {
throw new IllegalArgumentException(
"Error reading file, the file couldn't be found for filename: " + zipfilename);
}
// validate zip file
InputStream validationFileStream = new FileInputStream(myZipFile);
validateZip(validationFileStream);
workDir = new File(itemImportService.getTempWorkDir() + File.separator + TEMP_DIR
+ File.separator + context.getCurrentUser().getID());
sourcedir = itemImportService.unzip(
@@ -109,15 +121,22 @@ public class ItemImportCLI extends ItemImport {
// manage zip via remote url
Optional<InputStream> optionalFileStream = Optional.ofNullable(new URL(zipfilename).openStream());
if (optionalFileStream.isPresent()) {
// validate zip file via url
Optional<InputStream> validationFileStream = Optional.ofNullable(new URL(zipfilename).openStream());
if (validationFileStream.isPresent()) {
validateZip(validationFileStream.get());
}
workFile = new File(itemImportService.getTempWorkDir() + File.separator
+ zipfilename + "-" + context.getCurrentUser().getID());
FileUtils.copyInputStreamToFile(optionalFileStream.get(), workFile);
workDir = new File(itemImportService.getTempWorkDir() + File.separator + TEMP_DIR
+ File.separator + context.getCurrentUser().getID());
sourcedir = itemImportService.unzip(workFile, workDir.getAbsolutePath());
} else {
throw new IllegalArgumentException(
"Error reading file, the file couldn't be found for filename: " + zipfilename);
}
workDir = new File(itemImportService.getTempWorkDir() + File.separator + TEMP_DIR);
sourcedir = itemImportService.unzip(workFile, workDir.getAbsolutePath());
}
}
}

View File

@@ -64,7 +64,9 @@ import org.dspace.eperson.service.SubscribeService;
import org.dspace.event.Event;
import org.dspace.harvest.HarvestedItem;
import org.dspace.harvest.service.HarvestedItemService;
import org.dspace.identifier.DOI;
import org.dspace.identifier.IdentifierException;
import org.dspace.identifier.service.DOIService;
import org.dspace.identifier.service.IdentifierService;
import org.dspace.orcid.OrcidHistory;
import org.dspace.orcid.OrcidQueue;
@@ -123,6 +125,8 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl<Item> implements It
@Autowired(required = true)
protected IdentifierService identifierService;
@Autowired(required = true)
protected DOIService doiService;
@Autowired(required = true)
protected VersioningService versioningService;
@Autowired(required = true)
protected HarvestedItemService harvestedItemService;
@@ -786,6 +790,16 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl<Item> implements It
// Remove any Handle
handleService.unbindHandle(context, item);
// Delete a DOI if linked to the item.
// If no DOI consumer or provider is configured, but a DOI remains linked to this item's uuid,
// hibernate will throw a foreign constraint exception.
// Here we use the DOI service directly as it is able to manage DOIs even without any configured
// consumer or provider.
DOI doi = doiService.findDOIByDSpaceObject(context, item);
if (doi != null) {
doi.setDSpaceObject(null);
}
// remove version attached to the item
removeVersion(context, item);

View File

@@ -9,6 +9,7 @@ package org.dspace.discovery.configuration;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
@@ -22,6 +23,11 @@ public class DiscoverySortConfiguration {
private List<DiscoverySortFieldConfiguration> sortFields = new ArrayList<DiscoverySortFieldConfiguration>();
/**
* Default sort configuration to use when needed
*/
@Nullable private DiscoverySortFieldConfiguration defaultSortField;
public List<DiscoverySortFieldConfiguration> getSortFields() {
return sortFields;
}
@@ -30,6 +36,14 @@ public class DiscoverySortConfiguration {
this.sortFields = sortFields;
}
public DiscoverySortFieldConfiguration getDefaultSortField() {
return defaultSortField;
}
public void setDefaultSortField(DiscoverySortFieldConfiguration configuration) {
this.defaultSortField = configuration;
}
public DiscoverySortFieldConfiguration getSortFieldConfiguration(String sortField) {
if (StringUtils.isBlank(sortField)) {
return null;

View File

@@ -332,7 +332,9 @@ public class DiscoverQueryBuilder implements InitializingBean {
}
private String getDefaultSortDirection(DiscoverySortConfiguration searchSortConfiguration, String sortOrder) {
if (Objects.nonNull(searchSortConfiguration.getSortFields()) &&
if (searchSortConfiguration.getDefaultSortField() != null) {
sortOrder = searchSortConfiguration.getDefaultSortField().getDefaultSortOrder().name();
} else if (Objects.nonNull(searchSortConfiguration.getSortFields()) &&
!searchSortConfiguration.getSortFields().isEmpty()) {
sortOrder = searchSortConfiguration.getSortFields().get(0).getDefaultSortOrder().name();
}
@@ -342,7 +344,9 @@ public class DiscoverQueryBuilder implements InitializingBean {
private String getDefaultSortField(DiscoverySortConfiguration searchSortConfiguration) {
String sortBy;// Attempt to find the default one, if none found we use SCORE
sortBy = "score";
if (Objects.nonNull(searchSortConfiguration.getSortFields()) &&
if (searchSortConfiguration.getDefaultSortField() != null) {
sortBy = searchSortConfiguration.getDefaultSortField().getMetadataField();
} else if (Objects.nonNull(searchSortConfiguration.getSortFields()) &&
!searchSortConfiguration.getSortFields().isEmpty()) {
DiscoverySortFieldConfiguration defaultSort = searchSortConfiguration.getSortFields().get(0);
if (StringUtils.isBlank(defaultSort.getMetadataField())) {

View File

@@ -8,6 +8,7 @@
package org.dspace.app.itemimport;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.io.File;
import java.nio.file.Files;
@@ -33,6 +34,7 @@ import org.dspace.content.service.ItemService;
import org.dspace.content.service.RelationshipService;
import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory;
import org.flywaydb.core.internal.util.ExceptionUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -46,6 +48,7 @@ import org.junit.Test;
public class ItemImportCLIIT extends AbstractIntegrationTestWithDatabase {
private static final String ZIP_NAME = "saf.zip";
private static final String PDF_NAME = "test.pdf";
private static final String publicationTitle = "A Tale of Two Cities";
private static final String personTitle = "Person Test";
@@ -55,6 +58,7 @@ public class ItemImportCLIIT extends AbstractIntegrationTestWithDatabase {
private Collection collection;
private Path tempDir;
private Path workDir;
private static final String TEMP_DIR = ItemImport.TEMP_DIR;
@Before
@Override
@@ -226,6 +230,10 @@ public class ItemImportCLIIT extends AbstractIntegrationTestWithDatabase {
checkMetadata();
checkMetadataWithAnotherSchema();
checkBitstream();
// confirm that TEMP_DIR still exists
File workTempDir = new File(workDir + File.separator + TEMP_DIR);
assertTrue(workTempDir.exists());
}
@Test
@@ -254,6 +262,23 @@ public class ItemImportCLIIT extends AbstractIntegrationTestWithDatabase {
checkRelationship();
}
@Test
public void importItemByZipSafInvalidMimetype() throws Exception {
// use sample PDF file
Files.copy(getClass().getResourceAsStream("test.pdf"),
Path.of(tempDir.toString() + "/" + PDF_NAME));
String[] args = new String[] { "import", "-a", "-e", admin.getEmail(), "-c", collection.getID().toString(),
"-s", tempDir.toString(), "-z", PDF_NAME, "-m", tempDir.toString()
+ "/mapfile.out" };
try {
perfomImportScript(args);
} catch (Exception e) {
// should throw an exception due to invalid mimetype
assertEquals(UnsupportedOperationException.class, ExceptionUtils.getRootCause(e).getClass());
}
}
@Test
public void resumeImportItemBySafWithMetadataOnly() throws Exception {
// create simple SAF

View File

@@ -725,9 +725,6 @@ public class CollectionTest extends AbstractDSpaceObjectTest {
// Allow Item REMOVE perms
doNothing().when(authorizeServiceSpy)
.authorizeAction(any(Context.class), any(Item.class), eq(Constants.REMOVE));
// Allow Item WRITE perms (Needed to remove identifiers, e.g. DOI, before Item deletion)
doNothing().when(authorizeServiceSpy)
.authorizeAction(any(Context.class), any(Item.class), eq(Constants.WRITE));
// create & add item first
context.turnOffAuthorisationSystem();

View File

@@ -1189,8 +1189,6 @@ public class ItemTest extends AbstractDSpaceObjectTest {
doNothing().when(authorizeServiceSpy).authorizeAction(context, item, Constants.REMOVE, true);
// Allow Item DELETE perms
doNothing().when(authorizeServiceSpy).authorizeAction(context, item, Constants.DELETE);
// Allow Item WRITE perms (required to first delete identifiers)
doNothing().when(authorizeServiceSpy).authorizeAction(context, item, Constants.WRITE);
UUID id = item.getID();
itemService.delete(context, item);

View File

@@ -8,13 +8,16 @@
package org.dspace.discovery;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import org.dspace.AbstractIntegrationTestWithDatabase;
@@ -24,6 +27,7 @@ import org.dspace.authorize.AuthorizeException;
import org.dspace.builder.ClaimedTaskBuilder;
import org.dspace.builder.CollectionBuilder;
import org.dspace.builder.CommunityBuilder;
import org.dspace.builder.EPersonBuilder;
import org.dspace.builder.ItemBuilder;
import org.dspace.builder.PoolTaskBuilder;
import org.dspace.builder.WorkflowItemBuilder;
@@ -39,6 +43,8 @@ import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.CollectionService;
import org.dspace.content.service.ItemService;
import org.dspace.content.service.WorkspaceItemService;
import org.dspace.discovery.configuration.DiscoveryConfiguration;
import org.dspace.discovery.configuration.DiscoverySortFieldConfiguration;
import org.dspace.discovery.indexobject.IndexableClaimedTask;
import org.dspace.discovery.indexobject.IndexableCollection;
import org.dspace.discovery.indexobject.IndexableItem;
@@ -731,6 +737,64 @@ public class DiscoveryIT extends AbstractIntegrationTestWithDatabase {
}
}
/**
* Test designed to check if default sort option for Discovery is working, using <code>workspace</code>
* DiscoveryConfiguration <br/>
* <b>Note</b>: this test will be skipped if <code>workspace</code> do not have a default sort option set and of
* metadataType <code>dc_date_accessioned</code> or <code>lastModified</code>
* @throws SearchServiceException
*/
@Test
public void searchWithDefaultSortServiceTest() throws SearchServiceException {
DiscoveryConfiguration workspaceConf = SearchUtils.getDiscoveryConfiguration("workspace", null);
// Skip if no default sort option set for workspaceConf
if (workspaceConf.getSearchSortConfiguration().getDefaultSortField() == null) {
return;
}
DiscoverySortFieldConfiguration defaultSortField =
workspaceConf.getSearchSortConfiguration().getDefaultSortField();
// Populate the testing objects: create items in eperson's workspace and perform search in it
int numberItems = 10;
context.turnOffAuthorisationSystem();
EPerson submitter = EPersonBuilder.createEPerson(context).withEmail("submitter@example.org").build();
context.setCurrentUser(submitter);
Community community = CommunityBuilder.createCommunity(context).build();
Collection collection = CollectionBuilder.createCollection(context, community).build();
for (int i = 0; i < numberItems; i++) {
ItemBuilder.createItem(context, collection)
.withTitle("item " + i)
.build();
}
context.restoreAuthSystemState();
// Build query with default parameters (except for workspaceConf)
DiscoverQuery discoverQuery = SearchUtils.getQueryBuilder()
.buildQuery(context, new IndexableCollection(collection), workspaceConf,"",null,"Item",null,null,
null,null);
DiscoverResult result = searchService.search(context, discoverQuery);
/*
// code example for testing against sort by dc_date_accessioned
LinkedList<String> dc_date_accesioneds = result.getIndexableObjects().stream()
.map(o -> ((Item) o.getIndexedObject()).getMetadata())
.map(l -> l.stream().filter(m -> m.getMetadataField().toString().equals("dc_date_accessioned"))
.map(m -> m.getValue()).findFirst().orElse("")
)
.collect(Collectors.toCollection(LinkedList::new));
}*/
LinkedList<String> lastModifieds = result.getIndexableObjects().stream()
.map(o -> ((Item) o.getIndexedObject()).getLastModified().toString())
.collect(Collectors.toCollection(LinkedList::new));
assertFalse(lastModifieds.isEmpty());
for (int i = 1; i < lastModifieds.size() - 1; i++) {
assertTrue(lastModifieds.get(i).compareTo(lastModifieds.get(i + 1)) >= 0);
}
}
private void assertSearchQuery(String resourceType, int size) throws SearchServiceException {
assertSearchQuery(resourceType, size, size, 0, -1);
}

View File

@@ -80,6 +80,15 @@ public class DiscoverConfigurationConverter
sortOption.setSortOrder(discoverySearchSortConfiguration.getDefaultSortOrder().name());
searchConfigurationRest.addSortOption(sortOption);
}
DiscoverySortFieldConfiguration defaultSortField = searchSortConfiguration.getDefaultSortField();
if (defaultSortField != null) {
SearchConfigurationRest.SortOption sortOption = new SearchConfigurationRest.SortOption();
sortOption.setName(defaultSortField.getMetadataField());
sortOption.setActualName(defaultSortField.getType());
sortOption.setSortOrder(defaultSortField.getDefaultSortOrder().name());
searchConfigurationRest.setDefaultSortOption(sortOption);
}
}
}

View File

@@ -31,6 +31,8 @@ public class SearchConfigurationRest extends BaseObjectRest<String> {
private List<Filter> filters = new LinkedList<>();
private List<SortOption> sortOptions = new LinkedList<>();
private SortOption defaultSortOption;
public String getCategory() {
return CATEGORY;
}
@@ -75,6 +77,14 @@ public class SearchConfigurationRest extends BaseObjectRest<String> {
return sortOptions;
}
public SortOption getDefaultSortOption() {
return defaultSortOption;
}
public void setDefaultSortOption(SortOption defaultSortOption) {
this.defaultSortOption = defaultSortOption;
}
@Override
public boolean equals(Object object) {
return (object instanceof SearchConfigurationRest &&

View File

@@ -14,6 +14,7 @@ import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
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;
@@ -82,6 +83,7 @@ public class ItemImportIT extends AbstractEntityIntegrationTest {
private DSpaceRunnableParameterConverter dSpaceRunnableParameterConverter;
private Collection collection;
private Path workDir;
private static final String TEMP_DIR = ItemImport.TEMP_DIR;
@Before
@Override
@@ -126,6 +128,10 @@ public class ItemImportIT extends AbstractEntityIntegrationTest {
checkMetadata();
checkMetadataWithAnotherSchema();
checkBitstream();
// confirm that TEMP_DIR still exists
File workTempDir = new File(workDir + File.separator + TEMP_DIR);
assertTrue(workTempDir.exists());
}
@Test

View File

@@ -115,6 +115,8 @@ public class RestDiscoverQueryBuilderTest {
sortConfiguration.setSortFields(listSortField);
sortConfiguration.setDefaultSortField(defaultSort);
discoveryConfiguration.setSearchSortConfiguration(sortConfiguration);
DiscoverySearchFilterFacet subjectFacet = new DiscoverySearchFilterFacet();
@@ -167,6 +169,16 @@ public class RestDiscoverQueryBuilderTest {
page.getOffset(), "SCORE", "ASC");
}
@Test
public void testSortByDefaultSortField() throws Exception {
page = PageRequest.of(2, 10);
restQueryBuilder.buildQuery(context, null, discoveryConfiguration, null, null, emptyList(), page);
verify(discoverQueryBuilder, times(1))
.buildQuery(context, null, discoveryConfiguration, null, emptyList(), emptyList(),
page.getPageSize(), page.getOffset(), null, null);
}
@Test(expected = DSpaceBadRequestException.class)
public void testCatchIllegalArgumentException() throws Exception {
when(discoverQueryBuilder.buildQuery(any(), any(), any(), any(), any(), anyList(), any(), any(), any(),

View File

@@ -865,8 +865,11 @@
<!--The sort filters for the discovery search-->
<property name="searchSortConfiguration">
<bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration">
<!--The default sort filter to use for the initial workspace loading-->
<property name="defaultSortField" ref="sortLastModified" />
<property name="sortFields">
<list>
<ref bean="sortLastModified" />
<ref bean="sortScore" />
<ref bean="sortTitle" />
<ref bean="sortDateIssued" />
@@ -938,6 +941,8 @@
<!--The sort filters for the discovery search-->
<property name="searchSortConfiguration">
<bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration">
<!--The default sort filter to use for the initial workspace loading-->
<property name="defaultSortField" ref="sortLastModified" />
<property name="sortFields">
<list>
<ref bean="sortLastModified" />
@@ -1015,6 +1020,7 @@
<bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration">
<property name="sortFields">
<list>
<ref bean="sortLastModified" />
<ref bean="sortScore" />
<ref bean="sortTitle" />
<ref bean="sortDateIssued" />

View File

@@ -283,6 +283,7 @@
<!-- used by the DSpace Discovery Solr Indexer to track the last time a document was indexed -->
<field name="SolrIndexer.lastIndexed" type="date" indexed="true" stored="true" default="NOW" multiValued="false" omitNorms="true" />
<field name="lastModified" type="date" indexed="true" stored="true" default="NOW" multiValued="false" omitNorms="true" />
<copyField source="lastModified" dest="lastModified_dt" />
<!-- used to filter out items that are older versions of another item -->
<field name="latestVersion" type="boolean" indexed="true" stored="true" default="true" multiValued="false" omitNorms="true"/>