Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* 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.step;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonProperty.Access;

/**
* Java Bean to expose the CLARIN license section during in progress submission.
*
* @author Milan Majchrak (milan.majchrak at dataquest.sk)
*/
public class ClarinDataLicense implements SectionData {

private String name;

@JsonProperty(access = Access.READ_ONLY)
private String definition;

@JsonProperty(access = Access.READ_ONLY)
private String label;

private boolean granted = false;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getDefinition() {
return definition;
}

public void setDefinition(String definition) {
this.definition = definition;
}

public String getLabel() {
return label;
}

public void setLabel(String label) {
this.label = label;
}

public boolean isGranted() {
return granted;
}

public void setGranted(boolean granted) {
this.granted = granted;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,19 @@
import org.dspace.app.rest.repository.handler.service.UriListHandlerService;
import org.dspace.app.rest.submit.SubmissionService;
import org.dspace.app.rest.submit.UploadableStep;
import org.dspace.app.rest.submit.step.ClarinLicenseSubmissionUtils;
import org.dspace.app.rest.utils.BigMultipartFile;
import org.dspace.app.rest.utils.Utils;
import org.dspace.app.util.SubmissionConfig;
import org.dspace.app.util.SubmissionConfigReaderException;
import org.dspace.app.util.SubmissionStepConfig;
import org.dspace.authorize.AuthorizeException;
import org.dspace.authorize.service.AuthorizeService;
import org.dspace.content.Bitstream;
import org.dspace.content.Bundle;
import org.dspace.content.Collection;
import org.dspace.content.Item;
import org.dspace.content.LicenseUtils;
import org.dspace.content.MetadataValue;
import org.dspace.content.WorkspaceItem;
import org.dspace.content.clarin.ClarinLicense;
import org.dspace.content.service.BitstreamFormatService;
import org.dspace.content.service.BitstreamService;
import org.dspace.content.service.CollectionService;
Expand Down Expand Up @@ -532,12 +530,13 @@ private void maintainLicensesForItem(Context context, WorkspaceItem source, Oper
// Get item
Item item = source.getItem();
if (Objects.isNull(item)) {
// add log
log.warn("Cannot maintain CLARIN licenses: workspace item {} has no underlying item.", source.getID());
return;
}
// Get value from operation
if (!(op instanceof ReplaceOperation)) {
// add log
log.warn("Ignoring non-replace operation '{}' on license patch path for workspace item {}.",
op.getOp(), source.getID());
return;
}

Expand All @@ -555,53 +554,13 @@ private void maintainLicensesForItem(Context context, WorkspaceItem source, Oper
clarinLicenseName = jsonNodeValue.asText();
}

// Get clarin license by definition
ClarinLicense clarinLicense = clarinLicenseService.findByName(context, clarinLicenseName);
if (StringUtils.isNotBlank(clarinLicenseName) && Objects.isNull(clarinLicense)) {
throw new ClarinLicenseNotFoundException("Cannot patch workspace item with id: " + source.getID() + "," +
" because the clarin license with name: " + clarinLicenseName + " isn't supported in" +
" the CLARIN/DSpace");
}

// Clear the license metadata from the item
clarinLicenseService.clearLicenseMetadataFromItem(context, item);

// Detach the clarin licenses from the uploaded bitstreams
List<Bundle> bundles = item.getBundles(Constants.CONTENT_BUNDLE_NAME);
for (Bundle bundle : bundles) {
List<Bitstream> bitstreamList = bundle.getBitstreams();
for (Bitstream bitstream : bitstreamList) {
// in case bitstream ID exists in license table for some reason .. just remove it
this.clarinLicenseResourceMappingService.detachLicenses(context, bitstream);
}
}

// Save changes to database
itemService.update(context, item);

if (Objects.isNull(clarinLicense)) {
log.info("The clarin license is null so all item metadata for license was cleared and the" +
"licenses was detached.");
return;
}

// If the clarin license is not null that means some clarin license was updated and accepted
// Attach the new clarin license to every bitstream and add clarin license values to the item metadata.

// update item metadata with license data
clarinLicenseService.addLicenseMetadataToItem(context, clarinLicense, item);

// Attach the clarin license to the bitstreams
for (Bundle bundle : bundles) {
List<Bitstream> bitstreamList = bundle.getBitstreams();
for (Bitstream bitstream : bitstreamList) {
// in case bitstream ID exists in license table for some reason .. just remove it
this.clarinLicenseResourceMappingService.attachLicense(context, clarinLicense, bitstream);
}
// Delegate to the shared helper so the legacy `/license` path and the
// section path `/sections/clarin-license/select` apply the same logic.
try {
ClarinLicenseSubmissionUtils.applyLicense(context, item, clarinLicenseName);
} catch (ClarinLicenseNotFoundException ex) {
throw new UnprocessableEntityException(ex.getMessage(), ex);
}

// Save changes to database
itemService.update(context, item);
}

private void grantDistributionLicense(Context context, WorkspaceItem source, Operation op)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,68 +7,160 @@
*/
package org.dspace.app.rest.submit.step;

import java.util.List;
import javax.servlet.http.HttpServletRequest;

import org.atteo.evo.inflector.English;
import com.fasterxml.jackson.databind.JsonNode;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.dspace.app.rest.exception.ClarinLicenseNotFoundException;
import org.dspace.app.rest.exception.UnprocessableEntityException;
import org.dspace.app.rest.model.BitstreamRest;
import org.dspace.app.rest.model.patch.JsonValueEvaluator;
import org.dspace.app.rest.model.patch.Operation;
import org.dspace.app.rest.model.step.DataLicense;
import org.dspace.app.rest.model.patch.ReplaceOperation;
import org.dspace.app.rest.model.step.ClarinDataLicense;
import org.dspace.app.rest.submit.AbstractProcessingStep;
import org.dspace.app.rest.submit.SubmissionService;
import org.dspace.app.rest.submit.factory.PatchOperationFactory;
import org.dspace.app.rest.submit.factory.impl.PatchOperation;
import org.dspace.app.util.SubmissionStepConfig;
import org.dspace.content.Bitstream;
import org.dspace.content.InProgressSubmission;
import org.dspace.core.Constants;
import org.dspace.content.Item;
import org.dspace.content.MetadataValue;
import org.dspace.core.Context;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Clarin License Resource License step for DSpace Spring Rest. This Step will show license selector
* where the user could choose license for the bitstream.
* Submission step exposing the CLARIN resource license selected for the
* in-progress submission. Data is sourced from the item's {@code dc.rights*}
* metadata; the selection is updated via a section-scoped patch
* {@code /sections/clarin-license/select}.
*
* @author Milan Majchrak (milan.majchrak at dataquest.sk)
*
* This class is inspired by the class LicenseStep created by
* @author Luigi Andrea Pascarelli (luigiandrea.pascarelli at 4science.it)
*
*/
public class ClarinLicenseResourceStep extends AbstractProcessingStep {

private static final String DCTERMS_RIGHTSDATE = "dcterms.accessRights";
private static final Logger log = LoggerFactory.getLogger(ClarinLicenseResourceStep.class);

/**
* Sub-path of the section patch used to select a CLARIN license by name,
* e.g. {@code /sections/clarin-license/select}.
*/
private static final String LICENSE_SELECT_OPERATION_ENTRY = "select";

@Override
public DataLicense getData(SubmissionService submissionService, InProgressSubmission obj,
SubmissionStepConfig config)
throws Exception {
DataLicense result = new DataLicense();
Bitstream bitstream = bitstreamService
.getBitstreamByName(obj.getItem(), Constants.LICENSE_BUNDLE_NAME, Constants.LICENSE_BITSTREAM_NAME);
if (bitstream != null) {
String acceptanceDate = bitstreamService.getMetadata(bitstream, DCTERMS_RIGHTSDATE);
result.setAcceptanceDate(acceptanceDate);
result.setUrl(
configurationService.getProperty("dspace.server.url")
+ "/api/" + BitstreamRest.CATEGORY + "/" + English
.plural(BitstreamRest.NAME) + "/" + bitstream.getID() + "/content");
result.setGranted(true);
public ClarinDataLicense getData(SubmissionService submissionService, InProgressSubmission obj,
SubmissionStepConfig config) {
ClarinDataLicense result = new ClarinDataLicense();
Item item = obj.getItem();
if (item == null) {
return result;
}

List<MetadataValue> name = itemService.getMetadataByMetadataString(item, "dc.rights");
List<MetadataValue> uri = itemService.getMetadataByMetadataString(item, "dc.rights.uri");
List<MetadataValue> label = itemService.getMetadataByMetadataString(item, "dc.rights.label");

if (CollectionUtils.isNotEmpty(name)) {
result.setName(name.get(0).getValue());
}
if (CollectionUtils.isNotEmpty(uri)) {
result.setDefinition(uri.get(0).getValue());
}
if (CollectionUtils.isNotEmpty(label)) {
result.setLabel(label.get(0).getValue());
}
result.setGranted(CollectionUtils.isNotEmpty(name)
&& CollectionUtils.isNotEmpty(uri)
&& CollectionUtils.isNotEmpty(label));
return result;
}

@Override
public void doPatchProcessing(Context context, HttpServletRequest currentRequest, InProgressSubmission source,
Operation op, SubmissionStepConfig stepConf) throws Exception {

if (op.getPath().endsWith(LICENSE_STEP_OPERATION_ENTRY)) {
String path = op.getPath();

PatchOperation<String> patchOperation = new PatchOperationFactory()
.instanceOf(LICENSE_STEP_OPERATION_ENTRY, op.getOp());
patchOperation.perform(context, currentRequest, source, op);
if (path.endsWith("/" + LICENSE_SELECT_OPERATION_ENTRY)) {
if (!(op instanceof ReplaceOperation)) {
throw new UnprocessableEntityException(
"The operation '" + op.getOp() + "' is not supported for path " + path);
}
String licenseName = extractLicenseName(op);
// Section endpoint: a missing or blank license name is treated as a
// client error (422). The legacy `/license` path in
// WorkspaceItemRestRepository intentionally treats a blank value as
// "clear the current license" for backwards compatibility.
if (StringUtils.isBlank(licenseName)) {
throw new UnprocessableEntityException(
"The patch value for path " + path + " must contain a non-empty license name.");
}
try {
ClarinLicenseSubmissionUtils.applyLicense(context, source.getItem(), licenseName);
} catch (ClarinLicenseNotFoundException ex) {
// Surface invalid client input as 422 instead of leaking as 500.
throw new UnprocessableEntityException(ex.getMessage(), ex);
}
return;
}

if (path.endsWith(LICENSE_STEP_OPERATION_ENTRY)) {
// `granted` patches are a no-op on this section; kept for older clients.
log.info("Ignoring legacy '{}/granted' patch on the CLARIN license section.", stepConf.getId());
return;
}

} else {
throw new UnprocessableEntityException("The path " + op.getPath() + " cannot be patched");
throw new UnprocessableEntityException("The path " + path + " cannot be patched");
}
Comment thread
milanmajchrak marked this conversation as resolved.

/**
* Extract the CLARIN license name from a JSON Patch {@link Operation}.
* <p>
* The submission API receives section updates as JSON Patch operations
* (see {@code /sections/clarin-license/select}). The {@code value} field
* of such an operation is not strongly typed: depending on the request
* shape and how the JSON Patch payload was parsed upstream, it can arrive
* as:
* <ul>
* <li>a plain {@link String}, e.g. {@code "value": "CC-BY"};</li>
* <li>a {@link JsonValueEvaluator} wrapping a {@link JsonNode}, when the
* payload is sent as a JSON object such as
* {@code "value": { "value": "CC-BY" }} or as a bare textual node;</li>
* <li>{@code null} when the client omitted the value entirely.</li>
* </ul>
* This helper normalizes those cases into a single {@code String} license
* name (or {@code null} if no usable value is present), so the rest of the
* step can call {@link ClarinLicenseSubmissionUtils#applyLicense} with a
* simple value and treat missing input as a client error.
*
* @param op the JSON Patch operation targeting the license {@code select} path
* @return the license name extracted from the operation value, or {@code null}
* if the operation has no usable value (missing, null, or of an
* unsupported type)
*/
private String extractLicenseName(Operation op) {
Comment thread
vidiecan marked this conversation as resolved.
Object value = op.getValue();
if (value == null) {
return null;
}
if (value instanceof String) {
return (String) value;
}
if (value instanceof JsonValueEvaluator) {
JsonNode valueNode = ((JsonValueEvaluator) value).getValueNode();
if (valueNode == null) {
return null;
}
JsonNode inner = valueNode.get("value");
if (inner != null) {
return inner.asText();
}
if (valueNode.isTextual()) {
return valueNode.asText();
}
}
log.warn("Unsupported Operation value type for license name extraction: {}",
value.getClass().getName());
return null;
}
}
Loading
Loading