View Javadoc

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 }