|
7 | 7 | */ |
8 | 8 | package org.dspace.app.rest.submit.step; |
9 | 9 |
|
| 10 | +import java.util.List; |
10 | 11 | import javax.servlet.http.HttpServletRequest; |
11 | 12 |
|
12 | | -import org.atteo.evo.inflector.English; |
| 13 | +import com.fasterxml.jackson.databind.JsonNode; |
| 14 | +import org.apache.commons.collections4.CollectionUtils; |
| 15 | +import org.apache.commons.lang3.StringUtils; |
| 16 | +import org.dspace.app.rest.exception.ClarinLicenseNotFoundException; |
13 | 17 | import org.dspace.app.rest.exception.UnprocessableEntityException; |
14 | | -import org.dspace.app.rest.model.BitstreamRest; |
| 18 | +import org.dspace.app.rest.model.patch.JsonValueEvaluator; |
15 | 19 | import org.dspace.app.rest.model.patch.Operation; |
16 | | -import org.dspace.app.rest.model.step.DataLicense; |
| 20 | +import org.dspace.app.rest.model.patch.ReplaceOperation; |
| 21 | +import org.dspace.app.rest.model.step.ClarinDataLicense; |
17 | 22 | import org.dspace.app.rest.submit.AbstractProcessingStep; |
18 | 23 | import org.dspace.app.rest.submit.SubmissionService; |
19 | | -import org.dspace.app.rest.submit.factory.PatchOperationFactory; |
20 | | -import org.dspace.app.rest.submit.factory.impl.PatchOperation; |
21 | 24 | import org.dspace.app.util.SubmissionStepConfig; |
22 | | -import org.dspace.content.Bitstream; |
23 | 25 | import org.dspace.content.InProgressSubmission; |
24 | | -import org.dspace.core.Constants; |
| 26 | +import org.dspace.content.Item; |
| 27 | +import org.dspace.content.MetadataValue; |
25 | 28 | import org.dspace.core.Context; |
| 29 | +import org.slf4j.Logger; |
| 30 | +import org.slf4j.LoggerFactory; |
26 | 31 |
|
27 | 32 | /** |
28 | | - * Clarin License Resource License step for DSpace Spring Rest. This Step will show license selector |
29 | | - * where the user could choose license for the bitstream. |
| 33 | + * Submission step exposing the CLARIN resource license selected for the |
| 34 | + * in-progress submission. Data is sourced from the item's {@code dc.rights*} |
| 35 | + * metadata; the selection is updated via a section-scoped patch |
| 36 | + * {@code /sections/clarin-license/select}. |
30 | 37 | * |
31 | 38 | * @author Milan Majchrak (milan.majchrak at dataquest.sk) |
32 | | - * |
33 | | - * This class is inspired by the class LicenseStep created by |
34 | | - * @author Luigi Andrea Pascarelli (luigiandrea.pascarelli at 4science.it) |
35 | | - * |
36 | 39 | */ |
37 | 40 | public class ClarinLicenseResourceStep extends AbstractProcessingStep { |
38 | 41 |
|
39 | | - private static final String DCTERMS_RIGHTSDATE = "dcterms.accessRights"; |
| 42 | + private static final Logger log = LoggerFactory.getLogger(ClarinLicenseResourceStep.class); |
| 43 | + |
| 44 | + /** |
| 45 | + * Sub-path of the section patch used to select a CLARIN license by name, |
| 46 | + * e.g. {@code /sections/clarin-license/select}. |
| 47 | + */ |
| 48 | + private static final String LICENSE_SELECT_OPERATION_ENTRY = "select"; |
40 | 49 |
|
41 | 50 | @Override |
42 | | - public DataLicense getData(SubmissionService submissionService, InProgressSubmission obj, |
43 | | - SubmissionStepConfig config) |
44 | | - throws Exception { |
45 | | - DataLicense result = new DataLicense(); |
46 | | - Bitstream bitstream = bitstreamService |
47 | | - .getBitstreamByName(obj.getItem(), Constants.LICENSE_BUNDLE_NAME, Constants.LICENSE_BITSTREAM_NAME); |
48 | | - if (bitstream != null) { |
49 | | - String acceptanceDate = bitstreamService.getMetadata(bitstream, DCTERMS_RIGHTSDATE); |
50 | | - result.setAcceptanceDate(acceptanceDate); |
51 | | - result.setUrl( |
52 | | - configurationService.getProperty("dspace.server.url") |
53 | | - + "/api/" + BitstreamRest.CATEGORY + "/" + English |
54 | | - .plural(BitstreamRest.NAME) + "/" + bitstream.getID() + "/content"); |
55 | | - result.setGranted(true); |
| 51 | + public ClarinDataLicense getData(SubmissionService submissionService, InProgressSubmission obj, |
| 52 | + SubmissionStepConfig config) { |
| 53 | + ClarinDataLicense result = new ClarinDataLicense(); |
| 54 | + Item item = obj.getItem(); |
| 55 | + if (item == null) { |
| 56 | + return result; |
| 57 | + } |
| 58 | + |
| 59 | + List<MetadataValue> name = itemService.getMetadataByMetadataString(item, "dc.rights"); |
| 60 | + List<MetadataValue> uri = itemService.getMetadataByMetadataString(item, "dc.rights.uri"); |
| 61 | + List<MetadataValue> label = itemService.getMetadataByMetadataString(item, "dc.rights.label"); |
| 62 | + |
| 63 | + if (CollectionUtils.isNotEmpty(name)) { |
| 64 | + result.setName(name.get(0).getValue()); |
| 65 | + } |
| 66 | + if (CollectionUtils.isNotEmpty(uri)) { |
| 67 | + result.setDefinition(uri.get(0).getValue()); |
| 68 | + } |
| 69 | + if (CollectionUtils.isNotEmpty(label)) { |
| 70 | + result.setLabel(label.get(0).getValue()); |
56 | 71 | } |
| 72 | + result.setGranted(CollectionUtils.isNotEmpty(name) |
| 73 | + && CollectionUtils.isNotEmpty(uri) |
| 74 | + && CollectionUtils.isNotEmpty(label)); |
57 | 75 | return result; |
58 | 76 | } |
59 | 77 |
|
60 | 78 | @Override |
61 | 79 | public void doPatchProcessing(Context context, HttpServletRequest currentRequest, InProgressSubmission source, |
62 | 80 | Operation op, SubmissionStepConfig stepConf) throws Exception { |
63 | 81 |
|
64 | | - if (op.getPath().endsWith(LICENSE_STEP_OPERATION_ENTRY)) { |
| 82 | + String path = op.getPath(); |
65 | 83 |
|
66 | | - PatchOperation<String> patchOperation = new PatchOperationFactory() |
67 | | - .instanceOf(LICENSE_STEP_OPERATION_ENTRY, op.getOp()); |
68 | | - patchOperation.perform(context, currentRequest, source, op); |
| 84 | + if (path.endsWith("/" + LICENSE_SELECT_OPERATION_ENTRY)) { |
| 85 | + if (!(op instanceof ReplaceOperation)) { |
| 86 | + throw new UnprocessableEntityException( |
| 87 | + "The operation '" + op.getOp() + "' is not supported for path " + path); |
| 88 | + } |
| 89 | + String licenseName = extractLicenseName(op); |
| 90 | + // Section endpoint: a missing or blank license name is treated as a |
| 91 | + // client error (422). The legacy `/license` path in |
| 92 | + // WorkspaceItemRestRepository intentionally treats a blank value as |
| 93 | + // "clear the current license" for backwards compatibility. |
| 94 | + if (StringUtils.isBlank(licenseName)) { |
| 95 | + throw new UnprocessableEntityException( |
| 96 | + "The patch value for path " + path + " must contain a non-empty license name."); |
| 97 | + } |
| 98 | + try { |
| 99 | + ClarinLicenseSubmissionUtils.applyLicense(context, source.getItem(), licenseName); |
| 100 | + } catch (ClarinLicenseNotFoundException ex) { |
| 101 | + // Surface invalid client input as 422 instead of leaking as 500. |
| 102 | + throw new UnprocessableEntityException(ex.getMessage(), ex); |
| 103 | + } |
| 104 | + return; |
| 105 | + } |
| 106 | + |
| 107 | + if (path.endsWith(LICENSE_STEP_OPERATION_ENTRY)) { |
| 108 | + // `granted` patches are a no-op on this section; kept for older clients. |
| 109 | + log.info("Ignoring legacy '{}/granted' patch on the CLARIN license section.", stepConf.getId()); |
| 110 | + return; |
| 111 | + } |
69 | 112 |
|
70 | | - } else { |
71 | | - throw new UnprocessableEntityException("The path " + op.getPath() + " cannot be patched"); |
| 113 | + throw new UnprocessableEntityException("The path " + path + " cannot be patched"); |
| 114 | + } |
| 115 | + |
| 116 | + /** |
| 117 | + * Extract the CLARIN license name from a JSON Patch {@link Operation}. |
| 118 | + * <p> |
| 119 | + * The submission API receives section updates as JSON Patch operations |
| 120 | + * (see {@code /sections/clarin-license/select}). The {@code value} field |
| 121 | + * of such an operation is not strongly typed: depending on the request |
| 122 | + * shape and how the JSON Patch payload was parsed upstream, it can arrive |
| 123 | + * as: |
| 124 | + * <ul> |
| 125 | + * <li>a plain {@link String}, e.g. {@code "value": "CC-BY"};</li> |
| 126 | + * <li>a {@link JsonValueEvaluator} wrapping a {@link JsonNode}, when the |
| 127 | + * payload is sent as a JSON object such as |
| 128 | + * {@code "value": { "value": "CC-BY" }} or as a bare textual node;</li> |
| 129 | + * <li>{@code null} when the client omitted the value entirely.</li> |
| 130 | + * </ul> |
| 131 | + * This helper normalizes those cases into a single {@code String} license |
| 132 | + * name (or {@code null} if no usable value is present), so the rest of the |
| 133 | + * step can call {@link ClarinLicenseSubmissionUtils#applyLicense} with a |
| 134 | + * simple value and treat missing input as a client error. |
| 135 | + * |
| 136 | + * @param op the JSON Patch operation targeting the license {@code select} path |
| 137 | + * @return the license name extracted from the operation value, or {@code null} |
| 138 | + * if the operation has no usable value (missing, null, or of an |
| 139 | + * unsupported type) |
| 140 | + */ |
| 141 | + private String extractLicenseName(Operation op) { |
| 142 | + Object value = op.getValue(); |
| 143 | + if (value == null) { |
| 144 | + return null; |
| 145 | + } |
| 146 | + if (value instanceof String) { |
| 147 | + return (String) value; |
| 148 | + } |
| 149 | + if (value instanceof JsonValueEvaluator) { |
| 150 | + JsonNode valueNode = ((JsonValueEvaluator) value).getValueNode(); |
| 151 | + if (valueNode == null) { |
| 152 | + return null; |
| 153 | + } |
| 154 | + JsonNode inner = valueNode.get("value"); |
| 155 | + if (inner != null) { |
| 156 | + return inner.asText(); |
| 157 | + } |
| 158 | + if (valueNode.isTextual()) { |
| 159 | + return valueNode.asText(); |
| 160 | + } |
72 | 161 | } |
| 162 | + log.warn("Unsupported Operation value type for license name extraction: {}", |
| 163 | + value.getClass().getName()); |
| 164 | + return null; |
73 | 165 | } |
74 | 166 | } |
0 commit comments