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.model.checklistbank; 015 016import org.gbif.api.jackson.RankSerde; 017import org.gbif.api.util.UnicodeUtils; 018import org.gbif.api.vocabulary.NamePart; 019import org.gbif.api.vocabulary.NameType; 020import org.gbif.api.vocabulary.Rank; 021 022import java.util.Objects; 023 024import org.apache.commons.lang3.StringUtils; 025 026import com.fasterxml.jackson.annotation.JsonIgnore; 027import com.fasterxml.jackson.annotation.JsonInclude; 028import com.fasterxml.jackson.annotation.JsonProperty; 029import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 030import com.fasterxml.jackson.databind.annotation.JsonSerialize; 031 032import io.swagger.v3.oas.annotations.media.Schema; 033 034/** 035 * A container of a taxon name that is atomised into its relevant, separate parts. 036 * <br> 037 * Hybrid formulas that consist of multiple genera, binomials or species 038 * epithets are relatively poorly represented. A simple boolean flag indicates a 039 * hybrid formula, e.g. Polygala vulgaris × Polygala epinema but not named 040 * hybrids that are have an × or notho as part of a well-formed 041 * mono/bi/trinomial e.g. ×Polygala vulgaris. In the case of hybrid formulas, 042 * i.e. isHybrid=true, the first name in the formula is tried to be kept, at 043 * least the genus. 044 * <p/> 045 * A container of a taxon name that is only atomised into three name parts maximum plus rank and a notho property 046 * indicating the name part of named hybrids that is considered to be the hybrid. No authorship is kept. For subgenera 047 * we don't use parentheses to indicate the subgenus, but use explicit rank markers instead. 048 */ 049@SuppressWarnings("unused") 050public class ParsedName { 051 052 public static final Character HYBRID_MARKER = '×'; 053 private static final String HYBRID_MARKER_STR = HYBRID_MARKER.toString(); 054 055 private Integer key; 056 private String scientificName; 057 @JsonProperty("rankMarker") 058 @JsonInclude(JsonInclude.Include.NON_NULL) 059 @JsonSerialize(using = RankSerde.RankJsonSerializer.class) 060 @JsonDeserialize(using = RankSerde.RankJsonDeserializer.class) 061 private Rank rank; 062 private NameType type; 063 private String genusOrAbove; 064 private String infraGeneric; 065 private String specificEpithet; 066 private String infraSpecificEpithet; 067 private String cultivarEpithet; 068 private String strain; 069 private NamePart notho; 070 private String authorship; 071 private String year; 072 private String bracketAuthorship; 073 private String bracketYear; 074 private String sensu; 075 private boolean parsed = true; 076 private boolean parsedPartially = false; 077 078 /** 079 * nomenclatural status note. 080 */ 081 private String nomStatus; 082 private String remarks; 083 084 public ParsedName() { 085 } 086 087 public ParsedName( 088 NameType type, 089 String genusOrAbove, 090 String infraGeneric, 091 String specificEpithet, 092 String infraSpecificEpithet, 093 NamePart notho, 094 Rank rank, 095 String authorship, 096 String year, 097 String bracketAuthorship, 098 String bracketYear, 099 String cultivarEpithet, 100 String strain, 101 String sensu, 102 String nomStatus, 103 String remarks 104 ) { 105 this.type = type; 106 this.genusOrAbove = genusOrAbove; 107 this.infraGeneric = infraGeneric; 108 this.specificEpithet = specificEpithet; 109 this.infraSpecificEpithet = infraSpecificEpithet; 110 this.notho = notho; 111 this.rank = rank; 112 this.authorship = authorship; 113 this.year = year; 114 this.bracketAuthorship = bracketAuthorship; 115 this.bracketYear = bracketYear; 116 this.cultivarEpithet = cultivarEpithet; 117 this.strain = strain; 118 this.sensu = sensu; 119 this.nomStatus = nomStatus; 120 this.remarks = remarks; 121 } 122 123 /** 124 * The original author of this name, e.g. basionym or recombination author 125 */ 126 @Schema(description = "The original author of this name, e.g. basionym or recombination author.") 127 public String getAuthorship() { 128 return authorship; 129 } 130 131 public void setAuthorship(String authorship) { 132 this.authorship = authorship; 133 } 134 135 /** 136 * The authorship of the original name, i.e. basionym, given in brackets. 137 */ 138 @Schema(description = "The authorship of the original name, i.e. basionym, given in brackets.") 139 public String getBracketAuthorship() { 140 return bracketAuthorship; 141 } 142 143 public void setBracketAuthorship(String bracketAuthorship) { 144 this.bracketAuthorship = bracketAuthorship; 145 } 146 147 /** 148 * The code relevant year of publication of the original name, i.e. basionym, given in brackets. 149 */ 150 @Schema(description = "The code relevant year of publication of the original name, i.e. basionym, given in brackets.") 151 public String getBracketYear() { 152 return bracketYear; 153 } 154 155 public void setBracketYear(String bracketYear) { 156 this.bracketYear = bracketYear; 157 } 158 159 /** 160 * The cultivar, cultivar group or grex part of a cultivated plant name. 161 * If given the name should be of type NameType.CULTIVAR 162 */ 163 @Schema(description = "The cultivar, cultivar group or grex part of a cultivated plant name.\n\n" + 164 "If given the name should be of type CULTIVAR") 165 public String getCultivarEpithet() { 166 return cultivarEpithet; 167 } 168 169 public void setCultivarEpithet(String cultivarEpithet) { 170 this.cultivarEpithet = cultivarEpithet; 171 } 172 173 /** 174 * The strain or isolate name. Usually a capital collection code string followed by an accession number. 175 * See <a href="http://www.bacterio.net/-collections.html">List of culture collection codes</a> 176 * If given the name should be of type NameType.STRAIN 177 */ 178 @Schema(description = "The strain or isolate name. Usually a capital collection code string followed by an accession number.\n\n" + 179 "See [List of culture collection codes](http://www.bacterio.net/-collections.html). If given the name should be of type STRAIN.") 180 public String getStrain() { 181 return strain; 182 } 183 184 public void setStrain(String strain) { 185 this.strain = strain; 186 } 187 188 /** 189 * The genus part of a bi/trinomial or the monomial in case of names of higher ranks 190 */ 191 @Schema(description = "The genus part of a bi/trinomial or the monomial in case of names of higher ranks.") 192 public String getGenusOrAbove() { 193 return genusOrAbove; 194 } 195 196 /** 197 * The infrageneric part of a name, often given in parentheses between genus and species epithet, e.g. for a subgenus 198 */ 199 @Schema(description = "The infrageneric part of a name, often given in parentheses between genus and species epithet, " + 200 "e.g. for a subgenus") 201 public String getInfraGeneric() { 202 return infraGeneric; 203 } 204 205 @Schema(description = "The infraspecific part of a name.") 206 public String getInfraSpecificEpithet() { 207 return infraSpecificEpithet; 208 } 209 210 /** 211 * Any nomenclatoral remarks given in this name, e.g. nom. illeg. 212 */ 213 @Schema(description = "Any nomenclatoral remarks given in this name, e.g. nom. illeg.") 214 public String getNomStatus() { 215 return nomStatus; 216 } 217 218 public void setNomStatus(String nomStatus) { 219 this.nomStatus = nomStatus; 220 } 221 222 /** 223 * For hybrid names notho indicates which part of the name is considered a hybrid, 224 * i.e. genus, species or infraspecific epithet. 225 */ 226 @Schema(description = "For hybrid names notho indicates which part of the name is considered a hybrid, " + 227 "i.e. genus, species or infraspecific epithet.") 228 public NamePart getNotho() { 229 return notho; 230 } 231 232 public void setNotho(NamePart notho) { 233 this.notho = notho; 234 } 235 236 /** 237 * Any further remarks found 238 */ 239 @Schema(description = "Any further remarks found.") 240 public String getRemarks() { 241 return remarks; 242 } 243 244 public void setRemarks(String remarks) { 245 this.remarks = remarks; 246 } 247 248 /** 249 * Taxon concept references as part of the name, 250 * e.g. "MSW2005" for Gorilla gorilla (Savage, 1847) sec. MSW2005 251 */ 252 @Schema(description = "Taxon concept references as part of the name, e.g. “MSW2006” for *Gorilla gorilla* " + 253 "(Savage, 1847) sec. MSW2005") 254 public String getSensu() { 255 return sensu; 256 } 257 258 public void setSensu(String sensu) { 259 this.sensu = sensu; 260 } 261 262 @Schema(description = "The specific epithet of the name") 263 public String getSpecificEpithet() { 264 return specificEpithet; 265 } 266 267 /** 268 * @return the terminal epithet, infraspecific epithet if existing, the species epithet or null 269 */ 270 @Schema(description = "The terminal epithet, infraspecific epithet if existing, the species epithet or null") 271 @JsonIgnore 272 public String getTerminalEpithet() { 273 return infraSpecificEpithet == null ? specificEpithet : infraSpecificEpithet; 274 } 275 276 /** 277 * A coarse classification of names helping to deal with different syntactical name string structures. 278 */ 279 @Schema(description = "A coarse classification of names helping to deal with different syntactical name string structures.") 280 public NameType getType() { 281 return type; 282 } 283 284 public void setType(NameType type) { 285 this.type = type; 286 } 287 288 /** 289 * The year of publication as given in the authorship. 290 */ 291 @Schema(description = "The year of publication as given in the authorship.") 292 public String getYear() { 293 return year; 294 } 295 296 public void setYear(String year) { 297 this.year = year; 298 } 299 300 @JsonIgnore 301 public boolean hasAuthorship() { 302 return authorship != null || year != null || bracketAuthorship != null || bracketYear != null; 303 } 304 305 @Schema(description = "The taxonomic rank of the name.") 306 public void setRank(Rank rank) { 307 this.rank = rank; 308 } 309 310 @Schema(description = "The key for this parsed name object.") 311 public Integer getKey() { 312 return key; 313 } 314 315 public void setKey(Integer key) { 316 this.key = key; 317 } 318 319 /** 320 * The exact verbatim, full scientific name as given before parsing. 321 */ 322 @Schema(description = "The exact verbatim, full scientific name as given before parsing.") 323 public String getScientificName() { 324 return scientificName; 325 } 326 327 public void setScientificName(String scientificName) { 328 this.scientificName = scientificName; 329 } 330 331 /** 332 * The GBIF name parser cannot always parse the entire name. 333 * This is often the case when taxonomic, nomenclatural or identification notes are added to the end of the name. 334 * In such cases when the name was only partially parsed (NameUsageIssue.PARTIALLY_PARSABLE) 335 * this flag should be true. 336 * 337 * If the name was not parsed at all this will be false. 338 * 339 * @return true if name was only partially parsed, false otherwise 340 */ 341 @Schema(description = "The GBIF name parser cannot always parse the entire name.\n\n" + 342 "This is often the case when taxonomic, nomenclatural or identification notes are added to the end of the name. " + 343 "In such cases when the name was only partially parsed (NameUsageIssue.PARTIALLY_PARSABLE) " + 344 "this flag should be true.\n\n" + 345 "If the name was not parsed at all this will be false.") 346 public boolean isParsedPartially() { 347 return parsedPartially; 348 } 349 350 public void setParsedPartially(boolean parsedPartially) { 351 this.parsedPartially = parsedPartially; 352 } 353 354 /** 355 * A flag indicating if a name could not be parsed at all. 356 * If false, only the scientific name, rank and potentially the name type is given. 357 */ 358 @Schema(description = "A flag indicating if a name could not be parsed at all.\n\n" + 359 "If false, only the scientific name, rank and potentially the name type is given.") 360 public boolean isParsed() { 361 return parsed; 362 } 363 364 public void setParsed(boolean parsed) { 365 this.parsed = parsed; 366 } 367 368 /** 369 * @return The full concatenated authorship or null if it is a hybrid 370 */ 371 public String authorshipComplete() { 372 StringBuilder sb = new StringBuilder(); 373 appendAuthorship(sb); 374 return sb.toString().trim(); 375 } 376 377 /** 378 * build a name controlling all available flags for name parts to be included in the resulting name. 379 * 380 * @param hybridMarker include the hybrid marker with the name if existing 381 * @param rankMarker include the infraspecific or infrageneric rank marker with the name if existing 382 * @param authorship include the names authorship (authorteam and year) 383 * @param infrageneric include the infrageneric name in brackets for species or infraspecies 384 * @param genusForInfrageneric include the genus name in front of an infrageneric name (not a species) 385 * @param abbreviateGenus if true abreviate the genus with its first character 386 * @param decomposition decompose unicode ligatures into their corresponding ascii ones, e.g. æ beomes ae 387 * @param asciiOnly transform unicode letters into their corresponding ascii ones, e.g. ø beomes o and ü u 388 * @param showIndet if true include the rank marker for incomplete determinations, for example Puma spec. 389 * @param nomNote include nomenclatural notes 390 * @param remarks include informal remarks 391 */ 392 public String buildName( 393 boolean hybridMarker, 394 boolean rankMarker, 395 boolean authorship, 396 boolean infrageneric, 397 boolean genusForInfrageneric, 398 boolean abbreviateGenus, 399 boolean decomposition, 400 boolean asciiOnly, 401 boolean showIndet, 402 boolean nomNote, 403 boolean remarks, 404 boolean showSensu, 405 boolean showCultivar, 406 boolean showStrain 407 ) { 408 StringBuilder sb = new StringBuilder(); 409 410 if (NameType.CANDIDATUS == type) { 411 sb.append("Candidatus "); 412 } 413 414 if (genusOrAbove != null && (genusForInfrageneric || infraGeneric == null || specificEpithet != null)) { 415 if (hybridMarker && NamePart.GENERIC == notho) { 416 sb.append(HYBRID_MARKER); 417 } 418 if (abbreviateGenus) { 419 sb.append(genusOrAbove.charAt(0)).append('.'); 420 } else { 421 sb.append(genusOrAbove); 422 } 423 } 424 if (specificEpithet == null) { 425 if (Rank.SPECIES == rank) { 426 // no species epitheton given, but rank=species. Indetermined species! 427 if (showIndet(showIndet)) { 428 sb.append(" spec."); 429 } 430 } else if (rank != null && rank.isInfraspecific()) { 431 // no species epitheton given, but rank below species. Indetermined! 432 if (showIndet(showIndet)) { 433 sb.append(' '); 434 sb.append(rank.getMarker()); 435 } 436 } else if (infraGeneric != null) { 437 // this is the terminal name part - always show it! 438 if (rankMarker && rank != null) { 439 // If we know the rank we use explicit rank markers 440 // this is how botanical infrageneric names are formed, see http://www.iapt-taxon.org/nomen/main.php?page=art21 441 sb.append(' '); 442 appendRankMarker(sb, rank); 443 sb.append(infraGeneric); 444 445 } else { 446 if (genusForInfrageneric && genusOrAbove != null) { 447 // if we have shown the genus already and we do not know the rank we use parenthesis to indicate an infrageneric 448 sb.append(" (") 449 .append(infraGeneric) 450 .append(")"); 451 } else { 452 // no genus shown yet, just show the plain infrageneric name 453 sb.append(infraGeneric); 454 } 455 } 456 } 457 // genus/infrageneric authorship 458 if (authorship) { 459 appendAuthorship(sb); 460 } 461 } else { 462 if (infrageneric && infraGeneric != null && (rank == null || rank == Rank.GENUS)) { 463 // only show subgenus if requested 464 sb.append(" ("); 465 sb.append(infraGeneric); 466 sb.append(')'); 467 } 468 469 // species part 470 sb.append(' '); 471 if (hybridMarker && NamePart.SPECIFIC == notho) { 472 sb.append(HYBRID_MARKER); 473 } 474 String epi = specificEpithet.replaceAll("[ _-]", "-"); 475 sb.append(epi); 476 477 if (infraSpecificEpithet == null) { 478 // Indetermined? Only show indet cultivar marker if no cultivar epithet exists 479 if (showIndet(showIndet) && rank != null && rank.isInfraspecific() && (Rank.CULTIVAR != rank || cultivarEpithet == null)) { 480 // no infraspecific epitheton given, but rank below species. Indetermined! 481 sb.append(' '); 482 sb.append(rank.getMarker()); 483 } 484 485 // species authorship 486 if (authorship) { 487 appendAuthorship(sb); 488 } 489 } else { 490 // infraspecific part 491 sb.append(' '); 492 if (hybridMarker && NamePart.INFRASPECIFIC == notho) { 493 if (rankMarker) { 494 sb.append("notho"); 495 } else { 496 sb.append(HYBRID_MARKER); 497 } 498 } 499 if (rankMarker) { 500 appendRankMarker(sb, rank); 501 } 502 epi = infraSpecificEpithet.replaceAll("[ _-]", "-"); 503 sb.append(epi); 504 // non autonym authorship ? 505 if (authorship && !isAutonym()) { 506 appendAuthorship(sb); 507 } 508 } 509 } 510 511 // add cultivar name 512 if (showStrain && strain != null) { 513 sb.append(" "); 514 sb.append(strain); 515 } 516 517 // add cultivar name 518 if (showCultivar && cultivarEpithet != null) { 519 sb.append(" '"); 520 sb.append(cultivarEpithet); 521 sb.append("'"); 522 } 523 524 // add sensu/sec reference 525 if (showSensu && sensu != null) { 526 sb.append(" "); 527 sb.append(sensu); 528 } 529 530 // add nom status 531 if (nomNote && nomStatus != null) { 532 sb.append(", "); 533 sb.append(nomStatus); 534 } 535 536 // add remarks 537 if (remarks && this.remarks != null) { 538 sb.append(" ["); 539 sb.append(this.remarks); 540 sb.append("]"); 541 } 542 543 String name = sb.toString().trim(); 544 if (decomposition) { 545 name = UnicodeUtils.decompose(name); 546 } 547 if (asciiOnly) { 548 name = UnicodeUtils.ascii(name); 549 } 550 551 return StringUtils.trimToNull(name); 552 } 553 554 private boolean showIndet(boolean showIndet) { 555 return showIndet && (type == null || type.isParsable()); 556 } 557 558 private void appendRankMarker(StringBuilder sb, Rank rank) { 559 if (rank != null && !rank.isUncomparable() && rank.getMarker() != null) { 560 sb.append(rank.getMarker()); 561 sb.append(' '); 562 } 563 } 564 565 private void appendAuthorship(StringBuilder sb) { 566 if (bracketAuthorship == null) { 567 if (bracketYear != null) { 568 sb.append(" ("); 569 sb.append(bracketYear); 570 sb.append(")"); 571 } 572 } else { 573 sb.append(" ("); 574 sb.append(bracketAuthorship); 575 if (bracketYear != null) { 576 sb.append(", "); 577 sb.append(bracketYear); 578 } 579 sb.append(")"); 580 } 581 if (authorship != null) { 582 sb.append(" ").append(authorship); 583 } 584 if (year != null) { 585 sb.append(", "); 586 sb.append(year); 587 } 588 } 589 590 /** 591 * The canonical name sensu strictu with nothing else but 3 name parts at max (genus, species, infraspecific). No 592 * rank or hybrid markers and no authorship, cultivar or strain information. 593 * Infrageneric names are represented without a leading genus. 594 * Unicode characters will be replaced by their matching ASCII characters. 595 * <p/> 596 * For example: 597 * Abies alba 598 * Abies alba alpina 599 * Abies Bracteata 600 * Heucherella tiarelloides 601 * 602 * @return the 1, 2 or 3 parted name as a single string 603 */ 604 @Schema(description = "The canonical name *sensu strictu* with nothing else but three name parts at most (genus, " + 605 "species, infraspecific).\n\n" + 606 "No rank or hybrid markers and no authorship, cultivar or strain information.\n\n" + 607 "Infrageneric names are represented without a leading genus.\n\n" + 608 "Unicode characters are replaced by their matching ASCII characters.") 609 @JsonProperty 610 public String canonicalName() { 611 return buildName(false, false, false, false, false, false, true, true, true, false, false, false, false, false); 612 } 613 614 /** 615 * The code-compliant, canonical name with three name parts at max (genus, species, infraspecific), a rank marker for 616 * infraspecific names and cultivar or strain epithets. The canonical name can be a 1, 2 or 3 parted name, but does 617 * not include any informal notes or authorships. Notho taxa will have the hybrid marker. 618 * Unicode characters will be replaced by their matching ASCII characters. 619 * <p/> 620 * For example: 621 * Abies alba 622 * Abies alba subsp. alpina 623 * Abies sect. Bracteata 624 * ×Heucherella tiarelloides 625 * 626 * @return the 1,2 or 3 parted name as a single string 627 */ 628 @Schema(description = "The code-compliant, canonical name with three name parts at max (genus, species, " + 629 "infraspecific), a rank marker for infraspecific names and cultivar or strain epithets. The canonical name can " + 630 "be a 1, 2 or 3 parted name, but does not include any informal notes or authorships. Notho taxa will have the " + 631 "hybrid marker.\n\n" + 632 "Unicode characters will be replaced by their matching ASCII characters.") 633 @JsonProperty 634 public String canonicalNameWithMarker() { 635 return buildName(true, true, false, false, false, false, true, true, true, false, false, false, true, true); 636 } 637 638 /** 639 * The code-compliant, canonical name with rank and hybrid marker, authorship and cultivar or strain name included. 640 * Informal or nomenclatoral notes, concept references, subgenus and non-terminal authorships are removed. 641 * @return the 1,2 or 3 parted name as a single string 642 */ 643 @Schema(description = "The code-compliant, canonical name with rank and hybrid marker, authorship and cultivar " + 644 "or strain name included. Informal or nomenclatoral notes, concept references, subgenus and non-terminal " + 645 "authorships are removed.") 646 @JsonProperty 647 public String canonicalNameComplete() { 648 return buildName(true, true, true, false, true, false, true, false, true, false, false, false, true, true); 649 } 650 651 /** 652 * @return the species binomial if this parsed name is a species or below. Or null in case its superspecific 653 */ 654 public String canonicalSpeciesName() { 655 if (genusOrAbove != null && specificEpithet != null) { 656 return genusOrAbove + " " + specificEpithet; 657 } 658 return null; 659 } 660 661 /** 662 * @return the name with all details that exist. 663 */ 664 public String fullName() { 665 return buildName(true, true, true, true, true, false, false, false, true, true, true, true, true, true); 666 } 667 668 @JsonIgnore 669 public Integer getBracketYearInt() { 670 try { 671 return Integer.parseInt(bracketYear); 672 } catch (NumberFormatException e) { 673 return null; 674 } 675 } 676 677 /** 678 * @return rank as enumeration or null 679 */ 680 public Rank getRank() { 681 return rank; 682 } 683 684 @JsonIgnore 685 public Integer getYearInt() { 686 try { 687 return Integer.parseInt(year); 688 } catch (NumberFormatException e) { 689 return null; 690 } 691 } 692 693 @JsonIgnore 694 public boolean isAutonym() { 695 return specificEpithet != null && specificEpithet.equals(infraSpecificEpithet); 696 } 697 698 @JsonIgnore 699 public boolean isBinomial() { 700 return genusOrAbove != null && specificEpithet != null; 701 } 702 703 @JsonIgnore 704 public boolean isHybridFormula() { 705 return NameType.HYBRID == type; 706 } 707 708 /** 709 * @return true for names with an infraspecifc rank but missing lowest name part. E.g. Coccyzuz americanus ssp. or 710 * Asteraceae 711 * spec. but not Maxillaria sect. Acaules 712 */ 713 @JsonIgnore 714 public boolean isIndetermined() { 715 return rank != null && isParsed() && isParsableType() && rank.notOtherOrUnknown() && ( 716 (rank.isInfragenericStrictly() && infraGeneric == null) 717 || (rank.isSpeciesAggregateOrBelow() && specificEpithet == null) 718 || (rank.isInfraspecific() && infraSpecificEpithet == null) 719 ); 720 } 721 722 @JsonIgnore 723 public boolean isParsableType() { 724 return type != null && type.isParsable(); 725 } 726 727 @JsonIgnore 728 public boolean isQualified() { 729 return (authorship != null && !authorship.isEmpty()) 730 || (year != null && !year.isEmpty()) 731 || (bracketAuthorship != null && !bracketAuthorship.isEmpty()) 732 || (bracketYear != null && !bracketYear.isEmpty()); 733 } 734 735 /** 736 * @return true if a bracket authorship is given, indicating that the name has been subsequently recombined. 737 */ 738 @JsonIgnore 739 public boolean isRecombination() { 740 return (!StringUtils.isBlank(bracketAuthorship) || !StringUtils.isBlank(bracketYear)); 741 } 742 743 744 public void setGenusOrAbove(String genusOrAbove) { 745 if (genusOrAbove != null && genusOrAbove.startsWith(HYBRID_MARKER.toString())) { 746 this.genusOrAbove = genusOrAbove.substring(1); 747 notho = NamePart.GENERIC; 748 } else { 749 this.genusOrAbove = genusOrAbove; 750 } 751 } 752 753 public void setHybridFormula(boolean hybrid) { 754 if (hybrid) { 755 type = NameType.HYBRID; 756 } else if (NameType.HYBRID == type) { 757 type = null; 758 } 759 } 760 761 public void setInfraGeneric(String infraGeneric) { 762 if (infraGeneric != null && infraGeneric.startsWith(HYBRID_MARKER_STR)) { 763 this.infraGeneric = infraGeneric.substring(1); 764 notho = NamePart.INFRAGENERIC; 765 } else { 766 this.infraGeneric = infraGeneric; 767 } 768 } 769 770 public void setInfraSpecificEpithet(String infraSpecies) { 771 if (infraSpecies != null && infraSpecies.startsWith(HYBRID_MARKER_STR)) { 772 this.infraSpecificEpithet = infraSpecies.substring(1); 773 this.notho = NamePart.INFRASPECIFIC; 774 } else { 775 this.infraSpecificEpithet = infraSpecies; 776 } 777 } 778 779 public void setSpecificEpithet(String species) { 780 if (species != null && species.startsWith(HYBRID_MARKER_STR)) { 781 specificEpithet = species.substring(1); 782 notho = NamePart.SPECIFIC; 783 } else { 784 specificEpithet = species; 785 } 786 } 787 788 @Override 789 public boolean equals(Object o) { 790 if (this == o) { 791 return true; 792 } 793 if (o == null || getClass() != o.getClass()) { 794 return false; 795 } 796 ParsedName that = (ParsedName) o; 797 return Objects.equals(key, that.key) && 798 Objects.equals(scientificName, that.scientificName) && 799 rank == that.rank && 800 type == that.type && 801 Objects.equals(genusOrAbove, that.genusOrAbove) && 802 Objects.equals(infraGeneric, that.infraGeneric) && 803 Objects.equals(specificEpithet, that.specificEpithet) && 804 Objects.equals(infraSpecificEpithet, that.infraSpecificEpithet) && 805 Objects.equals(cultivarEpithet, that.cultivarEpithet) && 806 Objects.equals(strain, that.strain) && 807 Objects.equals(authorship, that.authorship) && 808 Objects.equals(year, that.year) && 809 Objects.equals(bracketAuthorship, that.bracketAuthorship) && 810 Objects.equals(bracketYear, that.bracketYear); 811 } 812 813 @Override 814 public int hashCode() { 815 return Objects 816 .hash(key, scientificName, rank, type, genusOrAbove, infraGeneric, specificEpithet, 817 infraSpecificEpithet, cultivarEpithet, strain, authorship, year, bracketAuthorship, 818 bracketYear); 819 } 820 821 @Override 822 public String toString() { 823 StringBuilder sb = new StringBuilder(); 824 sb.append(scientificName); 825 if (key != null) { 826 sb.append(" ["); 827 sb.append(key); 828 sb.append("]"); 829 } 830 if (genusOrAbove != null) { 831 sb.append(" G:").append(genusOrAbove); 832 } 833 if (infraGeneric != null) { 834 sb.append(" IG:").append(infraGeneric); 835 } 836 if (specificEpithet != null) { 837 sb.append(" S:").append(specificEpithet); 838 } 839 if (rank != null) { 840 sb.append(" R:").append(rank); 841 } 842 if (infraSpecificEpithet != null) { 843 sb.append(" IS:").append(infraSpecificEpithet); 844 } 845 if (cultivarEpithet != null) { 846 sb.append(" CV:").append(cultivarEpithet); 847 } 848 if (strain != null) { 849 sb.append(" STR:").append(strain); 850 } 851 if (authorship != null) { 852 sb.append(" A:").append(authorship); 853 } 854 if (year != null) { 855 sb.append(" Y:").append(year); 856 } 857 if (bracketAuthorship != null) { 858 sb.append(" BA:").append(bracketAuthorship); 859 } 860 if (bracketYear != null) { 861 sb.append(" BY:").append(bracketYear); 862 } 863 if (type != null) { 864 sb.append(" ["); 865 sb.append(type); 866 sb.append("]"); 867 } 868 return isHybridFormula() ? " [hybrid]" : sb.toString(); 869 } 870}