001/*
002 * Licensed under the Apache License, Version 2.0 (the "License");
003 * you may not use this file except in compliance with the License.
004 * You may obtain a copy of the License at
005 *
006 *     http://www.apache.org/licenses/LICENSE-2.0
007 *
008 * Unless required by applicable law or agreed to in writing, software
009 * distributed under the License is distributed on an "AS IS" BASIS,
010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011 * See the License for the specific language governing permissions and
012 * limitations under the License.
013 */
014package org.gbif.api.util;
015
016import org.gbif.api.model.common.DOI;
017import org.gbif.api.model.registry.CitationContact;
018import org.gbif.api.model.registry.Contact;
019import org.gbif.api.model.registry.Dataset;
020import org.gbif.api.model.registry.Endpoint;
021import org.gbif.api.model.registry.Organization;
022import org.gbif.api.vocabulary.ContactType;
023import org.gbif.api.vocabulary.DatasetType;
024
025import java.time.LocalDate;
026import java.time.ZoneId;
027import java.util.Arrays;
028import java.util.Collections;
029import java.util.Date;
030import java.util.EnumSet;
031import java.util.List;
032import java.util.Set;
033import java.util.UUID;
034
035import org.gbif.api.vocabulary.EndpointType;
036import org.junit.jupiter.api.Test;
037
038import static org.gbif.api.model.common.DOI.TEST_PREFIX;
039import static org.gbif.api.util.CitationGenerator.getAuthors;
040import static org.junit.jupiter.api.Assertions.assertEquals;
041import static org.junit.jupiter.api.Assertions.assertNotNull;
042import static org.junit.jupiter.api.Assertions.assertTrue;
043
044/** Unit tests related to {@link CitationGenerator}. */
045public class CitationGeneratorTest {
046
047  @Test
048  public void testCamtrapCitation() {
049    Organization org = new Organization();
050    org.setTitle("Research Institute for Nature and Forest (INBO)");
051
052    Dataset dataset = getCamtrapDataset();
053    dataset.getContacts().add(createContact("Jim", "Casaer", ContactType.ORIGINATOR));
054    dataset.getContacts().add(createContact("Niko", "Boone", ContactType.ORIGINATOR));
055    dataset.getContacts().add(createContact("Jan", "Vercammen", ContactType.ORIGINATOR));
056    dataset.getContacts().add(createContact("Sander", "Devisscher", ContactType.ORIGINATOR));
057    dataset.getContacts().add(createContact("Lynn", "Pallemaerts", ContactType.ORIGINATOR));
058    dataset.getContacts().add(createContact("Anneleen", "Rutten", ContactType.ORIGINATOR));
059    dataset.getContacts().add(createContact("Martijn", "Bollen", ContactType.ORIGINATOR));
060    dataset.getContacts().add(createContact("Peter", "Desmet", ContactType.ORIGINATOR));
061    dataset.getContacts().add(createContact("Sanne", "Govaert", ContactType.ORIGINATOR));
062    dataset.getContacts().add(createContact("Jim", "Casaer", ContactType.METADATA_AUTHOR));
063    dataset.getContacts().add(createContact("Jim", "Casaer", ContactType.ADMINISTRATIVE_POINT_OF_CONTACT));
064
065    CitationGenerator.CitationData citation = CitationGenerator.generateCitation(dataset, org);
066
067    String expectedCitation = "Casaer J, Boone N, Vercammen J, Devisscher S, Pallemaerts L, Rutten A, Bollen M, "
068        + "Desmet P, Govaert S (2025). GMU8_LEUVEN - Camera trap observations in natural habitats south of Leuven "
069        + "(Belgium). Research Institute for Nature and Forest (INBO). "
070        + "Occurrence dataset https://doi.org/10.15468/4u3wm4 accessed via GBIF.org on "
071        + LocalDate.now(ZoneId.of("UTC"))
072        + ".";
073
074    assertEquals(expectedCitation, citation.getCitation().getText());
075  }
076
077  @Test
078  public void testAuthorNames() {
079    Contact c = new Contact();
080    c.setLastName("Doe");
081    c.setFirstName("John D.");
082    assertEquals("Doe J D", CitationGenerator.getAuthorName(c));
083    assertEquals(0, getAuthors(Collections.singletonList(c)).size());
084
085    // test with missing first name
086    c = new Contact();
087    c.setLastName("Doe");
088    c.setOrganization("Awesome Organization");
089    assertEquals("Doe", CitationGenerator.getAuthorName(c));
090    assertEquals(0, getAuthors(Collections.singletonList(c)).size());
091
092    // test with missing parts
093    c = new Contact();
094    c.setFirstName("John");
095    c.setOrganization("Awesome Organization");
096    assertEquals("Awesome Organization", CitationGenerator.getAuthorName(c));
097    assertEquals(0, getAuthors(Collections.singletonList(c)).size());
098  }
099
100  @Test
101  public void testCompleteCitation() {
102    Organization org = new Organization();
103    org.setTitle("Cited Organization");
104
105    Dataset dataset = getTestDatasetObject();
106    dataset.getContacts().add(createContact("John D.", "Doe", ContactType.ORIGINATOR));
107
108    CitationGenerator.CitationData citation = CitationGenerator.generateCitation(dataset, org);
109
110    assertEquals(
111        "Doe J D (2009). Dataset to be cited. Version 2.1. Cited Organization. "
112            + "Sampling event dataset https://doi.org/10.21373/abcd accessed via GBIF.org on "
113            + LocalDate.now(ZoneId.of("UTC"))
114            + ".",
115        citation.getCitation().getText());
116    assertEquals(1, citation.getContacts().size());
117  }
118
119  @Test
120  public void testCompleteCitationUserWithoutName() {
121    Organization org = new Organization();
122    org.setTitle("Cited Organization");
123
124    Dataset dataset = getTestDatasetObject();
125    dataset.getContacts().add(createContact("John D.", "Doe", ContactType.ORIGINATOR));
126    dataset.getContacts().add(createContact("  ", "Smith", ContactType.ORIGINATOR));
127    dataset.getContacts().add(createContact("John", null, ContactType.ORIGINATOR));
128    dataset.getContacts().add(createContact(null, "Mendez", ContactType.ORIGINATOR));
129
130    CitationGenerator.CitationData citation = CitationGenerator.generateCitation(dataset, org);
131
132    assertEquals(
133        "Doe J D, Smith, Mendez (2009). Dataset to be cited. Version 2.1. Cited Organization. "
134            + "Sampling event dataset https://doi.org/10.21373/abcd accessed via GBIF.org on "
135            + LocalDate.now(ZoneId.of("UTC"))
136            + ".",
137        citation.getCitation().getText());
138
139    assertEquals(3, citation.getContacts().size());
140  }
141
142  @Test
143  public void testCompleteCitationNoAuthors() {
144    Organization org = new Organization();
145    org.setTitle("Cited Organization");
146
147    Dataset dataset = getTestDatasetObject();
148    dataset
149        .getContacts()
150        .add(
151            createContact(
152                null,
153                null,
154                "We are not using this field int the citation",
155                ContactType.ORIGINATOR));
156
157    CitationGenerator.CitationData citation = CitationGenerator.generateCitation(dataset, org);
158
159    assertEquals(
160        "Cited Organization (2009). Dataset to be cited. Version 2.1. "
161            + "Sampling event dataset https://doi.org/10.21373/abcd accessed via GBIF.org on "
162            + LocalDate.now(ZoneId.of("UTC"))
163            + ".",
164        citation.getCitation().getText());
165
166    assertEquals(0, citation.getContacts().size());
167  }
168
169  @Test
170  public void testCompleteCitationNoYear() {
171    Organization org = new Organization();
172    org.setTitle("Cited Organization");
173
174    Dataset dataset = getTestDatasetObject();
175    dataset.setPubDate(null);
176    dataset.getContacts().add(createContact("John", "Doe", ContactType.ORIGINATOR));
177
178    CitationGenerator.CitationData citation = CitationGenerator.generateCitation(dataset, org);
179
180    assertEquals(
181        "Doe J. Dataset to be cited. Version 2.1. Cited Organization. "
182            + "Sampling event dataset https://doi.org/10.21373/abcd accessed via GBIF.org on "
183            + LocalDate.now(ZoneId.of("UTC"))
184            + ".",
185        citation.getCitation().getText());
186
187    assertEquals(1, citation.getContacts().size());
188  }
189
190  @Test
191  public void testCompleteCitationAuthorMultipleRoles() {
192    Organization org = new Organization();
193    org.setTitle("Cited Organization");
194
195    Dataset dataset = getTestDatasetObject();
196
197    dataset.getContacts().add(createContact("John D.", "Doe", ContactType.ORIGINATOR));
198    dataset.getContacts().add(createContact("Jim", "Carey", ContactType.PROGRAMMER));
199    dataset.getContacts().add(createContact("John D.", "Doe", ContactType.METADATA_AUTHOR));
200
201    CitationGenerator.CitationData citation = CitationGenerator.generateCitation(dataset, org);
202
203    assertEquals(
204        "Doe J D (2009). Dataset to be cited. Version 2.1. Cited Organization. "
205            + "Sampling event dataset https://doi.org/10.21373/abcd accessed via GBIF.org on "
206            + LocalDate.now(ZoneId.of("UTC"))
207            + ".",
208        citation.getCitation().getText());
209
210    assertEquals(1, citation.getContacts().size());
211  }
212
213  @Test
214  public void testCompleteCitationCamtrapAuthorMultipleRoles() {
215    Organization org = new Organization();
216    org.setTitle("Cited Organization");
217
218    Dataset dataset = getTestCamtrapOccurrenceDatasetObject();
219
220    dataset.getContacts().add(createContact("Tim", "Robertson", ContactType.ORIGINATOR));
221    dataset.getContacts().add(createContact("John D.", "Doe", ContactType.POINT_OF_CONTACT));
222    dataset.getContacts().add(createContact("Jim", "Carey", ContactType.PRINCIPAL_INVESTIGATOR));
223    dataset.getContacts().add(createContact("Jack", "White", ContactType.CONTENT_PROVIDER));
224    dataset.getContacts().add(createContact("", "Rights Holder", ContactType.OWNER));
225    dataset.getContacts().add(createContact("", "Publisher", ContactType.DISTRIBUTOR));
226
227    CitationGenerator.CitationData citation = CitationGenerator.generateCitation(dataset, org);
228
229    assertEquals(
230        "Robertson T (2009). Dataset to be cited. Version 2.1. Cited Organization. "
231            + "Occurrence dataset https://doi.org/10.21373/abcd accessed via GBIF.org on "
232            + LocalDate.now(ZoneId.of("UTC"))
233            + ".",
234        citation.getCitation().getText());
235
236    assertEquals(1, citation.getContacts().size());
237  }
238
239  @Test
240  public void testCompleteCitationNoOriginator() {
241    Organization org = new Organization();
242    org.setTitle("Cited Organization");
243    Dataset dataset = getTestDatasetObject();
244    dataset.getContacts().add(createContact("John D.", "Doe", ContactType.METADATA_AUTHOR));
245
246    CitationGenerator.CitationData citation = CitationGenerator.generateCitation(dataset, org);
247
248    assertEquals(
249        "Cited Organization (2009). Dataset to be cited. Version 2.1. "
250            + "Sampling event dataset https://doi.org/10.21373/abcd accessed via GBIF.org on "
251            + LocalDate.now(ZoneId.of("UTC"))
252            + ".",
253        citation.getCitation().getText());
254    assertEquals(0, citation.getContacts().size());
255  }
256
257  @Test
258  public void testCompleteCitationOriginatorNoName() {
259    Organization org = new Organization();
260    org.setTitle("Cited Organization");
261    Dataset dataset = getTestDatasetObject();
262
263    dataset.getContacts().add(createContact(null, null, "Test Org.", ContactType.ORIGINATOR));
264    dataset.getContacts().add(createContact("John D.", "Doe", ContactType.METADATA_AUTHOR));
265
266    CitationGenerator.CitationData citation = CitationGenerator.generateCitation(dataset, org);
267
268    assertEquals(
269        "Cited Organization (2009). Dataset to be cited. Version 2.1. "
270            + "Sampling event dataset https://doi.org/10.21373/abcd accessed via GBIF.org on "
271            + LocalDate.now(ZoneId.of("UTC"))
272            + ".",
273        citation.getCitation().getText());
274
275    assertEquals(0, citation.getContacts().size());
276  }
277
278  @Test
279  public void testAuthors() {
280    Organization org = new Organization();
281    org.setTitle("Cited Organization");
282
283    Dataset dataset = getTestDatasetObject();
284
285    dataset.getContacts().add(createContact("John D.", "Doe", ContactType.ORIGINATOR));
286    dataset
287        .getContacts()
288        .add(createContact("John D.", "Doe", "Awesome Organization", ContactType.ORIGINATOR));
289    // author with incomplete name
290    dataset.getContacts().add(createContact("Programmer", "Last", ContactType.PROGRAMMER));
291
292    // we expect 1 author since the names (first and last) are mandatory
293    assertEquals(1, getAuthors(dataset.getContacts()).size());
294
295    // but, we can only generate the name for one of them
296    assertEquals(
297        1, CitationGenerator.generateAuthorsName(getAuthors(dataset.getContacts())).size());
298  }
299
300  @Test
301  public void testRepeatedAuthor() {
302    Organization org = new Organization();
303    org.setTitle("Cited Organization");
304
305    Dataset dataset = getTestDatasetObject();
306    Contact contact1 = createContact("John D.", "Doe", ContactType.ORIGINATOR);
307    contact1.setUserId(Collections.singletonList("user1"));
308
309    Contact contact2 = createContact("John D.", "Doe", ContactType.METADATA_AUTHOR);
310    contact2.setUserId(Arrays.asList("user1", "user2"));
311
312    dataset.getContacts().add(contact1);
313    dataset.getContacts().add(contact2);
314
315    List<CitationContact> authors = getAuthors(dataset.getContacts());
316
317    // Only one author added
318    assertEquals(1, authors.size());
319
320    // The authors keep the 2 roles
321    assertTrue(
322        authors
323            .get(0)
324            .getRoles()
325            .containsAll(EnumSet.of(ContactType.ORIGINATOR, ContactType.METADATA_AUTHOR)));
326
327    Set<String> firstAuthorUserId = authors.get(0).getUserId();
328    assertNotNull(firstAuthorUserId);
329
330    // The author has 2 users
331    assertTrue(firstAuthorUserId.containsAll(Arrays.asList("user1", "user2")));
332
333    // Repeated user is not added twice
334    assertEquals(2, firstAuthorUserId.size());
335
336    // we can only generate the name for one of them
337    assertEquals(
338        1, CitationGenerator.generateAuthorsName(getAuthors(dataset.getContacts())).size());
339  }
340
341  private Dataset getCamtrapDataset() {
342    Dataset dataset = new Dataset();
343    dataset.setTitle("GMU8_LEUVEN - Camera trap observations in natural habitats south of Leuven (Belgium)");
344    dataset.setDoi(new DOI("10.15468/4u3wm4"));
345    dataset.setType(DatasetType.OCCURRENCE);
346    dataset.setPubDate(
347        new Date(
348            LocalDate.of(2025, 9, 25).atStartOfDay(ZoneId.of("UTC")).toInstant().toEpochMilli()));
349
350    dataset.setPublishingOrganizationKey(UUID.fromString("1cd669d0-80ea-11de-a9d0-f1765f95f18b"));
351
352    return dataset;
353  }
354
355  private Dataset getTestDatasetObject() {
356    Dataset dataset = new Dataset();
357    dataset.setTitle("Dataset to be cited");
358    dataset.setVersion("2.1");
359    dataset.setDoi(new DOI(TEST_PREFIX + "/abcd"));
360    dataset.setPubDate(
361        new Date(
362            LocalDate.of(2009, 2, 8).atStartOfDay(ZoneId.of("UTC")).toInstant().toEpochMilli()));
363
364    dataset.setType(DatasetType.SAMPLING_EVENT);
365
366    return dataset;
367  }
368
369  private Dataset getTestCamtrapOccurrenceDatasetObject() {
370    Dataset dataset = new Dataset();
371    dataset.setTitle("Dataset to be cited");
372    dataset.setVersion("2.1");
373    dataset.setDoi(new DOI(TEST_PREFIX + "/abcd"));
374    dataset.setPubDate(
375        new Date(
376            LocalDate.of(2009, 2, 8).atStartOfDay(ZoneId.of("UTC")).toInstant().toEpochMilli()));
377
378    dataset.setType(DatasetType.OCCURRENCE);
379
380    Endpoint camtrapEndpoint = new Endpoint();
381    camtrapEndpoint.setType(EndpointType.CAMTRAP_DP);
382    dataset.addEndpoint(camtrapEndpoint);
383
384    return dataset;
385  }
386
387  private Contact createContact(String firstName, String lastName, ContactType ct) {
388    return createContact(firstName, lastName, null, ct);
389  }
390
391  private Contact createContact(
392      String firstName, String lastName, String organization, ContactType ct) {
393    Contact c = new Contact();
394    c.setFirstName(firstName);
395    c.setLastName(lastName);
396    c.setOrganization(organization);
397    c.setType(ct);
398    return c;
399  }
400}