2 * Copyright (C) 2007 EDIT
3 * European Distributed Institute of Taxonomy
4 * http://www.e-taxonomy.eu
6 * The contents of this file are subject to the Mozilla Public License Version 1.1
7 * See LICENSE.TXT at the top of this package for the full license terms.
10 package eu
.etaxonomy
.cdm
.api
.facade
;
12 import java
.beans
.PropertyChangeEvent
;
13 import java
.beans
.PropertyChangeListener
;
14 import java
.beans
.PropertyChangeSupport
;
15 import java
.text
.ParseException
;
16 import java
.util
.ArrayList
;
17 import java
.util
.HashMap
;
18 import java
.util
.HashSet
;
19 import java
.util
.List
;
23 import javax
.mail
.MethodNotSupportedException
; //FIMXE use other execption class
24 import javax
.persistence
.Transient
;
25 import javax
.xml
.bind
.annotation
.XmlTransient
;
27 import org
.apache
.log4j
.Logger
;
29 import eu
.etaxonomy
.cdm
.model
.agent
.AgentBase
;
30 import eu
.etaxonomy
.cdm
.model
.agent
.Person
;
31 import eu
.etaxonomy
.cdm
.model
.common
.CdmBase
;
32 import eu
.etaxonomy
.cdm
.model
.common
.IdentifiableSource
;
33 import eu
.etaxonomy
.cdm
.model
.common
.Language
;
34 import eu
.etaxonomy
.cdm
.model
.common
.LanguageString
;
35 import eu
.etaxonomy
.cdm
.model
.common
.TimePeriod
;
36 import eu
.etaxonomy
.cdm
.model
.description
.DescriptionElementBase
;
37 import eu
.etaxonomy
.cdm
.model
.description
.Feature
;
38 import eu
.etaxonomy
.cdm
.model
.description
.Sex
;
39 import eu
.etaxonomy
.cdm
.model
.description
.SpecimenDescription
;
40 import eu
.etaxonomy
.cdm
.model
.description
.Stage
;
41 import eu
.etaxonomy
.cdm
.model
.description
.TextData
;
42 import eu
.etaxonomy
.cdm
.model
.location
.NamedArea
;
43 import eu
.etaxonomy
.cdm
.model
.location
.Point
;
44 import eu
.etaxonomy
.cdm
.model
.location
.ReferenceSystem
;
45 import eu
.etaxonomy
.cdm
.model
.media
.Media
;
46 import eu
.etaxonomy
.cdm
.model
.name
.TaxonNameBase
;
47 import eu
.etaxonomy
.cdm
.model
.occurrence
.Collection
;
48 import eu
.etaxonomy
.cdm
.model
.occurrence
.DerivationEvent
;
49 import eu
.etaxonomy
.cdm
.model
.occurrence
.DerivedUnitBase
;
50 import eu
.etaxonomy
.cdm
.model
.occurrence
.DeterminationEvent
;
51 import eu
.etaxonomy
.cdm
.model
.occurrence
.FieldObservation
;
52 import eu
.etaxonomy
.cdm
.model
.occurrence
.GatheringEvent
;
53 import eu
.etaxonomy
.cdm
.model
.occurrence
.PreservationMethod
;
54 import eu
.etaxonomy
.cdm
.model
.occurrence
.Specimen
;
55 import eu
.etaxonomy
.cdm
.model
.occurrence
.SpecimenOrObservationBase
;
56 import eu
.etaxonomy
.cdm
.model
.reference
.ReferenceBase
;
59 * This class is a facade to the eu.etaxonomy.cdm.model.occurrence package from
60 * a specimen based view. It does not support all functionality available in the
61 * occurrence package.<BR>
62 * The most significant restriction is that a specimen may derive only from
63 * one direct derivation event and there must be only one field observation (gathering event)
64 * it derives from.<BR>
69 public class DerivedUnitFacade
{
70 private static final Logger logger
= Logger
.getLogger(DerivedUnitFacade
.class);
72 private static final String notSupportMessage
= "A specimen facade not supported exception has occurred at a place where this should not have happened. The developer should implement not support check properly during class initialization ";
76 private PropertyChangeSupport propertyChangeSupport
= new PropertyChangeSupport(this);
79 * Enum that defines the class the "Specimen" belongs to.
80 * Some methods of the facade are not available for certain classes
81 * and will throw an Exception when invoking them.
83 public enum DerivedUnitType
{
84 Specimen ("Specimen"),
85 Observation("Observation"),
86 LivingBeing("Living Being"),
88 DerivedUnit("Derived Unit");
90 String representation
;
91 private DerivedUnitType(String representation
){
92 this.representation
= representation
;
96 * @return the representation
98 public String
getRepresentation() {
99 return representation
;
102 private DerivedUnitBase
getNewDerivedUnitInstance(){
103 if (this == DerivedUnitType
.Specimen
){
104 return eu
.etaxonomy
.cdm
.model
.occurrence
.Specimen
.NewInstance();
105 }else if (this == DerivedUnitType
.Observation
){
106 return eu
.etaxonomy
.cdm
.model
.occurrence
.Observation
.NewInstance();
107 }else if (this == DerivedUnitType
.LivingBeing
){
108 return eu
.etaxonomy
.cdm
.model
.occurrence
.LivingBeing
.NewInstance();
109 }else if (this == DerivedUnitType
.Fossil
){
110 return eu
.etaxonomy
.cdm
.model
.occurrence
.Fossil
.NewInstance();
111 }else if (this == DerivedUnitType
.DerivedUnit
){
112 return eu
.etaxonomy
.cdm
.model
.occurrence
.DerivedUnit
.NewInstance();
114 throw new IllegalStateException("Unknown derived unit type " + this.getRepresentation());
121 private DerivedUnitFacadeConfigurator config
;
123 //private GatheringEvent gatheringEvent;
124 private DerivedUnitType type
; //needed?
126 private FieldObservation fieldObservation
;
128 private DerivedUnitBase derivedUnit
;
130 //media - the text data holding the media
131 private TextData derivedUnitMediaTextData
;
132 private TextData fieldObjectMediaTextData
;
135 private TextData ecology
;
136 private TextData plantDescription
;
138 public static DerivedUnitFacade
NewInstance(DerivedUnitType type
){
139 return new DerivedUnitFacade(type
);
142 public static DerivedUnitFacade
NewInstance(DerivedUnitBase derivedUnit
) throws DerivedUnitFacadeNotSupportedException
{
143 return new DerivedUnitFacade(derivedUnit
, null, DerivedUnitType
.Specimen
);
146 public static DerivedUnitFacade
NewInstance(DerivedUnitBase derivedUnit
, DerivedUnitFacadeConfigurator config
) throws DerivedUnitFacadeNotSupportedException
{
147 return new DerivedUnitFacade(derivedUnit
, config
, DerivedUnitType
.Specimen
);
150 // ****************** CONSTRUCTOR ****************************************************
152 private DerivedUnitFacade(DerivedUnitType type
){
153 this.config
= DerivedUnitFacadeConfigurator
.NewInstance();
156 derivedUnit
= type
.getNewDerivedUnitInstance();
160 private DerivedUnitFacade(DerivedUnitBase derivedUnit
, DerivedUnitFacadeConfigurator config
, DerivedUnitType type
) throws DerivedUnitFacadeNotSupportedException
{
161 //this.type = type; ??
164 config
= DerivedUnitFacadeConfigurator
.NewInstance();
166 this.config
= config
;
169 this.derivedUnit
= derivedUnit
;
173 if (this.derivedUnit
.getDerivedFrom() != null){
174 DerivationEvent derivationEvent
= getDerivationEvent(true);
176 Set
<FieldObservation
> fieldOriginals
= getFieldObservationsOriginals(derivationEvent
, null);
177 if (fieldOriginals
.size() > 1){
178 throw new DerivedUnitFacadeNotSupportedException("Specimen must not have more than 1 derivation event");
179 }else if (fieldOriginals
.size() == 0){
180 //fieldObservation = FieldObservation.NewInstance();
181 }else if (fieldOriginals
.size() == 1){
182 fieldObservation
= fieldOriginals
.iterator().next();
184 throw new IllegalStateException("Illegal state");
188 //test if unsupported
192 // String objectTypeExceptionText = "Specimen";
193 // SpecimenDescription imageGallery = getImageGalleryWithSupportTest(derivedUnit, objectTypeExceptionText, false);
194 // getImageTextDataWithSupportTest(imageGallery, objectTypeExceptionText);
195 this.derivedUnitMediaTextData
= inititialzeTextDataWithSupportTest(Feature
.IMAGE(), this.derivedUnit
, false, true);
198 // objectTypeExceptionText = "Field observation";
199 // imageGallery = getImageGalleryWithSupportTest(fieldObservation, objectTypeExceptionText, false);
200 // getImageTextDataWithSupportTest(imageGallery, objectTypeExceptionText);
201 fieldObjectMediaTextData
= initializeFieldObjectTextDataWithSupportTest(Feature
.IMAGE(), false, true);
203 //handle derivedUnit.getMedia()
204 if (derivedUnit
.getMedia().size() > 0){
205 //TODO better changed model here to allow only one place for images
206 if (this.config
.isMoveDerivedUnitMediaToGallery()){
207 Set
<Media
> mediaSet
= derivedUnit
.getMedia();
208 for (Media media
: mediaSet
){
209 this.addDerivedUnitMedia(media
);
211 mediaSet
.removeAll(getDerivedUnitMedia());
213 throw new DerivedUnitFacadeNotSupportedException("Specimen may not have direct media. Only (one) image gallery is allowed");
217 //handle fieldObservation.getMedia()
218 if (fieldObservation
!= null && fieldObservation
.getMedia() != null && fieldObservation
.getMedia().size() > 0){
219 //TODO better changed model here to allow only one place for images
220 if (this.config
.isMoveFieldObjectMediaToGallery()){
221 Set
<Media
> mediaSet
= fieldObservation
.getMedia();
222 for (Media media
: mediaSet
){
223 this.addFieldObjectMedia(media
);
225 mediaSet
.removeAll(getFieldObjectMedia());
227 throw new DerivedUnitFacadeNotSupportedException("Field object may not have direct media. Only (one) image gallery is allowed");
231 //test if descriptions are supported
232 ecology
= initializeFieldObjectTextDataWithSupportTest(Feature
.ECOLOGY(), false, false);
233 plantDescription
= initializeFieldObjectTextDataWithSupportTest(Feature
.DESCRIPTION(), false, false);
240 private void setCacheStrategy() {
241 derivedUnit
.setCacheStrategy(new DerivedUnitFacadeCacheStrategy());
247 * @param createIfNotExists
248 * @param isImageGallery
250 * @throws DerivedUnitFacadeNotSupportedException
252 private TextData
initializeFieldObjectTextDataWithSupportTest(Feature feature
, boolean createIfNotExists
, boolean isImageGallery
) throws DerivedUnitFacadeNotSupportedException
{
254 FieldObservation fieldObject
= getFieldObservation(createIfNotExists
) ;
255 if (fieldObject
== null){
258 return inititialzeTextDataWithSupportTest(feature
, fieldObject
, createIfNotExists
, isImageGallery
);
265 * @param createIfNotExists
266 * @param isImageGallery
268 * @throws DerivedUnitFacadeNotSupportedException
270 private TextData
inititialzeTextDataWithSupportTest(Feature feature
, SpecimenOrObservationBase specimen
, boolean createIfNotExists
,
271 boolean isImageGallery
) throws DerivedUnitFacadeNotSupportedException
{
272 if (feature
== null ){
275 TextData textData
= null;
276 if (createIfNotExists
){
277 textData
= TextData
.NewInstance(feature
);
280 Set
<SpecimenDescription
> descriptions
;
282 descriptions
= specimen
.getSpecimenDescriptionImageGallery();
284 descriptions
= specimen
.getSpecimenDescriptions(false);
286 if (descriptions
.size() == 0){
287 if (createIfNotExists
){
288 SpecimenDescription newSpecimenDescription
= SpecimenDescription
.NewInstance(specimen
);
289 newSpecimenDescription
.addElement(textData
);
295 Set
<DescriptionElementBase
> existingTextData
= new HashSet
<DescriptionElementBase
>();
296 for (SpecimenDescription description
: descriptions
){
297 for (DescriptionElementBase element
: description
.getElements()){
298 if (element
.isInstanceOf(TextData
.class) && ( feature
.equals(element
.getFeature() )|| isImageGallery
) ){
299 existingTextData
.add(element
);
303 if (existingTextData
.size() > 1){
304 throw new DerivedUnitFacadeNotSupportedException("Specimen facade does not support more than one description text data of type " + feature
.getLabel());
306 }else if (existingTextData
.size() == 1){
307 return CdmBase
.deproxy(existingTextData
.iterator().next(), TextData
.class);
309 SpecimenDescription description
= descriptions
.iterator().next();
310 description
.addElement(textData
);
315 //************************** METHODS *****************************************
317 private TextData
getDerivedUnitImageGalleryTextData(boolean createIfNotExists
) throws DerivedUnitFacadeNotSupportedException
{
318 if (this.derivedUnitMediaTextData
== null && createIfNotExists
){
319 this.derivedUnitMediaTextData
= getImageGalleryTextData(derivedUnit
, "Specimen");
321 return this.derivedUnitMediaTextData
;
324 private TextData
getObservationImageGalleryTextData(boolean createIfNotExists
) throws DerivedUnitFacadeNotSupportedException
{
325 if (this.fieldObjectMediaTextData
== null && createIfNotExists
){
326 this.fieldObjectMediaTextData
= getImageGalleryTextData(fieldObservation
, "Field observation");
328 return this.fieldObjectMediaTextData
;
334 * @param derivationEvent2
336 * @throws DerivedUnitFacadeNotSupportedException
338 private Set
<FieldObservation
> getFieldObservationsOriginals(DerivationEvent derivationEvent
, Set
<SpecimenOrObservationBase
> recursionAvoidSet
) throws DerivedUnitFacadeNotSupportedException
{
339 if (recursionAvoidSet
== null){
340 recursionAvoidSet
= new HashSet
<SpecimenOrObservationBase
>();
342 Set
<FieldObservation
> result
= new HashSet
<FieldObservation
>();
343 Set
<SpecimenOrObservationBase
> originals
= derivationEvent
.getOriginals();
344 for (SpecimenOrObservationBase original
: originals
){
345 if (original
.isInstanceOf(FieldObservation
.class)){
346 result
.add(CdmBase
.deproxy(original
, FieldObservation
.class));
347 }else if (original
.isInstanceOf(DerivedUnitBase
.class)){
348 //if specimen has already been tested exclude it from further recursion
349 if (recursionAvoidSet
.contains(original
)){
352 DerivedUnitBase derivedUnit
= CdmBase
.deproxy(original
, DerivedUnitBase
.class);
353 DerivationEvent originalDerivation
= derivedUnit
.getDerivedFrom();
354 // Set<DerivationEvent> derivationEvents = original.getDerivationEvents();
355 // for (DerivationEvent originalDerivation : derivationEvents){
356 Set
<FieldObservation
> fieldObservations
= getFieldObservationsOriginals(originalDerivation
, recursionAvoidSet
);
357 result
.addAll(fieldObservations
);
360 throw new DerivedUnitFacadeNotSupportedException("Unhandled specimen or observation base type: " + original
.getClass().getName() );
367 //*********** MEDIA METHODS ******************************
370 // * Returns the media list for a specimen. Throws an exception if the existing specimen descriptions
371 // * are not supported by this facade.
372 // * @param specimen the specimen the media belongs to
373 // * @param specimenExceptionText text describing the specimen for exception messages
375 // * @throws DerivedUnitFacadeNotSupportedException
377 // private List<Media> getImageGalleryMedia(SpecimenOrObservationBase specimen, String specimenExceptionText) throws DerivedUnitFacadeNotSupportedException{
378 // List<Media> result;
379 // SpecimenDescription imageGallery = getImageGalleryWithSupportTest(specimen, specimenExceptionText, true);
380 // TextData textData = getImageTextDataWithSupportTest(imageGallery, specimenExceptionText);
381 // result = textData.getMedia();
386 * Returns the media list for a specimen. Throws an exception if the existing specimen descriptions
387 * are not supported by this facade.
388 * @param specimen the specimen the media belongs to
389 * @param specimenExceptionText text describing the specimen for exception messages
391 * @throws DerivedUnitFacadeNotSupportedException
393 private TextData
getImageGalleryTextData(SpecimenOrObservationBase specimen
, String specimenExceptionText
) throws DerivedUnitFacadeNotSupportedException
{
395 SpecimenDescription imageGallery
= getImageGalleryWithSupportTest(specimen
, specimenExceptionText
, true);
396 result
= getImageTextDataWithSupportTest(imageGallery
, specimenExceptionText
);
402 * Returns the image gallery of the according specimen. Throws an exception if the attached
403 * image gallerie(s) are not supported by this facade.
404 * If no image gallery exists a new one is created if <code>createNewIfNotExists</code> is true and
405 * if specimen is not <code>null</code>.
407 * @param specimenText
408 * @param createNewIfNotExists
410 * @throws DerivedUnitFacadeNotSupportedException
412 private SpecimenDescription
getImageGalleryWithSupportTest(SpecimenOrObservationBase
<?
> specimen
, String specimenText
, boolean createNewIfNotExists
) throws DerivedUnitFacadeNotSupportedException
{
413 if (specimen
== null){
416 SpecimenDescription imageGallery
;
417 if (hasMultipleImageGalleries(specimen
)){
418 throw new DerivedUnitFacadeNotSupportedException( specimenText
+ " must not have more than 1 image gallery");
420 imageGallery
= getImageGallery(specimen
, createNewIfNotExists
);
421 getImageTextDataWithSupportTest(imageGallery
, specimenText
);
427 * Returns the media holding text data element of the image gallery. Throws an exception if multiple
428 * such text data already exist.
429 * Creates a new text data if none exists and adds it to the image gallery.
430 * If image gallery is <code>null</code> nothing happens.
431 * @param imageGallery
434 * @throws DerivedUnitFacadeNotSupportedException
436 private TextData
getImageTextDataWithSupportTest(SpecimenDescription imageGallery
, String specimenText
) throws DerivedUnitFacadeNotSupportedException
{
437 if (imageGallery
== null){
440 TextData textData
= null;
441 for (DescriptionElementBase element
: imageGallery
.getElements()){
442 if (element
.isInstanceOf(TextData
.class) && element
.getFeature().equals(Feature
.IMAGE())){
443 if (textData
!= null){
444 throw new DerivedUnitFacadeNotSupportedException( specimenText
+ " must not have more than 1 image text data element in image gallery");
446 textData
= CdmBase
.deproxy(element
, TextData
.class);
449 if (textData
== null){
450 textData
= TextData
.NewInstance(Feature
.IMAGE());
451 imageGallery
.addElement(textData
);
457 * Checks, if a specimen belongs to more than one description that is an image gallery
461 private boolean hasMultipleImageGalleries(SpecimenOrObservationBase
<?
> derivedUnit
){
463 Set
<SpecimenDescription
> descriptions
= derivedUnit
.getSpecimenDescriptions();
464 for (SpecimenDescription description
: descriptions
){
465 if (description
.isImageGallery()){
474 * Returns the image gallery for a specimen. If there are multiple specimen descriptions
475 * marked as image galleries an arbitrary one is chosen.
476 * If no image gallery exists, a new one is created if <code>createNewIfNotExists</code>
477 * is <code>true</code>.<Br>
478 * If specimen is <code>null</code> a null pointer exception is thrown.
479 * @param createNewIfNotExists
482 private SpecimenDescription
getImageGallery(SpecimenOrObservationBase
<?
> specimen
, boolean createIfNotExists
) {
483 SpecimenDescription result
= null;
484 Set
<SpecimenDescription
> descriptions
= specimen
.getSpecimenDescriptions();
485 for (SpecimenDescription description
: descriptions
){
486 if (description
.isImageGallery()){
487 result
= description
;
491 if (result
== null && createIfNotExists
){
492 result
= SpecimenDescription
.NewInstance(specimen
);
493 result
.setImageGallery(true);
499 * Adds a media to the specimens image gallery. If media is <code>null</code> nothing happens.
502 * @return true if media is not null (as specified by {@link java.util.Collection#add(Object) Collection.add(E e)}
503 * @throws DerivedUnitFacadeNotSupportedException
505 private boolean addMedia(Media media
, SpecimenOrObservationBase
<?
> specimen
) throws DerivedUnitFacadeNotSupportedException
{
507 List
<Media
> mediaList
= getMedia(specimen
, true);
508 return mediaList
.add(media
);
515 * Removes a media from the specimens image gallery.
518 * @return true if an element was removed as a result of this call (as specified by {@link java.util.Collection#remove(Object) Collection.remove(E e)}
519 * @throws DerivedUnitFacadeNotSupportedException
521 private boolean removeMedia(Media media
, SpecimenOrObservationBase
<?
> specimen
) throws DerivedUnitFacadeNotSupportedException
{
522 List
<Media
> mediaList
= getMedia(specimen
, true);
523 return mediaList
== null ?
null : mediaList
.remove(media
);
526 private List
<Media
> getMedia(SpecimenOrObservationBase
<?
> specimen
, boolean createIfNotExists
) throws DerivedUnitFacadeNotSupportedException
{
527 TextData textData
= getMediaTextData(specimen
, createIfNotExists
);
528 return textData
== null ?
null : textData
.getMedia();
532 * Returns the one media list of a specimen which is part of the only image gallery that
533 * this specimen is part of.<BR>
534 * If these conditions are not hold an exception is thrwon.
537 * @throws DerivedUnitFacadeNotSupportedException
539 // private List<Media> getMedia(SpecimenOrObservationBase<?> specimen) throws DerivedUnitFacadeNotSupportedException {
540 // if (specimen == null){
543 // if (specimen == this.derivedUnit){
544 // return getDerivedUnitImageGalleryMedia();
545 // }else if (specimen == this.fieldObservation){
546 // return getObservationImageGalleryTextData();
548 // return getImageGalleryMedia(specimen, "Undefined specimen ");
553 * Returns the one media list of a specimen which is part of the only image gallery that
554 * this specimen is part of.<BR>
555 * If these conditions are not hold an exception is thrwon.
558 * @throws DerivedUnitFacadeNotSupportedException
560 private TextData
getMediaTextData(SpecimenOrObservationBase
<?
> specimen
, boolean createIfNotExists
) throws DerivedUnitFacadeNotSupportedException
{
561 if (specimen
== null){
564 if (specimen
== this.derivedUnit
){
565 return getDerivedUnitImageGalleryTextData(createIfNotExists
);
566 }else if (specimen
== this.fieldObservation
){
567 return getObservationImageGalleryTextData(createIfNotExists
);
569 return getImageGalleryTextData(specimen
, "Undefined specimen ");
574 //****************** GETTER / SETTER / ADDER / REMOVER ***********************/
576 // ****************** Gathering Event *********************************/
579 public void addCollectingArea(NamedArea area
) {
580 getGatheringEvent(true).addCollectingArea(area
);
582 public void addCollectingAreas(java
.util
.Collection
<NamedArea
> areas
) {
583 for (NamedArea area
: areas
){
584 getGatheringEvent(true).addCollectingArea(area
);
587 public Set
<NamedArea
> getCollectingAreas() {
588 return (hasGatheringEvent() ?
getGatheringEvent(true).getCollectingAreas() : null);
590 public void removeCollectingArea(NamedArea area
) {
591 if (hasGatheringEvent()){
592 getGatheringEvent(true).removeCollectingArea(area
);
597 /** meter above/below sea level of the surface
598 * @see #getAbsoluteElevationError()
599 * @see #getAbsoluteElevationRange()
601 public Integer
getAbsoluteElevation() {
602 return (hasGatheringEvent() ?
getGatheringEvent(true).getAbsoluteElevation() : null);
604 public void setAbsoluteElevation(Integer absoluteElevation
) {
605 getGatheringEvent(true).setAbsoluteElevation(absoluteElevation
);
608 //absolute elevation error
609 public Integer
getAbsoluteElevationError() {
610 return (hasGatheringEvent() ?
getGatheringEvent(true).getAbsoluteElevationError() : null);
612 public void setAbsoluteElevationError(Integer absoluteElevationError
) {
613 getGatheringEvent(true).setAbsoluteElevationError(absoluteElevationError
);
617 * @see #getAbsoluteElevation()
618 * @see #getAbsoluteElevationError()
619 * @see #setAbsoluteElevationRange(Integer, Integer)
620 * @see #getAbsoluteElevationMaximum()
622 public Integer
getAbsoluteElevationMinimum(){
623 if ( ! hasGatheringEvent() ){
626 Integer minimum
= getGatheringEvent(true).getAbsoluteElevation();
627 if (getGatheringEvent(true).getAbsoluteElevationError() != null){
628 minimum
= minimum
- getGatheringEvent(true).getAbsoluteElevationError();
633 * @see #getAbsoluteElevation()
634 * @see #getAbsoluteElevationError()
635 * @see #setAbsoluteElevationRange(Integer, Integer)
636 * @see #getAbsoluteElevationMinimum()
638 public Integer
getAbsoluteElevationMaximum(){
639 if ( ! hasGatheringEvent() ){
642 Integer maximum
= getGatheringEvent(true).getAbsoluteElevation();
643 if (getGatheringEvent(true).getAbsoluteElevationError() != null){
644 maximum
= maximum
+ getGatheringEvent(true).getAbsoluteElevationError();
651 * This method replaces absoluteElevation and absoulteElevationError by
652 * internally translating minimum and maximum values into
653 * average and error values. As all these values are integer based
654 * it is necessary that the distance is between minimum and maximum is <b>even</b>,
655 * otherwise we will get a rounding error resulting in a maximum that is increased
657 * @see #setAbsoluteElevation(Integer)
658 * @see #setAbsoluteElevationError(Integer)
659 * @param minimumElevation minimum of the range
660 * @param maximumElevation maximum of the range
662 public void setAbsoluteElevationRange(Integer minimumElevation
, Integer maximumElevation
){
663 if (minimumElevation
== null || maximumElevation
== null){
664 Integer elevation
= minimumElevation
;
666 if (minimumElevation
== null){
667 elevation
= maximumElevation
;
668 if (elevation
== null){
672 getGatheringEvent(true).setAbsoluteElevation(elevation
);
673 getGatheringEvent(true).setAbsoluteElevationError(error
);
675 if (! isEvenDistance(minimumElevation
, maximumElevation
) ){
676 throw new IllegalArgumentException("Distance between minimum and maximum elevation must be even but was " + Math
.abs(minimumElevation
- maximumElevation
));
678 Integer absoluteElevationError
= Math
.abs(maximumElevation
- minimumElevation
);
679 absoluteElevationError
= absoluteElevationError
/ 2;
680 Integer absoluteElevation
= minimumElevation
+ absoluteElevationError
;
681 getGatheringEvent(true).setAbsoluteElevation(absoluteElevation
);
682 getGatheringEvent(true).setAbsoluteElevationError(absoluteElevationError
);
687 * @param minimumElevation
688 * @param maximumElevation
691 private boolean isEvenDistance(Integer minimumElevation
, Integer maximumElevation
) {
692 Integer diff
= ( maximumElevation
- minimumElevation
);
693 Integer testDiff
= (diff
/2) *2 ;
694 return (testDiff
== diff
);
698 public AgentBase
getCollector() {
699 return (hasGatheringEvent() ?
getGatheringEvent(true).getCollector() : null);
701 public void setCollector(AgentBase collector
){
702 getGatheringEvent(true).setCollector(collector
);
706 public String
getCollectingMethod() {
707 return (hasGatheringEvent() ?
getGatheringEvent(true).getCollectingMethod() : null);
709 public void setCollectingMethod(String collectingMethod
) {
710 getGatheringEvent(true).setCollectingMethod(collectingMethod
);
714 public Integer
getDistanceToGround() {
715 return (hasGatheringEvent() ?
getGatheringEvent(true).getDistanceToGround() : null);
717 public void setDistanceToGround(Integer distanceToGround
) {
718 getGatheringEvent(true).setDistanceToGround(distanceToGround
);
721 //distance to water surface
722 public Integer
getDistanceToWaterSurface() {
723 return (hasGatheringEvent() ?
getGatheringEvent(true).getDistanceToWaterSurface() : null);
725 public void setDistanceToWaterSurface(Integer distanceToWaterSurface
) {
726 getGatheringEvent(true).setDistanceToWaterSurface(distanceToWaterSurface
);
730 public Point
getExactLocation() {
731 return (hasGatheringEvent() ?
getGatheringEvent(true).getExactLocation() : null );
735 * Returns a sexagesimal representation of the exact location (e.g. 12°59'N, 35°23E).
736 * If the exact location is <code>null</code> the empty string is returned.
737 * @param includeEmptySeconds
738 * @param includeReferenceSystem
741 public String
getExactLocationText(boolean includeEmptySeconds
, boolean includeReferenceSystem
){
742 return (this.getExactLocation() == null ?
"" : this.getExactLocation().toSexagesimalString(includeEmptySeconds
, includeReferenceSystem
));
744 public void setExactLocation(Point exactLocation
) {
745 getGatheringEvent(true).setExactLocation(exactLocation
);
747 public void setExactLocationByParsing(String longitudeToParse
, String latitudeToParse
, ReferenceSystem referenceSystem
, Integer errorRadius
) throws ParseException
{
748 Point point
= Point
.NewInstance(null, null, referenceSystem
, errorRadius
);
749 point
.setLongitudeByParsing(longitudeToParse
);
750 point
.setLatitudeByParsing(latitudeToParse
);
751 setExactLocation(point
);
754 //gathering event description
755 public String
getGatheringEventDescription() {
756 return (hasGatheringEvent() ?
getGatheringEvent(true).getDescription() : null);
758 public void setGatheringEventDescription(String description
) {
759 getGatheringEvent(true).setDescription(description
);
763 public TimePeriod
getGatheringPeriod() {
764 return (hasGatheringEvent() ?
getGatheringEvent(true).getTimeperiod() : null);
766 public void setGatheringPeriod(TimePeriod timeperiod
) {
767 getGatheringEvent(true).setTimeperiod(timeperiod
);
771 public LanguageString
getLocality(){
772 return (hasGatheringEvent() ?
getGatheringEvent(true).getLocality() : null);
774 public String
getLocalityText(){
775 LanguageString locality
= getLocality();
776 if(locality
!= null){
777 return locality
.getText();
781 public Language
getLocalityLanguage(){
782 LanguageString locality
= getLocality();
783 if(locality
!= null){
784 return locality
.getLanguage();
790 * Sets the locality string in the default language
793 public void setLocality(String locality
){
794 Language language
= Language
.DEFAULT();
795 setLocality(locality
, language
);
797 public void setLocality(String locality
, Language language
){
798 LanguageString langString
= LanguageString
.NewInstance(locality
, language
);
799 setLocality(langString
);
801 public void setLocality(LanguageString locality
){
802 getGatheringEvent(true).setLocality(locality
);
806 * The gathering event will be used for the field object instead of the old gathering event.<BR>
807 * <B>This method will override all gathering values (see below).</B>
808 * @see #getAbsoluteElevation()
809 * @see #getAbsoluteElevationError()
810 * @see #getDistanceToGround()
811 * @see #getDistanceToWaterSurface()
812 * @see #getExactLocation()
813 * @see #getGatheringEventDescription()
814 * @see #getGatheringPeriod()
815 * @see #getCollectingAreas()
816 * @see #getCollectingMethod()
817 * @see #getLocality()
818 * @see #getCollector()
819 * @param gatheringEvent
821 public void setGatheringEvent(GatheringEvent gatheringEvent
) {
822 getFieldObservation(true).setGatheringEvent(gatheringEvent
);
824 public boolean hasGatheringEvent(){
825 return (getGatheringEvent(false) != null);
827 public GatheringEvent
getGatheringEvent() {
828 return getGatheringEvent(false);
831 public GatheringEvent
getGatheringEvent(boolean createIfNotExists
) {
832 if (! hasFieldObservation() && ! createIfNotExists
){
835 if (createIfNotExists
&& getFieldObservation(true).getGatheringEvent() == null ){
836 GatheringEvent gatheringEvent
= GatheringEvent
.NewInstance();
837 getFieldObservation(true).setGatheringEvent(gatheringEvent
);
839 return getFieldObservation(true).getGatheringEvent();
842 // ****************** Field Object ************************************/
845 * Returns true if a field observation exists (even if all attributes are empty or <code>null<code>.
848 public boolean hasFieldObject(){
849 return this.fieldObservation
!= null;
853 public String
getEcology(){
854 return getEcology(Language
.DEFAULT());
856 public String
getEcology(Language language
){
857 LanguageString languageString
= getEcologyAll().get(language
);
858 return (languageString
== null ?
null : languageString
.getText());
860 // public String getEcologyPreferred(List<Language> languages){
861 // LanguageString languageString = getEcologyAll().getPreferredLanguageString(languages);
862 // return languageString.getText();
864 public Map
<Language
, LanguageString
> getEcologyAll(){
865 if (ecology
== null){
867 ecology
= initializeFieldObjectTextDataWithSupportTest(Feature
.ECOLOGY(), true, false);
868 } catch (DerivedUnitFacadeNotSupportedException e
) {
869 throw new IllegalStateException(notSupportMessage
, e
);
872 return ecology
.getMultilanguageText();
875 public void setEcology(String ecology
){
876 setEcology(ecology
, null);
878 public void setEcology(String ecologyText
, Language language
){
879 if (language
== null){
880 language
= Language
.DEFAULT();
882 if (ecology
== null){
884 ecology
= initializeFieldObjectTextDataWithSupportTest(Feature
.ECOLOGY(), true, false);
885 } catch (DerivedUnitFacadeNotSupportedException e
) {
886 throw new IllegalStateException(notSupportMessage
, e
);
889 if (ecologyText
== null){
890 ecology
.removeText(language
);
892 ecology
.putText(ecologyText
, language
);
895 public void removeEcology(Language language
){
896 setEcology(null, language
);
899 * Removes ecology for the default language
901 public void removeEcology(){
902 setEcology(null, null);
904 public void removeEcologyAll(){
910 public String
getPlantDescription(){
911 return getPlantDescription(null);
913 public String
getPlantDescription(Language language
){
914 if (language
== null){
915 language
= Language
.DEFAULT();
917 LanguageString languageString
= getPlantDescriptionAll().get(language
);
918 return (languageString
== null ?
null : languageString
.getText());
920 // public String getPlantDescriptionPreferred(List<Language> languages){
921 // LanguageString languageString = getPlantDescriptionAll().getPreferredLanguageString(languages);
922 // return languageString.getText();
924 public Map
<Language
, LanguageString
> getPlantDescriptionAll(){
925 if (plantDescription
== null){
927 plantDescription
= initializeFieldObjectTextDataWithSupportTest(Feature
.DESCRIPTION(), true, false);
928 } catch (DerivedUnitFacadeNotSupportedException e
) {
929 throw new IllegalStateException(notSupportMessage
, e
);
932 return plantDescription
.getMultilanguageText();
934 public void setPlantDescription(String plantDescription
){
935 setPlantDescription(plantDescription
, null);
937 public void setPlantDescription(String plantDescriptionText
, Language language
){
938 if (language
== null){
939 language
= Language
.DEFAULT();
941 if (plantDescription
== null){
943 plantDescription
= initializeFieldObjectTextDataWithSupportTest(Feature
.DESCRIPTION(), true, false);
944 } catch (DerivedUnitFacadeNotSupportedException e
) {
945 throw new IllegalStateException(notSupportMessage
, e
);
948 if (plantDescriptionText
== null){
949 plantDescription
.removeText(language
);
951 plantDescription
.putText(plantDescriptionText
, language
);
954 public void removePlantDescription(Language language
){
955 setPlantDescription(null, language
);
960 //field object definition
961 public void addFieldObjectDefinition(String text
, Language language
) {
962 getFieldObservation(true).addDefinition(text
, language
);
964 public Map
<Language
, LanguageString
> getFieldObjectDefinition() {
965 if (! hasFieldObservation()){
966 return new HashMap
<Language
, LanguageString
>();
968 return getFieldObservation(true).getDefinition();
971 public String
getFieldObjectDefinition(Language language
) {
972 Map
<Language
, LanguageString
> map
= getFieldObjectDefinition();
973 LanguageString languageString
= (map
== null?
null : map
.get(language
));
974 if (languageString
!= null){
975 return languageString
.getText();
980 public void removeFieldObjectDefinition(Language lang
) {
981 if (hasFieldObservation()){
982 getFieldObservation(true).removeDefinition(lang
);
988 public boolean addFieldObjectMedia(Media media
) {
990 return addMedia(media
, getFieldObservation(true));
991 } catch (DerivedUnitFacadeNotSupportedException e
) {
992 throw new IllegalStateException(notSupportMessage
, e
);
996 * Returns true, if an image gallery for the field object exists.<BR>
997 * Returns also <code>true</code> if the image gallery is empty.
1000 public boolean hasFieldObjectImageGallery(){
1001 if (! hasFieldObject()){
1004 return (getImageGallery(fieldObservation
, false) != null);
1009 * @param createIfNotExists
1012 public SpecimenDescription
getFieldObjectImageGallery(boolean createIfNotExists
){
1015 textData
= initializeFieldObjectTextDataWithSupportTest(Feature
.IMAGE(), createIfNotExists
, true);
1016 } catch (DerivedUnitFacadeNotSupportedException e
) {
1017 throw new IllegalStateException(notSupportMessage
, e
);
1019 if (textData
!= null){
1020 return CdmBase
.deproxy(textData
.getInDescription(), SpecimenDescription
.class);
1026 * Returns the media for the field object.<BR>
1029 public List
<Media
> getFieldObjectMedia() {
1031 List
<Media
> result
= getMedia(getFieldObservation(false), false);
1032 return result
== null ?
new ArrayList
<Media
>() : result
;
1033 } catch (DerivedUnitFacadeNotSupportedException e
) {
1034 throw new IllegalStateException(notSupportMessage
, e
);
1037 public boolean removeFieldObjectMedia(Media media
) {
1039 return removeMedia(media
, getFieldObservation(false));
1040 } catch (DerivedUnitFacadeNotSupportedException e
) {
1041 throw new IllegalStateException(notSupportMessage
, e
);
1046 public String
getFieldNumber() {
1047 if (! hasFieldObservation()){
1050 return getFieldObservation(true).getFieldNumber();
1053 public void setFieldNumber(String fieldNumber
) {
1054 getFieldObservation(true).setFieldNumber(fieldNumber
);
1059 public String
getFieldNotes() {
1060 if (! hasFieldObservation()){
1063 return getFieldObservation(true).getFieldNotes();
1066 public void setFieldNotes(String fieldNotes
) {
1067 getFieldObservation(true).setFieldNotes(fieldNotes
);
1072 public Integer
getIndividualCount() {
1073 return (hasFieldObservation()?
getFieldObservation(true).getIndividualCount() : null );
1075 public void setIndividualCount(Integer individualCount
) {
1076 getFieldObservation(true).setIndividualCount(individualCount
);
1080 public Stage
getLifeStage() {
1081 return (hasFieldObservation()?
getFieldObservation(true).getLifeStage() : null );
1083 public void setLifeStage(Stage lifeStage
) {
1084 getFieldObservation(true).setLifeStage(lifeStage
);
1088 public Sex
getSex() {
1089 return (hasFieldObservation()?
getFieldObservation(true).getSex() : null );
1091 public void setSex(Sex sex
) {
1092 getFieldObservation(true).setSex(sex
);
1097 public boolean hasFieldObservation(){
1098 return (getFieldObservation(false) != null);
1102 * Returns the field observation as an object.
1105 public FieldObservation
getFieldObservation(){
1106 return getFieldObservation(false);
1110 * Returns the field observation as an object.
1113 public FieldObservation
getFieldObservation(boolean createIfNotExists
){
1114 if (fieldObservation
== null && createIfNotExists
){
1115 fieldObservation
= FieldObservation
.NewInstance();
1116 fieldObservation
.addPropertyChangeListener(getNewEventPropagationListener());
1117 DerivationEvent derivationEvent
= getDerivationEvent(true);
1118 derivationEvent
.addOriginal(fieldObservation
);
1120 return this.fieldObservation
;
1127 //****************** Specimen **************************************************
1130 public void addDerivedUnitDefinition(String text
, Language language
) {
1131 derivedUnit
.addDefinition(text
, language
);
1133 public Map
<Language
, LanguageString
> getDerivedUnitDefinitions(){
1134 return this.derivedUnit
.getDefinition();
1136 public String
getDerivedUnitDefinition(Language language
) {
1137 Map
<Language
,LanguageString
> languageMap
= derivedUnit
.getDefinition();
1138 LanguageString languageString
= languageMap
.get(language
);
1139 if (languageString
!= null){
1140 return languageString
.getText();
1145 public void removeDerivedUnitDefinition(Language lang
) {
1146 derivedUnit
.removeDefinition(lang
);
1150 public void addDetermination(DeterminationEvent determination
) {
1151 derivedUnit
.addDetermination(determination
);
1153 public Set
<DeterminationEvent
> getDeterminations() {
1154 return derivedUnit
.getDeterminations();
1156 public void removeDetermination(DeterminationEvent determination
) {
1157 derivedUnit
.removeDetermination(determination
);
1161 public boolean addDerivedUnitMedia(Media media
) {
1163 return addMedia(media
, derivedUnit
);
1164 } catch (DerivedUnitFacadeNotSupportedException e
) {
1165 throw new IllegalStateException(notSupportMessage
, e
);
1169 * Returns true, if an image gallery exists for the specimen.<BR>
1170 * Returns also <code>true</code> if the image gallery is empty.
1172 public boolean hasDerivedUnitImageGallery(){
1173 return (getImageGallery(derivedUnit
, false) != null);
1176 public SpecimenDescription
getDerivedUnitImageGallery(boolean createIfNotExists
){
1179 textData
= inititialzeTextDataWithSupportTest(Feature
.IMAGE(), derivedUnit
, createIfNotExists
, true);
1180 } catch (DerivedUnitFacadeNotSupportedException e
) {
1181 throw new IllegalStateException(notSupportMessage
, e
);
1183 if (textData
!= null){
1184 return CdmBase
.deproxy(textData
.getInDescription(), SpecimenDescription
.class);
1191 * Returns the media for the specimen.<BR>
1194 public List
<Media
> getDerivedUnitMedia() {
1196 List
<Media
> result
= getMedia(derivedUnit
, false);
1197 return result
== null ?
new ArrayList
<Media
>() : result
;
1198 } catch (DerivedUnitFacadeNotSupportedException e
) {
1199 throw new IllegalStateException(notSupportMessage
, e
);
1202 public boolean removeDerivedUnitMedia(Media media
) {
1204 return removeMedia(media
, derivedUnit
);
1205 } catch (DerivedUnitFacadeNotSupportedException e
) {
1206 throw new IllegalStateException(notSupportMessage
, e
);
1212 public String
getAccessionNumber() {
1213 return derivedUnit
.getAccessionNumber();
1215 public void setAccessionNumber(String accessionNumber
) {
1216 derivedUnit
.setAccessionNumber(accessionNumber
);
1220 public String
getCatalogNumber() {
1221 return derivedUnit
.getCatalogNumber();
1223 public void setCatalogNumber(String catalogNumber
) {
1224 derivedUnit
.setCatalogNumber(catalogNumber
);
1227 //Preservation Method
1230 * Only supported by specimen and fossils
1231 * @see #DerivedUnitType
1234 public PreservationMethod
getPreservationMethod() throws MethodNotSupportedByDerivedUnitTypeException
{
1235 if (derivedUnit
.isInstanceOf(Specimen
.class)){
1236 return CdmBase
.deproxy(derivedUnit
, Specimen
.class).getPreservation();
1238 throw new MethodNotSupportedByDerivedUnitTypeException("A preservation method is only available in derived units of type 'Specimen' or 'Fossil'");
1242 * Only supported by specimen and fossils
1243 * @see #DerivedUnitType
1246 public void setPreservationMethod(PreservationMethod preservation
)throws MethodNotSupportedByDerivedUnitTypeException
{
1247 if (derivedUnit
.isInstanceOf(Specimen
.class)){
1248 CdmBase
.deproxy(derivedUnit
, Specimen
.class).setPreservation(preservation
);
1250 throw new MethodNotSupportedByDerivedUnitTypeException("A preservation method is only available in derived units of type 'Specimen' or 'Fossil'");
1256 public TaxonNameBase
getStoredUnder() {
1257 return derivedUnit
.getStoredUnder();
1259 public void setStoredUnder(TaxonNameBase storedUnder
) {
1260 derivedUnit
.setStoredUnder(storedUnder
);
1264 public String
getCollectorsNumber() {
1265 return derivedUnit
.getCollectorsNumber();
1267 public void setCollectorsNumber(String collectorsNumber
) {
1268 this.derivedUnit
.setCollectorsNumber(collectorsNumber
);
1272 public String
getTitleCache() {
1273 if (! derivedUnit
.isProtectedTitleCache()){
1274 //always compute title cache anew as long as there are no property change listeners on
1275 //field observation, gathering event etc
1276 derivedUnit
.setTitleCache(null, false);
1278 return this.derivedUnit
.getTitleCache();
1280 public void setTitleCache(String titleCache
, boolean isProtected
) {
1281 this.derivedUnit
.setTitleCache(titleCache
, isProtected
);
1286 * Returns the derived unit itself.
1287 * @return the derived unit
1289 public DerivedUnitBase
getDerivedUnit() {
1290 return this.derivedUnit
;
1293 private boolean hasDerivationEvent(){
1294 return getDerivationEvent() == null ?
false : true;
1296 private DerivationEvent
getDerivationEvent(){
1297 return getDerivationEvent(false);
1299 private DerivationEvent
getDerivationEvent(boolean createIfNotExists
){
1300 DerivationEvent result
= derivedUnit
.getDerivedFrom();
1301 if (result
== null){
1302 result
= DerivationEvent
.NewInstance();
1303 derivedUnit
.setDerivedFrom(result
);
1308 public String
getExsiccatum() {
1309 logger
.warn("Exsiccatum method not yet supported. Needs model change");
1313 public String
setExsiccatum() throws MethodNotSupportedException
{
1314 throw new MethodNotSupportedException("Exsiccatum method not yet supported. Needs model change");
1319 public void addSource(IdentifiableSource source
){
1320 this.derivedUnit
.addSource(source
);
1323 * Creates an orignal source, adds it to the specimen and returns it.
1325 * @param microReference
1326 * @param originalNameString
1329 public IdentifiableSource
addSource(ReferenceBase reference
, String microReference
, String originalNameString
){
1330 IdentifiableSource source
= IdentifiableSource
.NewInstance(reference
, microReference
);
1331 source
.setOriginalNameString(originalNameString
);
1332 derivedUnit
.addSource(source
);
1336 public Set
<IdentifiableSource
> getSources(){
1337 return derivedUnit
.getSources();
1340 public void removeSource(IdentifiableSource source
){
1341 this.derivedUnit
.removeSource(source
);
1346 * @return the collection
1348 public Collection
getCollection() {
1349 return derivedUnit
.getCollection();
1354 * @param collection the collection to set
1356 public void setCollection(Collection collection
) {
1357 derivedUnit
.setCollection(collection
);
1361 // ******************************* Events *********************************************
1366 private PropertyChangeListener
getNewEventPropagationListener() {
1367 PropertyChangeListener listener
= new PropertyChangeListener(){
1369 public void propertyChange(PropertyChangeEvent event
) {
1370 derivedUnit
.firePropertyChange(event
);
1377 // private void firePropertyChange(PropertyChangeEvent evt) {
1378 // propertyChangeSupport.firePropertyChange(evt);
1383 //**************** Other Collections ***************************************************
1386 * Creates a duplicate specimen which derives from the same derivation event
1387 * as the facade specimen and adds collection data to it (all data available in
1388 * DerivedUnitBase and Specimen. Data from SpecimenOrObservationBase and above
1389 * are not yet shared at the moment.
1391 * @param catalogNumber
1392 * @param accessionNumber
1393 * @param collectorsNumber
1394 * @param storedUnder
1395 * @param preservation
1398 public Specimen
addDuplicate(Collection collection
, String catalogNumber
, String accessionNumber
,
1399 String collectorsNumber
, TaxonNameBase storedUnder
, PreservationMethod preservation
){
1400 Specimen duplicate
= Specimen
.NewInstance();
1401 duplicate
.setDerivedFrom(getDerivationEvent(true));
1402 duplicate
.setCollection(collection
);
1403 duplicate
.setCatalogNumber(catalogNumber
);
1404 duplicate
.setAccessionNumber(accessionNumber
);
1405 duplicate
.setCollectorsNumber(collectorsNumber
);
1406 duplicate
.setStoredUnder(storedUnder
);
1407 duplicate
.setPreservation(preservation
);
1411 public void addDuplicate(DerivedUnitBase duplicateSpecimen
){
1412 //TODO check derivedUnitType
1413 getDerivationEvent(true).addDerivative(duplicateSpecimen
);
1415 public Set
<Specimen
> getDuplicates(){
1416 Set
<Specimen
> result
= new HashSet
<Specimen
>();
1417 if (hasDerivationEvent()){
1418 for (DerivedUnitBase derivedUnit
: getDerivationEvent(true).getDerivatives()){
1419 if (derivedUnit
.isInstanceOf(Specimen
.class) && ! derivedUnit
.equals(this.derivedUnit
)){
1420 result
.add(CdmBase
.deproxy(derivedUnit
, Specimen
.class));
1426 public void removeDuplicate(Specimen duplicateSpecimen
){
1427 if (hasDerivationEvent()){
1428 getDerivationEvent(true).removeDerivative(duplicateSpecimen
);