Skip to content
Merged
1 change: 1 addition & 0 deletions checkstyle-suppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@
on JMockIt Expectations blocks and similar. See https://github.com/checkstyle/checkstyle/issues/3739 -->
<suppress checks="Indentation" files="src[/\\]test[/\\]java"/>
<suppress checks="Regexp" files="DSpaceHttpClientFactory\.java"/>
<suppress checks="Regexp" files="OpenAIRERestConnectorTest\.java"/>
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a checkstyle suppression for OpenAIRERestConnectorTest.java weakens the repo’s enforced convention (avoid direct HttpClientBuilder.create() usage). It would be better to adjust the test to comply (e.g., mock CloseableHttpClient) and remove this suppression.

Suggested change
<suppress checks="Regexp" files="OpenAIRERestConnectorTest\.java"/>

Copilot uses AI. Check for mistakes.
</suppressions>
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,11 @@ public InputStream get(String file, String accessToken) {
break;
}

// do not close this httpClient
result = getResponse.getEntity().getContent();
// the client will be closed, we need to copy the response stream to a new one that we can return
try (InputStream is = getResponse.getEntity().getContent()) {
byte[] bytes = is.readAllBytes();
result = new java.io.ByteArrayInputStream(bytes);
}
Comment on lines +210 to +214
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get() now reads the entire HTTP response into memory via readAllBytes() before returning a ByteArrayInputStream. For large OpenAIRE responses this can cause high memory usage. Consider refactoring so unmarshalling happens inside the HTTP-client try-with-resources (or return an InputStream that closes the underlying response/client on close) to keep streaming behavior without leaking connections.

Copilot uses AI. Check for mistakes.
}
} catch (MalformedURLException e1) {
getGotError(e1, url + '/' + file);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,19 @@ public int getNumberOfResults(String query) {
String encodedQuery = encodeValue(query);

Response projectResponse = connector.searchProjectByKeywords(0, 0, encodedQuery);
return Integer.parseInt(projectResponse.getHeader().getTotal());
if (projectResponse == null || projectResponse.getHeader() == null) {
return 0;
}
String total = projectResponse.getHeader().getTotal();
if (StringUtils.isBlank(total)) {
return 0;
}
try {
return Integer.parseInt(total);
} catch (NumberFormatException e) {
log.error("Failed to parse search result count from OpenAIRE: {}", e.getMessage());
return 0;
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/
package org.dspace.storage.bitstore;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.sql.SQLException;
Expand Down Expand Up @@ -185,12 +186,17 @@ public Map computeChecksumSpecStore(Context context, Bitstream bitstream, int st
}

@Override
public InputStream retrieve(Context context, Bitstream bitstream)
throws SQLException, IOException {
public InputStream retrieve(Context context, Bitstream bitstream) throws SQLException, IOException {
int storeNumber = this.whichStoreNumber(bitstream);
return this.getStore(storeNumber).get(bitstream);
}

@Override
public File retrieveFile(Context context, Bitstream bitstream) throws IOException {
int storeNumber = whichStoreNumber(bitstream);
return this.getStore(storeNumber).getFile(bitstream);
}

@Override
public void cleanup(boolean deleteDbRecords, boolean verbose) throws SQLException, IOException, AuthorizeException {
Context context = new Context(Context.Mode.BATCH_EDIT);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ protected void copyRelationships(
*/
private void manageRelationMetadata(Context c, Item itemNew, Item previousItem) throws SQLException {
// Remove copied `dc.relation.replaces` metadata for the new item.
itemService.clearMetadata(c, itemNew, "dc", "relation", "replaces", null);
itemService.clearMetadata(c, itemNew, "dc", "relation", "replaces", Item.ANY);

// Add metadata `dc.relation.replaces` to the new item.
// The metadata value is: `dc.identifier.uri` from the previous item.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,29 @@ public WorkspaceItemBuilder withFulltext(String name, String source, InputStream
return this;
}

/**
* Add bitstream with specific store number.
*
* @param name bitstream name
* @param source bitstream test source location
* @param is input stream of the bitstream
* @param storeNumber store number
*
* @return this WorkspaceItemBuilder
*/
public WorkspaceItemBuilder withBitstream(String name, String source, InputStream is, int storeNumber) {
try {
Item item = workspaceItem.getItem();
Bitstream b = itemService.createSingleBitstream(context, is, item);
b.setStoreNumber(storeNumber);
b.setName(context, name);
b.setSource(context, source);
} catch (Exception e) {
handleException(e);
}
return this;
}

/**
* Create workspaceItem with any metadata
* @param schema metadataSchema name e.g. `dc`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* 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.external;

import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;

import eu.openaire.jaxb.model.Response;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.dspace.app.client.DSpaceHttpClientFactory;
import org.junit.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;


public class OpenAIRERestConnectorTest {

@Test
public void searchProjectByKeywords() {
try (InputStream is = this.getClass().getResourceAsStream("openaire-projects.xml");
MockWebServer mockServer = new MockWebServer()) {
String projects = new String(is.readAllBytes(), StandardCharsets.UTF_8)
.replaceAll("( mushroom)", "( DEADBEEF)");
mockServer.enqueue(new MockResponse().setResponseCode(200).setBody(projects));

// setup mocks so we don't have to set whole DSpace kernel etc.
// still, the idea is to test how the get method behaves
CloseableHttpClient httpClient = spy(HttpClientBuilder.create().build());
doReturn(httpClient.execute(new HttpGet(mockServer.url("").toString())))
.when(httpClient).execute(Mockito.any());

Comment on lines +41 to +46
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test uses HttpClientBuilder.create() (disallowed by the repository’s checkstyle Regexp rule) and relies on a checkstyle suppression. Prefer mocking CloseableHttpClient directly (or using the existing DSpaceHttpClientFactory abstraction) so the suppression in checkstyle-suppressions.xml isn’t needed.

Copilot uses AI. Check for mistakes.
DSpaceHttpClientFactory mock = Mockito.mock(DSpaceHttpClientFactory.class);
when(mock.build()).thenReturn(httpClient);

try (MockedStatic<DSpaceHttpClientFactory> mockedFactory =
Mockito.mockStatic(DSpaceHttpClientFactory.class)) {
mockedFactory.when(DSpaceHttpClientFactory::getInstance).thenReturn(mock);
OpenAIRERestConnector connector = new OpenAIRERestConnector(mockServer.url("").toString());
Response response = connector.searchProjectByKeywords(0, 10, "keyword");
// Basically check it doesn't throw UnmarshallerException and that we are getting our mocked response
assertTrue("Expected the query to contain the replaced keyword",
response.getHeader().getQuery().contains("DEADBEEF"));
}
} catch (IOException e) {
e.printStackTrace();
}
Comment on lines +59 to +61
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The IOException is caught and only printStackTrace() is called, which can let the test pass silently on failures and is generally discouraged. Please fail the test (e.g., rethrow as AssertionError / use fail(...)) so an unexpected IOException causes a test failure.

Copilot uses AI. Check for mistakes.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
import java.util.List;
import java.util.Optional;

import eu.openaire.jaxb.model.Response;
import org.dspace.AbstractDSpaceTest;
import org.dspace.external.OpenAIRERestConnector;
import org.dspace.external.factory.ExternalServiceFactory;
import org.dspace.external.model.ExternalDataObject;
import org.dspace.external.provider.ExternalDataProvider;
Expand Down Expand Up @@ -102,4 +104,21 @@ public void testGetDataObjectWInvalidId() {

assertTrue("openAIREFunding.getExternalDataObject.notExists:WRONGID", result.isEmpty());
}

@Test
public void testGetNumberOfResultsWhenResponseIsNull() {
// Create a mock connector that returns null
OpenAIREFundingDataProvider provider = new OpenAIREFundingDataProvider();
provider.setSourceIdentifier("test");
provider.setConnector(new OpenAIRERestConnector("test") {
@Override
public Response searchProjectByKeywords(int page, int size, String... keywords) {
return null;
}
});

// Should return 0 when response is null, not throw NullPointerException
int result = provider.getNumberOfResults("test");
assertEquals("Should return 0 when response is null", 0, result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/**
* 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.identifier;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.hamcrest.MatcherAssert.assertThat;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.TimeZone;

import org.dspace.AbstractIntegrationTestWithDatabase;
import org.dspace.builder.CollectionBuilder;
import org.dspace.builder.CommunityBuilder;
import org.dspace.builder.ItemBuilder;
import org.dspace.builder.VersionBuilder;
import org.dspace.content.Collection;
import org.dspace.content.Item;
import org.dspace.content.MetadataValue;
import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.InstallItemService;
import org.dspace.content.service.ItemService;
import org.dspace.kernel.ServiceManager;
import org.dspace.services.factory.DSpaceServicesFactory;
import org.dspace.workflow.WorkflowItem;
import org.dspace.workflow.WorkflowItemService;
import org.dspace.workflow.factory.WorkflowServiceFactory;
import org.junit.Before;
import org.junit.Test;

/**
* Unit Tests for ClarinVersionedHandleIdentifierProvider
*
* @authorMilan Kuchtiak
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Javadoc typo: @authorMilan Kuchtiak is missing a space after @author, so it won’t render as intended. Please change it to @author Milan Kuchtiak.

Suggested change
* @authorMilan Kuchtiak
* @author Milan Kuchtiak

Copilot uses AI. Check for mistakes.
*/
public class ClarinVersionedHandleIdentifierProviderIT extends AbstractIntegrationTestWithDatabase {
private IdentifierServiceImpl identifierService;
private InstallItemService installItemService;
private ItemService itemService;
private WorkflowItemService workflowItemService;

private Collection collection;

@Before
@Override
public void setUp() throws Exception {
super.setUp();
context.turnOffAuthorisationSystem();

ServiceManager serviceManager = DSpaceServicesFactory.getInstance().getServiceManager();
identifierService = serviceManager.getServicesByType(IdentifierServiceImpl.class).get(0);

itemService = ContentServiceFactory.getInstance().getItemService();
installItemService = ContentServiceFactory.getInstance().getInstallItemService();
workflowItemService = WorkflowServiceFactory.getInstance().getWorkflowItemService();

// Clean out providers to avoid any being used for creation of community and collection
identifierService.setProviders(new ArrayList<>());

parentCommunity = CommunityBuilder.createCommunity(context)
.withName("Parent Community")
.build();
collection = CollectionBuilder.createCollection(context, parentCommunity)
.withName("Collection")
.build();
}

@Test
public void testNewVersionMetadata() throws Exception {
registerProvider(ClarinVersionedHandleIdentifierProvider.class);
Item itemV1 = ItemBuilder.createItem(context, collection)
.withTitle("First version")
.build();

// new item "dc.relation.replaces" metadata has to be set to this value
String itemV1HandleRef = itemService.getMetadataFirstValue(itemV1, "dc", "identifier", "uri", Item.ANY);

// set "dc.relation.replaces" metadata on itemV1
itemService.addMetadata(context, itemV1, "dc", "relation", "replaces", null, "some_value");
// replace "dc.date.available" metadata on itemV1 to some old value
itemService.clearMetadata(context, itemV1, "dc", "date", "available", Item.ANY);
itemService.addMetadata(context, itemV1, "dc", "date", "available", null, "2020-01-01");
// simulate itemV1 having a DOI identifier assigned
itemService.addMetadata(context, itemV1, "dc", "identifier", "doi", null,
"https://handle.stage.datacite.org/10.5072/dspace-1");

Item itemV2 = VersionBuilder.createVersion(context, itemV1, "Second version").build().getItem();

// check that "dc.date.available", metadata is not copied to itemV2
assertThat(itemService.getMetadata(itemV2, "dc", "date", "available", Item.ANY).size(), equalTo(0));

// check that "dc.identifier.uri", metadata is not copied to itemV2
assertThat(itemService.getMetadata(itemV2, "dc", "identifier", "uri", Item.ANY).size(), equalTo(0));

// check that "dc.identifier.doi", metadata is not copied to itemV2
assertThat(itemService.getMetadata(itemV2, "dc", "identifier", "doi", Item.ANY).size(), equalTo(0));

// check that "dc.relation.replaces" points to itemV1
List<MetadataValue> metadataValues = itemService.getMetadata(itemV2, "dc", "relation", "replaces", Item.ANY);
assertThat(metadataValues.size(), equalTo(1));
assertThat(metadataValues.get(0).getValue(), equalTo(itemV1HandleRef));

WorkflowItem workflowItem = workflowItemService.create(context, itemV2, collection);
Item installedItem = installItemService.installItem(context, workflowItem);

// get current date
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
calendar.setTimeZone(TimeZone.getTimeZone("UTC"));
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String date = sdf.format(calendar.getTime());

// check that "dc.relation.replaces" points to itemV1
metadataValues = itemService.getMetadata(installedItem, "dc", "relation", "replaces", Item.ANY);
assertThat(metadataValues.size(), equalTo(1));
assertThat(metadataValues.get(0).getValue(), equalTo(itemV1HandleRef));

// Check that itemV2 has the correct "dc.date.available" metadata set to current date
metadataValues = itemService.getMetadata(installedItem, "dc", "date", "available", Item.ANY);
assertThat(metadataValues.size(), equalTo(1));
assertThat(metadataValues.get(0).getValue(), startsWith(date));

// check "dc.identifier.uri" metadata has new value different from itemV1
metadataValues = itemService.getMetadata(installedItem, "dc", "identifier", "uri", Item.ANY);
assertThat(metadataValues.size(), equalTo(1));
assertThat(metadataValues.get(0).getValue(), not(itemV1HandleRef));
}

private void registerProvider(Class type) {
// Register our new provider
IdentifierProvider identifierProvider =
(IdentifierProvider) DSpaceServicesFactory.getInstance().getServiceManager()
.getServiceByName(type.getName(), type);
if (identifierProvider == null) {
DSpaceServicesFactory.getInstance().getServiceManager().registerServiceClass(type.getName(), type);
identifierProvider = (IdentifierProvider) DSpaceServicesFactory.getInstance().getServiceManager()
.getServiceByName(type.getName(), type);
}

// Overwrite the identifier-service's providers with the new one to ensure only this provider is used
identifierService = DSpaceServicesFactory.getInstance().getServiceManager()
.getServicesByType(IdentifierServiceImpl.class).get(0);
identifierService.setProviders(new ArrayList<>());
identifierService.setProviders(List.of(identifierProvider));
}
}
Loading