001/*
002 * Copyright 2021 Global Biodiversity Information Facility (GBIF)
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.gbif.dwc.terms;
017
018import com.google.common.collect.Sets;
019import com.google.common.reflect.ClassPath;
020import org.junit.jupiter.api.Test;
021
022import java.util.Arrays;
023import java.util.HashSet;
024import java.util.Set;
025import java.util.concurrent.ExecutorService;
026import java.util.concurrent.Executors;
027import java.util.concurrent.TimeUnit;
028import java.util.stream.Collectors;
029
030import static org.junit.jupiter.api.Assertions.assertEquals;
031import static org.junit.jupiter.api.Assertions.assertFalse;
032import static org.junit.jupiter.api.Assertions.assertNotEquals;
033import static org.junit.jupiter.api.Assertions.assertThrows;
034
035public class TermFactoryTest {
036
037  final TermFactory TF = TermFactory.instance();
038
039  /**
040   * GBIF code assumes a term coming from any of the Term enumerations mostly have unique simple names.
041   * This tests verifies that!
042   *
043   * AcefTerm is known to overlap, so its excluded, see skipSimple in AcefTermTest.
044   */
045  @Test
046  public void testKnownTermUniqueness() {
047    Set<String> names = new HashSet<>();
048
049    addTerms(names, DwcTerm.values());
050    addTerms(names, DcTerm.values());
051    addTerms(names, GbifTerm.values());
052    addTerms(names, GbifInternalTerm.values());
053    addTerms(names, IucnTerm.values());
054    //addTerms(names, DcElement.values());
055    //addTerms(names, AcefTerm.values());
056    //addTerms(names, PlaziTerm.values());
057    addTerms(names, GadmTerm.values());
058    //addTerms(names, DwcaTerm.values());
059
060    // Audubon Core
061    addTerms(names, termsBut(AcTerm.values(), AcTerm.Multimedia, AcTerm.relatedResourceID));
062    addTerms(names, ExifTerm.values());
063    addTerms(names, IptcTerm.values());
064    addTerms(names, PhotoshopTerm.values());
065    addTerms(names, XmpTerm.values());
066    addTerms(names, XmpRightsTerm.values());
067
068    // Terms for extensions supported in GBIF downloads.
069    addTerms(names, ChronoTerm.values());
070    addTerms(names, GbifDnaTerm.values());
071    addTerms(names, GbifMiqeTerm.values());
072    addTerms(names, GermplasmTerm.values());
073    addTerms(names, termsBut(GgbnTerm.values(), GgbnTerm.MaterialSample));
074    addTerms(names, MixsTerm.values());
075    addTerms(names, ObisTerm.values());
076    addTerms(names, Wgs84GeoPositioningTerm.values());
077  }
078
079  private Term[] termsBut(Term[] terms, Term... exclude) {
080    Set<Term> excl = Sets.newHashSet(exclude);
081    return Arrays.stream(terms)
082                 .filter(t -> !excl.contains(t))
083                 .collect(Collectors.toList()).toArray(new Term[]{});
084  }
085
086  private void addTerms(Set<String> names, Term[] terms) {
087    for (Term t : terms) {
088      assertFalse(names.contains(t.simpleName()), "Duplicate simple name " + t.simpleName() + " for " + t);
089      if (t instanceof AlternativeNames) {
090        for (String a : ((AlternativeNames) t).alternativeNames()) {
091          assertFalse(names.contains(a), "Duplicate alternative name " + a + " for " + t);
092          names.add(a);
093        }
094      }
095      names.add(t.simpleName());
096    }
097  }
098
099  @Test
100  public void testCompleteness() throws Exception {
101    for (ClassPath.ClassInfo info : ClassPath.from(getClass().getClassLoader()).getTopLevelClasses(DwcTerm.class.getPackage().getName())) {
102      Class<?> cl = info.load();
103      if (cl.isEnum() && Term.class.isAssignableFrom(cl)) {
104        Class<Term> tcl = (Class<Term>) cl;
105        for (Term t : tcl.getEnumConstants()) {
106          assertEquals(t, TF.findTerm(t.qualifiedName()), "Unknown term " + t.qualifiedName());
107          assertEquals(t, TF.findTerm(t.prefixedName()), "Unknown term " + t.prefixedName());
108        }
109      }
110    }
111  }
112
113  @Test
114  public void testFindTerm() {
115    assertEquals(DwcTerm.scientificName, TF.findTerm("ScientificName"));
116    assertEquals(DwcTerm.scientificName, TF.findTerm("dwc:scientificName"));
117    assertEquals(DwcTerm.scientificName, TF.findTerm("http://rs.tdwg.org/dwc/terms/scientificName"));
118    assertEquals(DcElement.identifier, TF.findTerm("dc:identifier"));
119    assertEquals(GbifTerm.Identifier, TF.findTerm("Identifier"));
120    assertEquals(GbifTerm.Identifier, TF.findClassTerm("identifier"));
121    assertEquals(DcTerm.identifier, TF.findTerm("identifier"));
122    assertEquals(DcTerm.identifier, TF.findPropertyTerm("identifier"));
123    assertEquals(DcTerm.identifier, TF.findTerm("id"));
124    assertEquals(DwcTerm.parentNameUsageID, TF.findTerm("dwc:higherNameUsageID"));
125    assertEquals(DwcTerm.acceptedNameUsageID, TF.findTerm("dwc:acceptedTaxonId"));
126    assertEquals(DwcTerm.acceptedNameUsageID, TF.findTerm("dwc:acceptedTaxonID"));
127    assertEquals(DwcTerm.acceptedNameUsageID, TF.findTerm("acceptedTaxonID"));
128    assertEquals(DwcTerm.acceptedNameUsageID, TF.findTerm("http://rs.tdwg.org/dwc/terms/acceptedTaxonId"));
129    assertEquals(DwcTerm.acceptedNameUsageID, TF.findPropertyTerm("dwc:acceptedTaxonId"));
130    assertEquals(DwcTerm.acceptedNameUsageID, TF.findPropertyTerm("dwc:acceptedTaxonID"));
131    assertEquals(DwcTerm.acceptedNameUsageID, TF.findPropertyTerm("acceptedTaxonID"));
132    assertEquals(DwcTerm.acceptedNameUsageID, TF.findPropertyTerm("http://rs.tdwg.org/dwc/terms/acceptedTaxonId"));
133    assertEquals(AcefTerm.AcceptedTaxonID, TF.findPropertyTerm("AcceptedTaxonID"));
134    assertEquals(AcefTerm.AcceptedTaxonID, TF.findTerm("acef:AcceptedTaxonID"));
135    assertEquals(AcefTerm.AcceptedTaxonID, TF.findPropertyTerm("acef:AcceptedTaxonID"));
136
137    assertEquals(DwcTerm.vernacularName, TF.findTerm("dwc:vernacularName"));
138    assertEquals(DwcTerm.vernacularName, TF.findTerm("vernacularName"));
139    assertEquals(GbifTerm.VernacularName, TF.findTerm("VernacularName"));
140    assertEquals(GbifTerm.VernacularName, TF.findTerm("gbif:VernacularName"));
141
142    assertEquals(GbifInternalTerm.unitQualifier, TF.findTerm("UNIT_QUALIFIER"));
143
144    assertEquals("threatStatus", TF.findTerm("http://rs.gbif.org/terms/1.0/threatStatus").simpleName());
145    assertEquals("threatStatus", TF.findTerm("http://rs.gbif.org/terms/1321.43/threatStatus").simpleName());
146
147    assertEquals(DwcTerm.catalogNumber, TF.findTerm("\"catalogNumber\""));
148    assertEquals(AcefTerm.Details, TF.findTerm("acef:source"));
149    assertEquals(DwcTerm.family, TF.findTerm("dwc:family"));
150    assertEquals(DwcTerm.family, TF.findTerm("family"));
151    assertEquals(AcefTerm.Family, TF.findTerm("acef:family"));
152
153    assertEquals(DwcaTerm.ID, TF.findTerm("dwca:ID"));
154
155    assertEquals(BibTexTerm.CLASS_TERM, TF.findTerm("bib:BibTeX"));
156    assertEquals(BibTexTerm.CLASS_TERM, TF.findTerm("http://bibtex.org/BibTeX"));
157
158    Term t = BibTexTerm.buildFromURI("http://bibtex.org/creator");
159    assertEquals(t, TF.findTerm("http://bibtex.org/creator"));
160    assertEquals(t, TF.findTerm("bib:creator"));
161
162    // ACEF namespace has changed: https://github.com/gbif/portal-feedback/issues/4890
163    assertEquals(AcefTerm.Country, TF.findTerm("acef:Country"));
164    assertEquals(AcefTerm.Country, TF.findTerm("http://rs.col.plus/terms/acef/Country"));
165    assertEquals(AcefTerm.Country, TF.findTerm("https://rs.col.plus/terms/acef/Country"));
166    assertEquals(DwcTerm.country, TF.findTerm("country"));
167
168    // MIxS uses unreadable identifiers in qualified names.
169    assertEquals(MixsTerm.samp_size, TF.findTerm("samp_size"));
170    assertEquals(MixsTerm.samp_size, TF.findTerm("mixs:samp_size"));
171    assertEquals(MixsTerm.samp_size, TF.findTerm("https://w3id.org/gensc/terms/MIXS:0000001"));
172    assertEquals(MixsTerm.samp_size, TF.findTerm("https://w3id.org/mixs/0000001"));
173    assertEquals(MixsTerm.lib_reads_seqd, TF.findTerm("http://gensc.org/ns/mixs/lib_reads_seqd"));
174    assertEquals(MixsTerm.assembly_name, TF.findTerm("http://gensc.org/ns/mixs/assembly"));
175    assertEquals(MixsTerm.assembly_qual, TF.findTerm("http://gensc.org/ns/mixs/finishing_strategy"));
176    assertEquals(MixsTerm.annot, TF.findTerm("http://gensc.org/ns/mixs/annot_source"));
177  }
178
179  @Test
180  public void addUnknownTerm() {
181    TermFactory factory = TermFactory.instance();
182
183    Term me1 = factory.findTerm("http://me.com/#me");
184    Term me2 = factory.findTerm("http://me.com/me");
185    Term me3 = factory.findTerm("http://me.org/me");
186
187    assertNotEquals(me1, me2);
188    assertNotEquals(me1, me3);
189    assertNotEquals(me2, me3);
190  }
191
192  @Test
193  public void badTerm() {
194    TermFactory factory = TermFactory.instance();
195    assertThrows(IllegalArgumentException.class, () -> factory.findTerm("Hallo Tim"));
196  }
197
198  @Test
199  public void removedGbifTerms() {
200    assertEquals(DwcTerm.genericName, TF.findTerm("genericName"));
201    assertEquals(DwcTerm.recordedByID, TF.findTerm("recordedByID"));
202    assertEquals(DwcTerm.identifiedByID, TF.findTerm("identifiedByID"));
203
204    assertEquals(DwcTerm.genericName, TF.findTerm("http://rs.gbif.org/terms/1.0/genericName"));
205    assertEquals(DwcTerm.recordedByID, TF.findTerm("http://rs.gbif.org/terms/1.0/recordedByID"));
206    assertEquals(DwcTerm.identifiedByID, TF.findTerm("http://rs.gbif.org/terms/1.0/identifiedByID"));
207
208    assertEquals(DwcTerm.genericName, TF.findTerm("gbif:genericName"));
209    assertEquals(DwcTerm.recordedByID, TF.findTerm("gbif:recordedByID"));
210    assertEquals(DwcTerm.identifiedByID, TF.findTerm("gbif:identifiedByID"));
211  }
212
213  @Test
214  public void addSimpleTerm() {
215    TermFactory factory = TermFactory.instance();
216
217    Term hallo = factory.findTerm("hallo");
218    assertEquals(UnknownTerm.class, hallo.getClass());
219    assertEquals("http://unknown.org/hallo", hallo.qualifiedName());
220    assertEquals("hallo", hallo.simpleName());
221
222
223    Term tim = factory.findTerm("Tim");
224    assertEquals(UnknownTerm.class, tim.getClass());
225    assertEquals("http://unknown.org/Tim", tim.qualifiedName());
226    assertEquals("http://unknown.org", tim.namespace().toString());
227    assertEquals("Tim", tim.simpleName());
228
229    Term eva = factory.findTerm("tim:Eva");
230    assertEquals(UnknownTerm.class, eva.getClass());
231    assertEquals("http://unknown.org/tim/Eva", eva.qualifiedName());
232    assertEquals("http://unknown.org", tim.namespace().toString());
233    assertEquals("http://unknown.org/tim/Eva", eva.qualifiedName());
234    assertEquals("tim:Eva", eva.prefixedName());
235    assertEquals("Eva", eva.simpleName());
236
237    assertNotEquals(hallo, tim);
238  }
239
240  @Test
241  public void addUnknownSimpleTerm() {
242    TermFactory factory = TermFactory.instance();
243
244    Term t1 = factory.findTerm("me");
245    Term t2 = factory.findTerm("me");
246    Term t3 = factory.findTerm("Ne");
247
248    assertEquals(t1, t2);
249    assertNotEquals(t1, t3);
250    assertNotEquals(t2, t3);
251  }
252
253  /**
254     * Not a real test, just a way of running many concurrent TermFactory.instance() calls to verify thread safety.
255     */
256  @Test
257  public void testMultithreadStart() throws InterruptedException {
258    int threadCount = 100;
259    ExecutorService tp = Executors.newFixedThreadPool(threadCount);
260    for (int i = 0; i < threadCount; i++) {
261      tp.submit(new TermFactoryLoader());
262    }
263    tp.shutdown();
264    tp.awaitTermination(30, TimeUnit.SECONDS);
265  }
266
267  private static class TermFactoryLoader implements Runnable {
268    @Override
269    public void run() {
270      TermFactory.instance();
271    }
272  }
273}