1 /*
2 * Copyright (C) 2017-2019 Centre National d'Etudes Spatiales (CNES).
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 3.0 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this library; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
17 * MA 02110-1301 USA
18 */
19 package fr.cnes.doi.client;
20
21 import static fr.cnes.doi.client.ClientMDS.METADATA_RESOURCE;
22 import fr.cnes.doi.exception.ClientMdsException;
23 import fr.cnes.doi.settings.DoiSettings;
24 import fr.cnes.doi.utils.Utils;
25 import fr.cnes.doi.utils.spec.Requirement;
26 import java.io.IOException;
27 import java.nio.charset.StandardCharsets;
28 import java.text.MessageFormat;
29 import java.util.ArrayList;
30 import java.util.Arrays;
31 import java.util.Collections;
32 import java.util.Iterator;
33 import java.util.List;
34 import java.util.Map;
35 import java.util.logging.Level;
36 import javax.xml.XMLConstants;
37 import javax.xml.bind.JAXBException;
38 import javax.xml.bind.ValidationEvent;
39 import javax.xml.bind.ValidationEventHandler;
40 import javax.xml.bind.ValidationException;
41 import javax.xml.validation.SchemaFactory;
42 import org.datacite.schema.kernel_4.Resource;
43 import org.datacite.schema.kernel_4.Resource.Identifier;
44 import org.restlet.data.ChallengeScheme;
45 import org.restlet.data.CharacterSet;
46 import org.restlet.data.Form;
47 import org.restlet.data.Language;
48 import org.restlet.data.MediaType;
49 import org.restlet.data.Parameter;
50 import org.restlet.data.Reference;
51 import org.restlet.data.Status;
52 import org.restlet.ext.jaxb.JaxbRepresentation;
53 import org.restlet.representation.Representation;
54 import org.restlet.representation.StringRepresentation;
55 import org.restlet.resource.ResourceException;
56
57 /**
58 * Client to query Metadata store service at
59 * <a href="https://support.datacite.org/docs/mds-2">Datacite</a>.
60 *
61 * @author Jean-Christophe Malapert (jean-christophe.malapert@cnes.fr)
62 * @see "https://mds.datacite.org/static/apidoc"
63 */
64 @Requirement(reqId = Requirement.DOI_INTER_010, reqName = Requirement.DOI_INTER_010_NAME)
65 public class ClientMDS extends BaseClient {
66
67 /**
68 * Metadata store service endpoint {@value #DATA_CITE_URL}.
69 */
70 public static final String DATA_CITE_URL = "https://mds.datacite.org";
71
72 /**
73 * Metadata store mock service endpoint {@value #DATA_CITE_MOCK_URL}.
74 */
75 public static final String DATA_CITE_MOCK_URL = "http://localhost:" + DATACITE_MOCKSERVER_PORT;
76
77 /**
78 * Metadata store test service endpoint {@value #DATA_CITE_TEST_URL}.
79 */
80 public static final String DATA_CITE_TEST_URL = "https://mds.test.datacite.org";
81
82 /**
83 * DOI resource {@value #DOI_RESOURCE}.
84 */
85 public static final String DOI_RESOURCE = "doi";
86
87 /**
88 * Metadata resource {@value #METADATA_RESOURCE}.
89 */
90 public static final String METADATA_RESOURCE = "metadata";
91
92 /**
93 * Media resource {@value #MEDIA_RESOURCE}.
94 */
95 public static final String MEDIA_RESOURCE = "media";
96
97 /**
98 * Test mode sets to true.
99 */
100 public static final Parameter TEST_MODE = new Parameter("testMode", "true");
101
102 /**
103 * Test DOI prefix {@value #TEST_DOI_PREFIX}.
104 */
105 public static final String TEST_DOI_PREFIX = "10.80163";
106
107 /**
108 * DOI query parameter {@value #POST_DOI}.
109 */
110 public static final String POST_DOI = "doi";
111
112 /**
113 * URL query parameter {@value #POST_URL}.
114 */
115 public static final String POST_URL = "url";
116
117 /**
118 * Default XML schema for Datacite: {@value #SCHEMA_DATACITE}
119 */
120 public static final String SCHEMA_DATACITE = "https://schema.datacite.org/meta/kernel-4-1/metadata.xsd";
121
122 /**
123 * SCHEMA_FACTORY.
124 */
125 private static final SchemaFactory SCHEMA_FACTORY = SchemaFactory.newInstance(
126 XMLConstants.W3C_XML_SCHEMA_NS_URI
127 );
128
129 /**
130 * Loads DOI settings.
131 */
132 private static final DoiSettings DOI_SETTINGS = DoiSettings.getInstance();
133
134 /**
135 * DataCite recommends that only the following characters are used within a
136 * DOI name:
137 * <ul>
138 * <li>0-9</li>
139 * <li>a-z</li>
140 * <li>A-Z</li>
141 * <li>- (dash)</li>
142 * <li>. (dot)</li>
143 * <li>_ (underscore)</li>
144 * <li>+ (plus)</li>
145 * <li>: (colon)</li>
146 * <li>/ (slash)</li>
147 * </ul>
148 *
149 * @param test DOI name to test
150 * @throws IllegalArgumentException An exception is thrown when at least one
151 * character is not part of 0-9a-zA-Z\\-._+:/ of a DOI name
152 */
153 public static void checkIfAllCharsAreValid(final String test) {
154 if (!test.matches("^[0-9a-zA-Z\\-._+:/\\s]+$")) {
155 throw new IllegalArgumentException("Only these characters are allowed "
156 + "0-9a-zA-Z\\-._+:/ in a DOI name");
157 }
158 }
159 /**
160 * Selected test mode.
161 */
162 private final Parameter testMode;
163 /**
164 * Context.
165 */
166 private final Context context;
167
168 /**
169 * Creates a client to handle DataCite server.
170 *
171 * There is special test prefix 10.5072 available to all datacentres. Please
172 * use it for all your testing DOIs. Your real prefix should not be used for
173 * test DOIs. Note that DOIs with test prefix will behave like any other
174 * DOI, e.g. they can be normally resolved. They will not be exposed by
175 * upcoming services like search and OAI, though. Periodically we purge *
176 * all 10.5072 datasets from the system.
177 *
178 * <p>
179 * It is important to understand that the Handle System (the technical
180 * infrastructure for DOIs) is a distributed network system. The consequence
181 * of this manifests is its inherent latency. For example, DOIs have TTL
182 * (time to live) defaulted to 24 hours, so your changes will be visible to
183 * the resolution infrastructure only when the TTL expires. Also, if you
184 * create a DOI and then immediately try to update its URL, you might get
185 * the error message HANDLE NOT EXISTS. This is because it takes some time
186 * for the system to register a handle for a DOI.
187 *
188 * Each API call can have optional query parametertestMode. If set to "true"
189 * or "1" the request will not change the database nor will the DOI handle
190 * will be registered or updated, e.g. POST /doi?testMode=true and the
191 * testing prefix will be used instead of the provided prefix
192 *
193 * @param context Context using
194 * @throws fr.cnes.doi.exception.ClientMdsException Cannot the Datacite
195 * schema
196 */
197 public ClientMDS(final Context context) throws ClientMdsException {
198 super(context.getDataCiteUrl());
199 this.context = context;
200 this.testMode = this.context.hasTestMode() ? TEST_MODE : null;
201 }
202
203 /**
204 * Creates a client to handle DataCite with a HTTP Basic authentication.
205 *
206 * All the traffic goes via HTTPS - please remember we do not support bare
207 * HTTP. All the requests to this system require HTTP Basic authentication
208 * header. You will get your username and password from your local DataCite
209 * allocator. Each account have some constraints associated with it:
210 * <ul>
211 * <li>you will be allowed to mint DOIs only with prefix assigned to
212 * you</li>
213 * <li>you will be allowed to mint DOIs only with URLs in host domains
214 * assigned to you</li>
215 * <li>you might not be able to mint unlimited number of DOIs, there is a
216 * quota assigned to you by your allocator (the quota can be extended or
217 * lifted though)</li>
218 * </ul>
219 * Each API call can have optional query parametertestMode. If set to "true"
220 * or "1" the request will not change the database nor will the DOI handle
221 * will be registered or updated, e.g. POST /doi?testMode=true.
222 *
223 * @param context Context using
224 * @param login Login
225 * @param pwd password
226 * @throws fr.cnes.doi.exception.ClientMdsException Cannot the Datacite
227 * schema
228 */
229 public ClientMDS(final Context context,
230 final String login,
231 final String pwd) throws ClientMdsException {
232 this(context);
233 this.getLog().debug("Authentication with HTTP_BASIC : {}/{}",
234 login, Utils.transformPasswordToStars(pwd));
235 this.getClient().setChallengeResponse(ChallengeScheme.HTTP_BASIC, login, pwd);
236 }
237
238 /**
239 * Creates a client to handle DataCite with a HTTP Basic authentication.
240 *
241 * @param login Login
242 * @param pwd password
243 * @throws fr.cnes.doi.exception.ClientMdsException Cannot the Datacite
244 * schema
245 */
246 public ClientMDS(final String login,
247 final String pwd) throws ClientMdsException {
248 this(Context.PROD);
249 this.getLog().debug("Authentication with HTTP_BASIC : {}/{}",
250 login, Utils.transformPasswordToStars(pwd));
251 this.getClient().setChallengeResponse(ChallengeScheme.HTTP_BASIC, login, pwd);
252 }
253
254 /**
255 * Returns the {@link #TEST_MODE} or an empty parameter according to
256 * <i>isTestMode</i>
257 *
258 * @return the test mode
259 */
260 private Parameter getTestMode() {
261 return this.testMode;
262 }
263
264 /**
265 * Renames the current DOI prefix by the DOI test prefix.
266 *
267 * @param doiName real DOI name
268 * @return the renamed DOI with the test prefix
269 */
270 private String useTestPrefix(final String doiName) {
271 final String[] split = doiName.split("/");
272 split[0] = TEST_DOI_PREFIX;
273 final String testingPrefix = String.join("/", split);
274 final String message = String.format(
275 "DOI %s has been renamed as %s for testing", doiName, testingPrefix
276 );
277 this.getLog().warn(message);
278 return testingPrefix;
279 }
280
281 /**
282 * Init the client to the reference {@link #DATA_CITE_URL} or
283 * {@link #DATA_CITE_TEST_URL}
284 */
285 private void initReference() {
286 this.getClient().setReference(this.context.getDataCiteUrl());
287 }
288
289 /**
290 * Create reference. The parameter ?testMode=true is added in DEV context
291 *
292 * @param segment segment to add to the end point
293 * @return new URL
294 */
295 private Reference createReference(final String segment) {
296 this.initReference();
297 Reference url = this.getClient().addSegment(segment);
298 if (this.getTestMode() != null) {
299 url = url.addQueryParameter(this.getTestMode());
300 }
301 return url;
302 }
303
304 /**
305 * Creates the URL to query.
306 *
307 * @param segment segment to add to the end point service
308 * @param doiName doi name
309 * @return the URL to query
310 */
311 private Reference createReferenceWithDOI(final String segment,
312 final String doiName) {
313 final String requestDOI = getDoiAccorgindToContext(doiName);
314 final Reference ref = createReference(segment);
315 final String[] split = requestDOI.split("/");
316 for (final String segmentUri : split) {
317 ref.addSegment(segmentUri);
318 }
319 return ref;
320 }
321
322 /**
323 * Returns the right DOI according to the context (DEV, POST_DEV, ...). When
324 * the context has a DOI test prefix, the real DOI prefix is replaced by the
325 * DOI test prefix.
326 *
327 * @param doiName DOI name
328 * @return the right DOI
329 */
330 private String getDoiAccorgindToContext(final String doiName) {
331 return this.context.hasDoiTestPrefix() ? useTestPrefix(doiName) : doiName;
332 }
333
334 /**
335 * Checks the input parameters and specially the validity of the DOI name.
336 * The real prefix is replaced by the test prefix in DEV, POST_DEV and
337 * PRE_PROD context. The DOI prefix may replace according to the
338 * {@link ClientMDS#context}.
339 *
340 * @param form query form
341 * @throws IllegalArgumentException An exception is thrown when doi and url
342 * are not provided or when one character at least in the DOI name is not
343 * valid
344 */
345 private void checkInputForm(final Form form) throws IllegalArgumentException {
346 final Map<String, String> map = form.getValuesMap();
347 if (map.containsKey(POST_DOI) && map.containsKey(POST_URL)) {
348 final String doiName = form.getFirstValue(POST_DOI);
349 checkIfAllCharsAreValid(doiName);
350 form.set(POST_DOI, getDoiAccorgindToContext(doiName));
351 } else {
352 throw new IllegalArgumentException(MessageFormat.format(
353 "{0} and {1} parameters are required",
354 POST_DOI, POST_URL)
355 );
356 }
357 }
358
359 /**
360 * Returns the text of a response.
361 *
362 * @param rep Response of the server
363 * @return the text of the response
364 * @throws ClientMdsException An exception is thrown when cannot convert the
365 * Representation to text
366 */
367 private String getText(final Representation rep) throws ClientMdsException {
368 final String result;
369 try {
370 result = rep.getText();
371 } catch (IOException ex) {
372 throw new ClientMdsException(Status.SERVER_ERROR_INTERNAL, ex);
373 }
374 return result;
375 }
376
377 /**
378 * Returns the response as a list of String of an URI.
379 *
380 * @param segment resource name
381 * @return the response
382 * @throws ClientMdsException - if an error happens when requesting
383 * CrossCite
384 */
385 private List<String> getList(final String segment) throws ClientMdsException {
386 try {
387 final Reference ref = this.createReference(segment);
388 this.getClient().setReference(ref);
389 final Representation rep = this.getClient().get();
390 final Status status = this.getClient().getStatus();
391 if (status.isSuccess()) {
392 final String result = rep.getText();
393 return Arrays.asList(result.split("\n"));
394 } else {
395 throw new ClientMdsException(status, status.getDescription());
396 }
397 } catch (IOException | ResourceException ex) {
398 throw new ClientMdsException(Status.SERVER_ERROR_INTERNAL, ex.getMessage(), ex);
399 } finally {
400 this.getClient().release();
401 }
402 }
403
404 /**
405 * This request returns an URL associated with a given DOI. A 200 status is
406 * an operation successful. A 204 status means no content (DOI is known to
407 * MDS, but is not minted (or not resolvable e.g. due to handle's latency)
408 * The DOI prefix may replace according to the {@link ClientMDS#context}.
409 *
410 * @param doiName DOI name
411 * @return an URL or no content (DOI is known to MDS, but is not minted (or
412 * not resolvable e.g. due to handle's latency))
413 * @throws ClientMdsException - if an error happens <ul>
414 * <li>401 Unauthorized - no login</li>
415 * <li>403 - login problem or dataset belongs to another party</li>
416 * <li>404 Not Found - DOI does not exist in our database</li>
417 * <li>500 Internal Server Error - server internal error, try later and if
418 * problem persists please contact us</li>
419 * </ul>
420 * @see "https://mds.datacite.org/static/apidoc#tocAnchor-13"
421 */
422 public String getDoi(final String doiName) throws ClientMdsException {
423 final Reference url = createReferenceWithDOI(DOI_RESOURCE, doiName);
424 this.getLog().info("GET {0}", url.toString());
425
426 this.getClient().setReference(url);
427 Representation rep;
428 try {
429 rep = this.getClient().get();
430 return (rep == null) ? "" : this.getText(rep);
431 } catch (ResourceException ex) {
432 throw new ClientMdsException(ex.getStatus(), ex.getMessage(), this.getClient().
433 getResponseEntity(), ex);
434 } finally {
435 this.getClient().release();
436 }
437 }
438
439 public List<String> getDois() throws ClientMdsException {
440 return getList(DOI_RESOURCE);
441 }
442
443 /**
444 * Returns only the dois within the specified project from the search
445 * result.
446 *
447 * @param idProject project ID
448 * @return the search result
449 * @throws fr.cnes.doi.exception.ClientMdsException When an error happens
450 * with Datacite
451 */
452 public List<String> getDois(final String idProject) throws ClientMdsException {
453 final List<String> doiListFiltered = new ArrayList<>();
454 for (final String doi : this.getDois()) {
455 if (doi.contains(idProject)) {
456 doiListFiltered.add(doi);
457 }
458 }
459 return Collections.unmodifiableList(doiListFiltered);
460 }
461
462 /**
463 * Will mint new DOI if specified DOI doesn't exist.
464 *
465 * This method will attempt to update URL if you specify existing DOI.
466 * Standard domains and quota restrictions check will be performed. A
467 * Datacentre's doiQuotaUsed will be increased by 1. A new record in
468 * Datasets will be created when 201 Created status is returned.
469 *
470 * The DOI prefix may replace according to the {@link ClientMDS#context}.
471 *
472 * @param form A form with the following attributes doi and url
473 * @return short explanation of status code e.g. CREATED,
474 * HANDLE_ALREADY_EXISTS etc
475 * @throws ClientMdsException - if an error happens <ul>
476 * <li>400 Bad Request - request body must be exactly two lines: DOI and
477 * URL; wrong domain, wrong prefix</li>
478 * <li>401 Unauthorized - no login</li>
479 * <li>403 Forbidden - login problem, quota exceeded</li>
480 * <li>412 Precondition failed - metadata must be uploaded first</li>
481 * <li>500 Internal Server Error - server internal error, try later and if
482 * problem persists please contact us</li>
483 * </ul>
484 * @see "https://mds.datacite.org/static/apidoc#tocAnchor-15"
485 */
486 public String createDoi(final Form form) throws ClientMdsException {
487 try {
488 this.checkInputForm(form);
489 final Reference url = createReference(DOI_RESOURCE+"/"+form.getFirstValue(POST_DOI));
490 this.getLog().debug("PUT {0}", url.toString());
491 final Representation rep = createRequest(url, form);
492 return getText(rep);
493 } catch (ResourceException ex) {
494 throw new ClientMdsException(ex.getStatus(), ex.getMessage(), this.getClient().
495 getResponseEntity(), ex);
496 } finally {
497 this.getClient().release();
498 }
499 }
500
501 /**
502 * Creates the request and requests the DOI creation
503 *
504 * @param url url
505 * @param form form
506 * @return representation of the response
507 */
508 private Representation createRequest(final Reference url, final Form form) {
509 this.getClient().setReference(url);
510 String requestBody = POST_DOI + "=" + form.getFirstValue(POST_DOI) + "\n"
511 + POST_URL + "=" + form.getFirstValue(POST_URL);
512 requestBody = new String(requestBody.getBytes(
513 StandardCharsets.UTF_8),
514 StandardCharsets.UTF_8
515 );
516 final Map<String, Object> requestAttributes = this.getClient().getRequestAttributes();
517 requestAttributes.put("charset", StandardCharsets.UTF_8);
518 requestAttributes.put("Content-Type", "text/plain");
519 this.getLog().info("PUT {} with parameters {}", url, requestBody);
520 return this.getClient().put(requestBody, MediaType.TEXT_PLAIN);
521 }
522
523 /**
524 * Parses the XML representation that implements the DATACITE schema.
525 *
526 * @param rep XML representation
527 * @return the Resource object
528 * @throws ClientMdsException Will throw when a problem happens during the
529 * parsing
530 */
531 private synchronized Resource parseDataciteResource(final Representation rep) throws
532 ClientMdsException {
533 final JaxbRepresentation<Resource> resource = new JaxbRepresentation<>(rep, Resource.class);
534 try {
535 return resource.getObject();
536 } catch (IOException ex) {
537 throw new ClientMdsException(Status.SERVER_ERROR_INTERNAL, ex);
538 }
539 }
540
541 /**
542 * This request returns the most recent version of metadata associated with
543 * a given DOI. A status of 200 is an operation successful. The DOI prefix
544 * may replace according to the {@link ClientMDS#context}.
545 *
546 * @param doiName DOI name
547 * @return XML representing a dataset
548 * @throws ClientMdsException - if an error happens <ul>
549 * <li>401 Unauthorized - no login</li>
550 * <li>403 Forbidden - login problem or dataset belongs to another
551 * party</li>
552 * <li>404 Not Found - DOI does not exist in our database</li>
553 * <li>410 Gone - the requested dataset was marked inactive (using DELETE
554 * method)</li>
555 * <li>500 Internal Server Error - server internal error, try later and if
556 * problem persists please contact us</li>
557 * </ul>
558 */
559 public Resource getMetadataAsObject(final String doiName) throws ClientMdsException {
560 final Representation rep = getMetadata(doiName);
561 return parseDataciteResource(rep);
562 }
563
564 /**
565 * Returns the metadata based on its DOI name. A status of 200 is an
566 * operation successful. The DOI prefix may replace according to the
567 * {@link ClientMDS#context}.
568 *
569 * @param doiName DOI name
570 * @return the metadata as XML
571 * @throws ClientMdsException - if an error happens <ul>
572 * <li>200 OK - operation successful</li>
573 * <li>401 Unauthorized - no login</li>
574 * <li>403 Forbidden - login problem or dataset belongs to another
575 * party</li>
576 * <li>404 Not Found - DOI does not exist in our database</li>
577 * <li>410 Gone - the requested dataset was marked inactive (using DELETE
578 * method)</li>
579 * <li>500 Internal Server Error - server internal error, try later and if
580 * problem persists please contact us</li>
581 * </ul>
582 * @see "https://mds.datacite.org/static/apidoc#tocAnchor-15"
583 */
584 public Representation getMetadata(final String doiName) throws ClientMdsException {
585 final Reference url = createReferenceWithDOI(METADATA_RESOURCE, doiName);
586 this.getLog().debug("GET {}", url.toString());
587 this.getClient().setReference(url);
588 this.getLog().info("GET {}", url);
589 try {
590 return this.getClient().get(MediaType.APPLICATION_XML);
591 } catch (ResourceException ex) {
592 this.getClient().release();
593 throw new ClientMdsException(ex.getStatus(), ex.getMessage(), this.getClient().
594 getResponseEntity(), ex);
595 }
596 }
597
598 /**
599 * This request stores new version of metadata. Creates metadata with 201
600 * status when operation successful. The DOI prefix may replace according to
601 * the {@link ClientMDS#context}.
602 *
603 * @param entity A valid XML
604 * @return short explanation of status code e.g.
605 * CREATED,HANDLE_ALREADY_EXISTS etc
606 * @throws ClientMdsException - if an error happens <ul>
607 * <li>400 Bad Request - invalid XML, wrong prefix</li>
608 * <li>401 Unauthorized - no login</li>
609 * <li>403 Forbidden - login problem, quota exceeded</li>
610 * <li>500 Internal Server Error - server internal error, try later and if
611 * problem persists please contact us</li>
612 * </ul>
613 * @see "https://mds.datacite.org/static/apidoc#tocAnchor-18"
614 */
615 public String createMetadata(final Representation entity) throws ClientMdsException {
616 final Resource resource = parseDataciteResource(entity);
617 return this.createMetadata(resource);
618 }
619
620 /**
621 * Creates metadata with 201 status when operation successful. The DOI
622 * prefix may replace according to the {@link ClientMDS#context}.
623 *
624 * The method is synchronized because marshall method is not thread-safe.
625 *
626 * @param entity Metadata
627 * @return short explanation of status code e.g. CREATED,
628 * HANDLE_ALREADY_EXISTS etc
629 * @throws ClientMdsException - if an error happens <ul>
630 * <li>400 Bad Request - invalid XML, wrong prefix</li>
631 * <li>401 Unauthorized - no login</li>
632 * <li>403 Forbidden - login problem, quota exceeded</li>
633 * <li>500 Internal Server Error - server internal error, try later and if
634 * problem persists please contact us</li>
635 * </ul>
636 * @see "https://mds.datacite.org/static/apidoc#tocAnchor-18"
637 */
638 public String createMetadata(final Resource entity) throws ClientMdsException {
639 try {
640 final Identifier identifier = entity.getIdentifier();
641 identifier.setValue(getDoiAccorgindToContext(identifier.getValue()));
642 final Reference url = createReference(METADATA_RESOURCE+"/"+identifier.getValue());
643 this.getLog().debug("PUT {}", url.toString());
644 final JaxbRepresentation<Resource> result = new JaxbRepresentation<>(entity);
645 result.setCharacterSet(CharacterSet.UTF_8);
646 result.setMediaType(MediaType.APPLICATION_XML);
647 this.getClient().setReference(url);
648 this.getClient().getRequestAttributes().put("Content-Type", "application/xml");
649 this.getClient().getRequestAttributes().put("charset", "UTF-8");
650 this.getClient().setMethod(null);
651 final Representation response = this.getClient().put(result);
652 return getText(response);
653 } catch (ResourceException ex) {
654 throw new ClientMdsException(ex.getStatus(), ex.getMessage(), this.getClient().
655 getResponseEntity(), ex);
656 } finally {
657 this.getClient().release();
658 }
659 }
660
661 /**
662 * Parses the metadata and returns the Resource object from DataCite.
663 *
664 * The method is synchronized because unmarshall method is not thread-safe.
665 *
666 * @param entity metadata
667 * @return the Resource object from DataCite
668 * @throws ValidationException When validation failed
669 */
670 public synchronized Resource parseMetadata(final Representation entity) throws
671 ValidationException {
672
673 try {
674 final JaxbRepresentation<Resource> resourceEntity = new JaxbRepresentation<>(entity,
675 Resource.class);
676 final MyValidationEventHandler validationHandler = new MyValidationEventHandler(this.
677 getClient().getLogger());
678 resourceEntity.setValidationEventHandler(validationHandler);
679 final Resource resource = resourceEntity.getObject();
680 if (validationHandler.isValid()) {
681 return resource;
682 } else {
683 throw new ValidationException(validationHandler.getErrorMsg());
684 }
685 } catch (IOException | JAXBException ex) {
686 throw new ValidationException("Cannot read the metadata", ex);
687 }
688 }
689
690 /**
691 * This request marks a dataset as 'inactive'.
692 *
693 * To activate it again, POST new metadata or set the isActive-flag in the
694 * user interface. A status of 200 is an operation successful. The DOI
695 * prefix may replace according to the {@link ClientMDS#context}.
696 *
697 * @param doiName DOI name
698 * @return XML representing a dataset
699 * @throws ClientMdsException - if an error happens <ul>
700 * <li>401 Unauthorized - no login</li>
701 * <li>403 Forbidden - login problem or dataset belongs to another
702 * party</li>
703 * <li>404 Not Found - DOI does not exist in our database</li>
704 * <li>500 Internal Server Error - server internal error, try later and if
705 * problem persists please contact us</li>
706 * </ul>
707 * @see "https://mds.datacite.org/static/apidoc#tocAnchor-19"
708 */
709 public Resource deleteMetadataDoiAsObject(final String doiName) throws ClientMdsException {
710 final Representation rep = this.deleteMetadata(doiName);
711 return parseDataciteResource(rep);
712 }
713
714 /**
715 * This request marks a dataset as 'inactive'.
716 *
717 * To activate it again, POST new metadata or set the isActive-flag in the
718 * user interface. A status of 200 is an operation successful The DOI prefix
719 * may replace according to the {@link ClientMDS#context}.
720 *
721 * @param doiName DOI name
722 * @return the deleted metadata
723 * @throws ClientMdsException - if an error happens <ul>
724 * <li>401 Unauthorized - no login</li>
725 * <li>403 Forbidden - login problem or dataset belongs to another
726 * party</li>
727 * <li>404 Not Found - DOI does not exist in our database</li>
728 * <li>500 Internal Server Error - server internal error, try later and if
729 * problem persists please contact us</li>
730 * </ul>
731 * @see "https://mds.datacite.org/static/apidoc#tocAnchor-19"
732 */
733 public Representation deleteMetadata(final String doiName) throws ClientMdsException {
734 final Reference url = createReferenceWithDOI(METADATA_RESOURCE, doiName);
735 this.getLog().debug("DELETE {}", url.toString());
736 this.getClient().setReference(url);
737 try {
738 return this.getClient().delete();
739 } catch (ResourceException ex) {
740 this.getClient().release();
741 throw new ClientMdsException(ex.getStatus(), ex.getMessage(), this.getClient().
742 getResponseEntity(), ex);
743 }
744 }
745
746 /**
747 * This request returns list of pairs of media type and URLs associated with
748 * a given DOI. A status of 200 is an operation successful. The DOI prefix
749 * may replace according to the {@link ClientMDS#context}.
750 *
751 * @param doiName DOI name
752 * @return list of pairs of media type and URLs
753 * @throws ClientMdsException - if an error happens <ul>
754 * <li>401 Unauthorized - no login</li>
755 * <li>403 login problem or dataset belongs to another party</li>
756 * <li>404 Not Found - No media attached to the DOI or DOI does not exist in
757 * our database</li>
758 * <li>500 Internal Server Error - server internal error, try later and if
759 * problem persists please contact us</li>
760 * </ul>
761 * @see "https://mds.datacite.org/static/apidoc#tocAnchor-21"
762 */
763 public String getMedia(final String doiName) throws ClientMdsException {
764 final String result;
765 final Reference url = createReferenceWithDOI(MEDIA_RESOURCE, doiName);
766 this.getLog().debug("GET {}", url.toString());
767 this.getClient().setReference(url);
768 try {
769 final Representation response = this.getClient().get();
770 result = this.getText(response);
771 return result;
772 } catch (ResourceException ex) {
773 throw new ClientMdsException(ex.getStatus(), ex.getMessage(), this.getClient().
774 getResponseEntity(), ex);
775 } finally {
776 this.getClient().release();
777 }
778 }
779
780 /**
781 * Will add/update media type/urls pairs to a DOI. A status of 200 is an
782 * operation successful.Standard domain restrictions check will be
783 * performed. The DOI prefix may replace according to the
784 * {@link ClientMDS#context}.
785 *
786 * @param doiName DOI identifier
787 * @param form Multiple lines in the following format{mime-type}={url} where
788 * {mime-type} and {url} have to be replaced by your mime type and URL,
789 * UFT-8 encoded.
790 * @return short explanation of status code
791 * @throws ClientMdsException - if an error happens <ul>
792 * <li>400 Bad Request - one or more of the specified mime-types or urls are
793 * invalid (e.g. non supported mime-type, not allowed url domain, etc.)</li>
794 * <li>401 Unauthorized - no login</li>
795 * <li>403 Forbidden - login problem</li>
796 * <li>500 Internal Server Error - server internal error, try later and if
797 * problem persists please contact us</li>
798 * </ul>
799 * @see "https://mds.datacite.org/static/apidoc#tocAnchor-22"
800 */
801 public String createMedia(final String doiName,
802 final Form form) throws ClientMdsException {
803 final String result;
804 final Reference url = createReferenceWithDOI(MEDIA_RESOURCE, doiName);
805 this.getLog().debug("POST {}", url.toString());
806 this.getClient().setReference(url);
807 final Representation entity = createEntity(form);
808 try {
809 final Representation response = this.getClient().post(entity, MediaType.TEXT_PLAIN);
810 result = this.getText(response);
811 return result;
812 } catch (ResourceException ex) {
813 throw new ClientMdsException(ex.getStatus(), ex.getMessage(), this.getClient().
814 getResponseEntity(), ex);
815 } finally {
816 this.getClient().release();
817 }
818 }
819
820 /**
821 * Creates an entity based on the form. The form contains a set of
822 * mime-type/url
823 *
824 * @param mediaForm form
825 * @return Text entity
826 */
827 private Representation createEntity(final Form mediaForm) {
828 final Iterator<Parameter> iter = mediaForm.iterator();
829 StringBuilder entity = new StringBuilder();
830 while (iter.hasNext()) {
831 final Parameter param = iter.next();
832 final String mimeType = param.getName();
833 final String url = param.getValue();
834 entity = entity.append(mimeType).append("=").append(url).append("\n");
835 }
836 return new StringRepresentation(
837 entity.toString(),
838 MediaType.TEXT_PLAIN,
839 Language.ENGLISH,
840 CharacterSet.UTF_8
841 );
842 }
843
844 /**
845 * Metadata Validation.
846 */
847 @Requirement(reqId = Requirement.DOI_ARCHI_020, reqName = Requirement.DOI_ARCHI_020_NAME)
848 private static class MyValidationEventHandler implements ValidationEventHandler {
849
850 /**
851 * Logger.
852 */
853 private final java.util.logging.Logger logger;
854
855 /**
856 * Indicates if an error was happening.
857 */
858 private boolean hasError = false;
859
860 /**
861 * Error message.
862 */
863 private String errorMsg = null;
864
865 /**
866 * Validation handler
867 *
868 * @param logger logger
869 */
870 MyValidationEventHandler(final java.util.logging.Logger logger) {
871 this.logger = logger;
872 }
873
874 /**
875 * Handles event
876 *
877 * @param event event
878 * @return True
879 */
880 @Override
881 public boolean handleEvent(final ValidationEvent event) {
882 final StringBuilder stringBuilder = new StringBuilder("\nEVENT");
883 stringBuilder.append("SEVERITY: ").append(event.getSeverity()).append("\n");
884 stringBuilder.append("MESSAGE: ").append(event.getMessage()).append("\n");
885 stringBuilder.append("LINKED EXCEPTION: ").append(event.getLinkedException()).append(
886 "\n");
887 stringBuilder.append("LOCATOR\n");
888 stringBuilder.append(" LINE NUMBER: ").append(event.getLocator().getLineNumber()).
889 append("\n");
890 stringBuilder.append(" COLUMN NUMBER: ").
891 append(event.getLocator().getColumnNumber()).append("\n");
892 stringBuilder.append(" OFFSET: ").append(event.getLocator().getOffset()).
893 append("\n");
894 stringBuilder.append(" OBJECT: ").append(event.getLocator().getObject()).
895 append("\n");
896 stringBuilder.append(" NODE: ").append(event.getLocator().getNode()).append("\n");
897 stringBuilder.append(" URL ").append(event.getLocator().getURL()).append("\n");
898 this.errorMsg = stringBuilder.toString();
899 this.logger.info(this.errorMsg);
900 this.hasError = true;
901 return true;
902 }
903
904 /**
905 * Returns true when metadata is valid against the schema otherwise
906 * false.
907 *
908 * @return true when metadata is valid against the schema otherwise
909 * false
910 */
911 public boolean isValid() {
912 return !this.isNotValid();
913
914 }
915
916 /**
917 * Returns true when metadata is not valid against the schema otherwise
918 * false.
919 *
920 * @return true when metadata is not valid against the schema otherwise
921 * false
922 */
923 public boolean isNotValid() {
924 return this.hasError;
925 }
926
927 /**
928 * Returns the errorMsg or null when no error message.
929 *
930 * @return the errorMsg or null when no error message
931 */
932 public String getErrorMsg() {
933 return this.errorMsg;
934 }
935 }
936
937 /**
938 * Datacite API.
939 */
940 public enum DATACITE_API_RESPONSE {
941 /**
942 * Get/Delete successfully a DOI or a media. SUCCESS_OK is used as
943 * status
944 */
945 SUCCESS(Status.SUCCESS_OK, "Operation successful"),
946 /**
947 * Create successfully a DOI. SUCCESS_CREATED is as used as status.
948 */
949 SUCESS_CREATED(Status.SUCCESS_CREATED, "Operation successful"),
950 /**
951 * Get a DOI without metadata. SUCCESS_NO_CONTENT is used as status
952 */
953 SUCCESS_NO_CONTENT(Status.SUCCESS_NO_CONTENT,
954 " the DOI is known to DataCite Metadata Store (MDS), but no metadata have been registered"),
955 /**
956 * Fail to create a media or the metadata. CLIENT_ERROR_BAD_REQUEST is
957 * used as status
958 */
959 BAD_REQUEST(Status.CLIENT_ERROR_BAD_REQUEST,
960 "invalid XML, wrong prefix or request body must be exactly two lines: DOI and URL; wrong domain, wrong prefix"),
961 /**
962 * Fail to authorize the user to create/delete a DOI.
963 * CLIENT_ERROR_UNAUTHORIZED is used as status
964 */
965 UNAUTHORIZED(Status.CLIENT_ERROR_UNAUTHORIZED, "no login"),
966 /**
967 * Fail to create/delete media/metadata/Landing page.
968 * CLIENT_ERROR_FORBIDDEN is used as status
969 */
970 FORBIDDEN(Status.CLIENT_ERROR_FORBIDDEN,
971 "login problem, wrong prefix, permission problem or dataset belongs to another party"),
972 /**
973 * Fail to get the DOI. CLIENT_ERROR_NOT_FOUND is used as status
974 */
975 DOI_NOT_FOUND(Status.CLIENT_ERROR_NOT_FOUND, "DOI does not exist in our database"),
976 /**
977 * Get an inactive DOI. CLIENT_ERROR_GONE is used as status
978 */
979 DOI_INACTIVE(Status.CLIENT_ERROR_GONE,
980 "the requested dataset was marked inactive (using DELETE method)"),
981 /**
982 * Fail to create a DOI because metadata must be uploaded first.
983 * CLIENT_ERROR_PRECONDITION_FAILED is used as status
984 */
985 PROCESS_ERROR(Status.CLIENT_ERROR_PRECONDITION_FAILED, "metadata must be uploaded first"),
986 /**
987 * Internal server Error. INTERNAL_SERVER_ERROR is used as status.
988 */
989 INTERNAL_SERVER_ERROR(Status.SERVER_ERROR_INTERNAL, "Internal server error");
990
991 /**
992 * HTTP status.
993 */
994 private final Status status;
995
996 /**
997 * message.
998 */
999 private final String message;
1000
1001 /**
1002 * Creates enumeration
1003 *
1004 * @param status status
1005 * @param message message
1006 */
1007 DATACITE_API_RESPONSE(final Status status, final String message) {
1008 this.status = status;
1009 this.message = message;
1010 }
1011
1012 /**
1013 * Returns the status
1014 *
1015 * @return the status
1016 */
1017 public Status getStatus() {
1018 return this.status;
1019 }
1020
1021 /**
1022 * Returns the short message
1023 *
1024 * @return the short message
1025 */
1026 public String getShortMessage() {
1027 return this.message;
1028 }
1029
1030 /**
1031 * Returns the message for a specific Status.
1032 *
1033 * @param statusToFind status to search
1034 * @return the message or empty string
1035 */
1036 public static String getMessageFromStatus(final Status statusToFind) {
1037 String result = "";
1038 final int codeToFind = statusToFind.getCode();
1039 final DATACITE_API_RESPONSE[] values = DATACITE_API_RESPONSE.values();
1040 for (int i = 0; i <= values.length; i++) {
1041 final DATACITE_API_RESPONSE value = values[i];
1042 final int codeValue = value.getStatus().getCode();
1043 if (codeValue == codeToFind) {
1044 result = value.message;
1045 break;
1046 }
1047 }
1048 return result;
1049 }
1050 }
1051
1052 /**
1053 * Options for each context
1054 */
1055 public enum Context {
1056
1057 /**
1058 * Development context.
1059 */
1060 DEV(false, true, DATA_CITE_MOCK_URL, Level.ALL),
1061 /**
1062 * Post development context.
1063 */
1064 POST_DEV(false, true, DATA_CITE_TEST_URL, Level.ALL),
1065 /**
1066 * Pre production context.
1067 */
1068 PRE_PROD(false, true, DATA_CITE_URL, Level.FINE),
1069 /**
1070 * Production context.
1071 */
1072 PROD(false, false, DATA_CITE_URL, Level.INFO);
1073
1074 /**
1075 * Each API call can have optional query parametertestMode. If set to
1076 * "true" or "1" the request will not change the database nor will the
1077 * DOI handle will be registered or updated, e.g. POST
1078 * /doi?testMode=true and the testing prefix will be used instead of the
1079 * provided prefix
1080 */
1081 private final boolean isTestMode;
1082
1083 /**
1084 * There is special test prefix 10.5072 available to all datacentres.
1085 * Please use it for all your testing DOIs. Your real prefix should not
1086 * be used for test DOIs. Note that DOIs with test prefix will behave
1087 * like any other DOI, e.g. they can be normally resolved. They will not
1088 * be exposed by upcoming services like search and OAI, though.
1089 * Periodically we purge all 10.5072 datasets from the system.
1090 */
1091 private final boolean isDoiPrefix;
1092
1093 /**
1094 * Level log.
1095 */
1096 private Level levelLog;
1097
1098 /**
1099 * DataCite URL.
1100 */
1101 private String dataCiteUrl;
1102
1103 Context(final boolean isTestMode,
1104 final boolean isDoiPrefix,
1105 final String dataciteUrl,
1106 final Level levelLog) {
1107 this.isTestMode = isTestMode;
1108 this.isDoiPrefix = isDoiPrefix;
1109 this.dataCiteUrl = dataciteUrl;
1110 this.levelLog = levelLog;
1111 }
1112
1113 /**
1114 * Returns true when the context has a DOI dev.
1115 *
1116 * @return True when the context has a DOI dev
1117 */
1118 public boolean hasDoiTestPrefix() {
1119 return this.isDoiPrefix;
1120 }
1121
1122 /**
1123 * Returns true when the context must not register data in DataCite
1124 *
1125 * @return true when the context must not register data in DataCite
1126 */
1127 public boolean hasTestMode() {
1128 return this.isTestMode;
1129 }
1130
1131 /**
1132 * Returns the log level.
1133 *
1134 * @return the log level
1135 */
1136 public Level getLevelLog() {
1137 return this.levelLog;
1138 }
1139
1140 /**
1141 * Returns the service end point.
1142 *
1143 * @return the service end point
1144 */
1145 public String getDataCiteUrl() {
1146 return this.dataCiteUrl;
1147 }
1148
1149 /**
1150 * Sets the DataCite URL for the context
1151 *
1152 * @param dataCiteUrl DataCite URL
1153 */
1154 private void setDataCiteURl(final String dataCiteUrl) {
1155 this.dataCiteUrl = dataCiteUrl;
1156 }
1157
1158 /**
1159 * Sets the level log for the context
1160 *
1161 * @param levelLog level log
1162 */
1163 private void setLevelLog(final Level levelLog) {
1164 this.levelLog = levelLog;
1165 }
1166
1167 /**
1168 * Sets the level log for a given context
1169 *
1170 * @param context the context
1171 * @param levelLog the level log
1172 */
1173 public static void setLevelLog(final Context context,
1174 final Level levelLog) {
1175 context.setLevelLog(levelLog);
1176 }
1177
1178 /**
1179 * Sets the DataCite URL for a given context
1180 *
1181 * @param context the context
1182 * @param dataCiteUrl the DataCite URL
1183 */
1184 public static void setDataCiteUrl(final Context context,
1185 final String dataCiteUrl) {
1186 context.setDataCiteURl(dataCiteUrl);
1187 }
1188
1189 }
1190
1191 }