mirror of
https://github.com/DSpace/DSpace.git
synced 2025-10-07 01:54:22 +00:00
[TLC-249] Refactor identifiers create endpoint
This commit is contained in:
@@ -44,7 +44,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
@RestController
|
||||
@RequestMapping("/api/" + IdentifierRestController.CATEGORY)
|
||||
public class IdentifierRestController implements InitializingBean {
|
||||
public static final String CATEGORY = "pid";
|
||||
public static final String CATEGORY = "pid_SDFSDFSDFSDF";
|
||||
|
||||
public static final String ACTION = "find";
|
||||
|
||||
|
@@ -1,166 +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;
|
||||
|
||||
import static org.dspace.app.rest.utils.RegexUtils.REGEX_REQUESTMAPPING_IDENTIFIER_AS_UUID;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.UUID;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.dspace.app.rest.converter.ConverterService;
|
||||
import org.dspace.app.rest.converter.MetadataConverter;
|
||||
import org.dspace.app.rest.model.IdentifiersRest;
|
||||
import org.dspace.app.rest.model.ItemRest;
|
||||
import org.dspace.app.rest.model.hateoas.ItemResource;
|
||||
import org.dspace.app.rest.repository.ItemRestRepository;
|
||||
import org.dspace.app.rest.utils.ContextUtil;
|
||||
import org.dspace.app.rest.utils.Utils;
|
||||
import org.dspace.authorize.AuthorizeException;
|
||||
import org.dspace.content.Item;
|
||||
import org.dspace.content.logic.TrueFilter;
|
||||
import org.dspace.content.service.ItemService;
|
||||
import org.dspace.core.Context;
|
||||
import org.dspace.identifier.DOI;
|
||||
import org.dspace.identifier.DOIIdentifierProvider;
|
||||
import org.dspace.identifier.IdentifierException;
|
||||
import org.dspace.identifier.IdentifierNotFoundException;
|
||||
import org.dspace.identifier.service.DOIService;
|
||||
import org.dspace.identifier.service.IdentifierService;
|
||||
import org.dspace.services.factory.DSpaceServicesFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.rest.webmvc.ControllerUtils;
|
||||
import org.springframework.data.rest.webmvc.ResourceNotFoundException;
|
||||
import org.springframework.hateoas.RepresentationModel;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* Controller to register a DOI for an item, if it has no DOI already, or a DOI in a state where it can be
|
||||
* advanced to queue for reservation or registration.
|
||||
*
|
||||
* @author Kim Shepherd
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/" + ItemRest.CATEGORY + "/" + ItemRest.PLURAL_NAME + REGEX_REQUESTMAPPING_IDENTIFIER_AS_UUID
|
||||
+ "/" + IdentifiersRest.NAME)
|
||||
public class ItemIdentifierController {
|
||||
|
||||
@Autowired
|
||||
ConverterService converter;
|
||||
|
||||
@Autowired
|
||||
ItemService itemService;
|
||||
|
||||
@Autowired
|
||||
ItemRestRepository itemRestRepository;
|
||||
|
||||
@Autowired
|
||||
MetadataConverter metadataConverter;
|
||||
|
||||
@Autowired
|
||||
IdentifierService identifierService;
|
||||
|
||||
@Autowired
|
||||
DOIService doiService;
|
||||
|
||||
@Autowired
|
||||
Utils utils;
|
||||
|
||||
/**
|
||||
* Request that an identifier of a given type is 'created' for an item. Depending on the identifier type
|
||||
* this could mean minting, registration, reservation, queuing for registration later, etc.
|
||||
*
|
||||
* @return 201 CREATED on success, or 302 FOUND if already created, or an error
|
||||
*/
|
||||
@RequestMapping(method = RequestMethod.POST)
|
||||
@PreAuthorize("hasPermission(#uuid, 'ITEM', 'ADMIN')")
|
||||
public ResponseEntity<RepresentationModel<?>> registerIdentifierForItem(@PathVariable UUID uuid,
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
@RequestParam(name = "type") String type)
|
||||
throws SQLException, AuthorizeException {
|
||||
Context context = ContextUtil.obtainContext(request);
|
||||
|
||||
Item item = itemService.find(context, uuid);
|
||||
|
||||
|
||||
if (item == null) {
|
||||
throw new ResourceNotFoundException("Could not find item with id " + uuid);
|
||||
}
|
||||
|
||||
// Check for a valid identifier type and register the appropriate type of identifier
|
||||
if ("doi".equals(type)) {
|
||||
return registerDOI(context, item);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Valid identifier type (eg. 'doi') is required, parameter name 'type'");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue a new, pending or minted DOI for registration for a given item. Requires administrative privilege.
|
||||
* This request is sent from the Register DOI button (configurable) on the item status page.
|
||||
*
|
||||
* @return 302 FOUND if the DOI is already registered or reserved, 201 CREATED if queued for registration
|
||||
*/
|
||||
private ResponseEntity<RepresentationModel<?>> registerDOI(Context context, Item item)
|
||||
throws SQLException, AuthorizeException {
|
||||
String identifier = null;
|
||||
HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
|
||||
ItemRest itemRest;
|
||||
try {
|
||||
DOIIdentifierProvider doiIdentifierProvider = DSpaceServicesFactory.getInstance().getServiceManager()
|
||||
.getServiceByName("org.dspace.identifier.DOIIdentifierProvider", DOIIdentifierProvider.class);
|
||||
if (doiIdentifierProvider != null) {
|
||||
DOI doi = doiService.findDOIByDSpaceObject(context, item);
|
||||
boolean exists = false;
|
||||
boolean pending = false;
|
||||
if (null != doi) {
|
||||
exists = true;
|
||||
Integer doiStatus = doiService.findDOIByDSpaceObject(context, item).getStatus();
|
||||
// Check if this DOI has a status which makes it eligible for registration
|
||||
if (null == doiStatus || DOIIdentifierProvider.MINTED.equals(doiStatus)
|
||||
|| DOIIdentifierProvider.PENDING.equals(doiStatus)) {
|
||||
pending = true;
|
||||
}
|
||||
}
|
||||
if (!exists || pending) {
|
||||
// Mint identifier and return 201 CREATED
|
||||
doiIdentifierProvider.register(context, item, new TrueFilter());
|
||||
httpStatus = HttpStatus.CREATED;
|
||||
} else {
|
||||
// This DOI exists and isn't in a state where it can be queued for registration
|
||||
// We'll return 302 FOUND to indicate it's here and not an error, but no creation was performed
|
||||
httpStatus = HttpStatus.FOUND;
|
||||
}
|
||||
} else {
|
||||
throw new IllegalStateException("No DOI provider is configured");
|
||||
}
|
||||
} catch (IdentifierNotFoundException e) {
|
||||
httpStatus = HttpStatus.NOT_FOUND;
|
||||
} catch (IdentifierException e) {
|
||||
throw new IllegalStateException("Failed to register identifier: " + identifier);
|
||||
}
|
||||
// We didn't exactly change the item, but we did queue an identifier which is closely associated with it
|
||||
// so we should update the last modified date here
|
||||
itemService.updateLastModified(context, item);
|
||||
itemRest = converter.toRest(item, utils.obtainProjection());
|
||||
context.complete();
|
||||
ItemResource itemResource = converter.toResource(itemRest);
|
||||
// Return the status and item resource
|
||||
return ControllerUtils.toResponseEntity(httpStatus, new HttpHeaders(), itemResource);
|
||||
}
|
||||
}
|
@@ -125,6 +125,7 @@ public class RestResourceController implements InitializingBean {
|
||||
// this doesn't work as we don't have an active http request
|
||||
// see https://github.com/spring-projects/spring-hateoas/issues/408
|
||||
// Link l = linkTo(this.getClass(), r).withRel(r);
|
||||
log.error(r);
|
||||
String[] split = r.split("\\.", 2);
|
||||
String plural = English.plural(split[1]);
|
||||
Link l = Link.of("/api/" + split[0] + "/" + plural, plural);
|
||||
|
@@ -8,6 +8,7 @@
|
||||
package org.dspace.app.rest.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.dspace.app.rest.RestResourceController;
|
||||
|
||||
/**
|
||||
* Implementation of IdentifierRest REST resource, representing some DSpace identifier
|
||||
@@ -15,7 +16,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
*
|
||||
* @author Kim Shepherd <kim@shepherd.nz>
|
||||
*/
|
||||
public class IdentifierRest implements RestModel {
|
||||
public class IdentifierRest extends BaseObjectRest<String> implements RestModel {
|
||||
|
||||
// Set names used in component wiring
|
||||
public static final String NAME = "identifier";
|
||||
@@ -51,6 +52,11 @@ public class IdentifierRest implements RestModel {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypePlural() {
|
||||
return PLURAL_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the identifier value eg full DOI URL
|
||||
* @return identifier value eg. https://doi.org/123/234
|
||||
@@ -98,4 +104,14 @@ public class IdentifierRest implements RestModel {
|
||||
public void setIdentifierStatus(String identifierStatus) {
|
||||
this.identifierStatus = identifierStatus;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return "ppid";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class getController() {
|
||||
return RestResourceController.class;
|
||||
}
|
||||
}
|
||||
|
@@ -18,7 +18,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
*
|
||||
* @author Kim Shepherd <kim@shepherd.nz>
|
||||
*/
|
||||
public class IdentifiersRest implements RestModel {
|
||||
public class IdentifiersRest extends BaseObjectRest<String> {
|
||||
|
||||
// Set names used in component wiring
|
||||
public static final String NAME = "identifiers";
|
||||
@@ -44,4 +44,14 @@ public class IdentifiersRest implements RestModel {
|
||||
public void setIdentifiers(List<IdentifierRest> identifiers) {
|
||||
this.identifiers = identifiers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class getController() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 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.IdentifierRest;
|
||||
import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource;
|
||||
import org.dspace.app.rest.utils.Utils;
|
||||
|
||||
/**
|
||||
*
|
||||
* Simple HAL wrapper for IdentifierRest model
|
||||
*
|
||||
* @author Kim Shepherd
|
||||
*/
|
||||
@RelNameDSpaceResource(IdentifierRest.NAME)
|
||||
public class IdentifierResource extends DSpaceResource<IdentifierRest> {
|
||||
public IdentifierResource(IdentifierRest model, Utils utils) {
|
||||
super(model, utils);
|
||||
}
|
||||
}
|
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* 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.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.NotSupportedException;
|
||||
|
||||
import org.dspace.app.rest.DiscoverableEndpointsService;
|
||||
import org.dspace.app.rest.IdentifierRestController;
|
||||
import org.dspace.app.rest.Parameter;
|
||||
import org.dspace.app.rest.SearchRestMethod;
|
||||
import org.dspace.app.rest.exception.LinkNotFoundException;
|
||||
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.IdentifierRest;
|
||||
import org.dspace.app.rest.repository.handler.service.UriListHandlerService;
|
||||
import org.dspace.authorize.AuthorizeException;
|
||||
import org.dspace.content.DSpaceObject;
|
||||
import org.dspace.content.Item;
|
||||
import org.dspace.content.logic.TrueFilter;
|
||||
import org.dspace.content.service.ItemService;
|
||||
import org.dspace.core.Context;
|
||||
import org.dspace.handle.service.HandleService;
|
||||
import org.dspace.identifier.DOI;
|
||||
import org.dspace.identifier.DOIIdentifierProvider;
|
||||
import org.dspace.identifier.IdentifierException;
|
||||
import org.dspace.identifier.service.DOIService;
|
||||
import org.dspace.identifier.service.IdentifierService;
|
||||
import org.dspace.services.factory.DSpaceServicesFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Controller for exposition of vocabularies entry details for the submission
|
||||
*
|
||||
* @author Andrea Bollini (andrea.bollini at 4science.it)
|
||||
*/
|
||||
@Component("pid.identifier")
|
||||
public class IdentifierRestRepository extends DSpaceRestRepository<IdentifierRest, String> {
|
||||
@Autowired
|
||||
private DiscoverableEndpointsService discoverableEndpointsService;
|
||||
@Autowired
|
||||
private UriListHandlerService uriListHandlerService;
|
||||
@Autowired
|
||||
private DOIService doiService;
|
||||
@Autowired
|
||||
private HandleService handleService;
|
||||
@Autowired
|
||||
private ItemService itemService;
|
||||
@Autowired
|
||||
private IdentifierService identifierService;
|
||||
|
||||
@PreAuthorize("permitAll()")
|
||||
@Override
|
||||
public Page<IdentifierRest> findAll(Context context, Pageable pageable) {
|
||||
List<IdentifierRest> results = new ArrayList<>();
|
||||
//return converter.toRestPage(results, pageable, utils.obtainProjection());
|
||||
return new PageImpl<>(results, pageable, 0);
|
||||
}
|
||||
|
||||
@PreAuthorize("permitAll()")
|
||||
@Override
|
||||
public IdentifierRest findOne(Context context, String identifier) {
|
||||
DSpaceObject dso;
|
||||
IdentifierRest identifierRest = new IdentifierRest();
|
||||
try {
|
||||
// Resolve to an object first - if that fails then this is not a valid identifier anyway.
|
||||
dso = identifierService.resolve(context, identifier);
|
||||
if (dso != null) {
|
||||
// DSpace has no concept of a higher-level Identifier object, so in order to detect the type
|
||||
// and return sufficient information, we have to try the identifier types we know are currently
|
||||
// supported.
|
||||
// First, try to resolve to a handle.
|
||||
dso = handleService.resolveToObject(context, identifier);
|
||||
if (dso == null) {
|
||||
// No object found for a handle, try DOI
|
||||
DOI doi = doiService.findByDoi(context, identifier);
|
||||
if (doi != null) {
|
||||
String doiUrl = doiService.DOIToExternalForm(doi.getDoi());
|
||||
identifierRest.setIdentifierType("doi");
|
||||
identifierRest.setIdentifierStatus(DOIIdentifierProvider.statusText[doi.getStatus()]);
|
||||
identifierRest.setValue(doiUrl);
|
||||
}
|
||||
} else {
|
||||
// Handle found
|
||||
identifierRest.setIdentifierType("handle");
|
||||
identifierRest.setIdentifierStatus(null);
|
||||
identifierRest.setValue(handleService.getCanonicalForm(dso.getHandle()));
|
||||
}
|
||||
} else {
|
||||
throw new LinkNotFoundException(IdentifierRestController.CATEGORY, IdentifierRest.NAME, identifier);
|
||||
}
|
||||
} catch (SQLException | IdentifierException e) {
|
||||
throw new LinkNotFoundException(IdentifierRestController.CATEGORY, IdentifierRest.NAME, identifier);
|
||||
}
|
||||
return identifierRest;
|
||||
}
|
||||
|
||||
@SearchRestMethod(name = "findByItem")
|
||||
@PreAuthorize("permitAll()")
|
||||
public Page<IdentifierRest> findByItem(@Parameter(value = "uuid", required = true)
|
||||
String uuid, Pageable pageable) {
|
||||
Context context = obtainContext();
|
||||
List<IdentifierRest> results = new ArrayList<>();
|
||||
try {
|
||||
DSpaceObject dso = itemService.find(context, UUID.fromString(uuid));
|
||||
String handle = dso.getHandle();
|
||||
DOI doi = doiService.findDOIByDSpaceObject(context, dso);
|
||||
if (doi != null) {
|
||||
String doiUrl = doiService.DOIToExternalForm(doi.getDoi());
|
||||
results.add(new IdentifierRest(doiUrl, "doi", DOIIdentifierProvider.statusText[doi.getStatus()]));
|
||||
}
|
||||
if (handle != null) {
|
||||
String handleUrl = handleService.getCanonicalForm(handle);
|
||||
results.add(new IdentifierRest(handleUrl, "handle", null));
|
||||
}
|
||||
} catch (SQLException | IdentifierException e) {
|
||||
throw new LinkNotFoundException(IdentifierRestController.CATEGORY, IdentifierRest.NAME, uuid);
|
||||
}
|
||||
// Return list of identifiers for this DSpaceObject
|
||||
return new PageImpl<>(results, pageable, results.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Right now, the only supported identifier type is DOI
|
||||
* @param context
|
||||
* the dspace context
|
||||
* @param list
|
||||
* The list of Strings that will be used as data for the object that's to be created
|
||||
* This list is retrieved from the uri-list body
|
||||
* @return
|
||||
* @throws AuthorizeException
|
||||
* @throws SQLException
|
||||
* @throws RepositoryMethodNotImplementedException
|
||||
*/
|
||||
@Override
|
||||
protected IdentifierRest createAndReturn(Context context, List<String> list)
|
||||
throws AuthorizeException, SQLException, RepositoryMethodNotImplementedException {
|
||||
HttpServletRequest request = getRequestService().getCurrentRequest().getHttpServletRequest();
|
||||
// Extract 'type' from request
|
||||
String type = request.getParameter("type");
|
||||
if (!"doi".equals(type)) {
|
||||
throw new NotSupportedException("Only identifiers of type 'doi' are supported");
|
||||
}
|
||||
IdentifierRest identifierRest = new IdentifierRest();
|
||||
try {
|
||||
Item item = uriListHandlerService.handle(context, request, list, Item.class);
|
||||
if (item == null) {
|
||||
throw new UnprocessableEntityException(
|
||||
"No DSpace Item found, the uri-list does not contain a valid resource");
|
||||
}
|
||||
// Does this item have a DOI already? If the DOI doesn't exist or has a null, MINTED or PENDING status
|
||||
// then we proceed with a typical create operation and return 201 success with the object
|
||||
DOI doi = doiService.findDOIByDSpaceObject(context, item);
|
||||
if (doi == null || null == doi.getStatus() || DOIIdentifierProvider.MINTED.equals(doi.getStatus())
|
||||
|| DOIIdentifierProvider.PENDING.equals(doi.getStatus())) {
|
||||
// Proceed with creation
|
||||
// Register things
|
||||
identifierRest = registerDOI(context, item);
|
||||
} else {
|
||||
// Nothing to do here, return existing DOI
|
||||
identifierRest = new IdentifierRest(doiService.DOIToExternalForm(doi.getDoi()),
|
||||
"doi", DOIIdentifierProvider.statusText[doi.getStatus()]);
|
||||
}
|
||||
} catch (AuthorizeException e) {
|
||||
throw new RESTAuthorizationException(e);
|
||||
} catch (IdentifierException e) {
|
||||
throw new UnprocessableEntityException(e.getMessage());
|
||||
}
|
||||
return identifierRest;
|
||||
}
|
||||
|
||||
private IdentifierRest registerDOI(Context context, Item item)
|
||||
throws SQLException, AuthorizeException {
|
||||
String identifier = null;
|
||||
IdentifierRest identifierRest = new IdentifierRest();
|
||||
identifierRest.setIdentifierType("doi");
|
||||
try {
|
||||
DOIIdentifierProvider doiIdentifierProvider = DSpaceServicesFactory.getInstance().getServiceManager()
|
||||
.getServiceByName("org.dspace.identifier.DOIIdentifierProvider", DOIIdentifierProvider.class);
|
||||
if (doiIdentifierProvider != null) {
|
||||
String doiValue = doiIdentifierProvider.register(context, item, new TrueFilter());
|
||||
identifierRest.setValue(doiValue);
|
||||
// Get new status
|
||||
DOI doi = doiService.findByDoi(context, doiValue);
|
||||
if (doi != null) {
|
||||
identifierRest.setIdentifierStatus(DOIIdentifierProvider.statusText[doi.getStatus()]);
|
||||
}
|
||||
} else {
|
||||
throw new IllegalStateException("No DOI provider is configured");
|
||||
}
|
||||
} catch (IdentifierException e) {
|
||||
throw new IllegalStateException("Failed to register identifier: " + identifier);
|
||||
}
|
||||
// We didn't exactly change the item, but we did queue an identifier which is closely associated with it,
|
||||
// so we should update the last modified date here
|
||||
itemService.updateLastModified(context, item);
|
||||
context.complete();
|
||||
return identifierRest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<IdentifierRest> getDomainClass() {
|
||||
return IdentifierRest.class;
|
||||
}
|
||||
}
|
@@ -41,7 +41,7 @@
|
||||
|
||||
# Show Register DOI button in item status page?
|
||||
# Default: false
|
||||
#identifiers.item-status.registerDOI = true
|
||||
#identifiers.item-status.register-doi = true
|
||||
|
||||
# Which identifier types to show in submission step?
|
||||
# Default: handle, doi (currently the only supported identifier 'types')
|
||||
|
Reference in New Issue
Block a user