Project

General

Profile

Download (40.4 KB) Statistics
| Branch: | Tag: | Revision:
1
/**
2
 * Copyright (C) 2009 EDIT
3
 * European Distributed Institute of Taxonomy
4
 * http://www.e-taxonomy.eu
5
 *
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.
8
 */
9

    
10
package eu.etaxonomy.cdm.io.markup;
11

    
12
import java.util.ArrayList;
13
import java.util.List;
14
import java.util.Map;
15
import java.util.Set;
16
import java.util.UUID;
17
import java.util.regex.Matcher;
18
import java.util.regex.Pattern;
19

    
20
import javax.xml.stream.XMLEventReader;
21
import javax.xml.stream.XMLStreamException;
22
import javax.xml.stream.events.Attribute;
23
import javax.xml.stream.events.XMLEvent;
24

    
25
import org.apache.commons.lang.StringUtils;
26
import org.apache.log4j.Logger;
27

    
28
import eu.etaxonomy.cdm.api.facade.DerivedUnitFacade;
29
import eu.etaxonomy.cdm.api.facade.DerivedUnitFacadeCacheStrategy;
30
import eu.etaxonomy.cdm.common.CdmUtils;
31
import eu.etaxonomy.cdm.model.agent.TeamOrPersonBase;
32
import eu.etaxonomy.cdm.model.common.Annotation;
33
import eu.etaxonomy.cdm.model.common.AnnotationType;
34
import eu.etaxonomy.cdm.model.common.CdmBase;
35
import eu.etaxonomy.cdm.model.common.DefinedTerm;
36
import eu.etaxonomy.cdm.model.common.Language;
37
import eu.etaxonomy.cdm.model.common.Marker;
38
import eu.etaxonomy.cdm.model.common.MarkerType;
39
import eu.etaxonomy.cdm.model.common.TimePeriod;
40
import eu.etaxonomy.cdm.model.description.DescriptionElementBase;
41
import eu.etaxonomy.cdm.model.description.Feature;
42
import eu.etaxonomy.cdm.model.description.IndividualsAssociation;
43
import eu.etaxonomy.cdm.model.description.TaxonDescription;
44
import eu.etaxonomy.cdm.model.location.Country;
45
import eu.etaxonomy.cdm.model.location.NamedArea;
46
import eu.etaxonomy.cdm.model.location.NamedAreaLevel;
47
import eu.etaxonomy.cdm.model.name.HomotypicalGroup;
48
import eu.etaxonomy.cdm.model.name.NonViralName;
49
import eu.etaxonomy.cdm.model.name.Rank;
50
import eu.etaxonomy.cdm.model.name.SpecimenTypeDesignation;
51
import eu.etaxonomy.cdm.model.name.SpecimenTypeDesignationStatus;
52
import eu.etaxonomy.cdm.model.name.TaxonNameBase;
53
import eu.etaxonomy.cdm.model.occurrence.Collection;
54
import eu.etaxonomy.cdm.model.occurrence.DerivedUnit;
55
import eu.etaxonomy.cdm.model.occurrence.DeterminationEvent;
56
import eu.etaxonomy.cdm.model.occurrence.FieldUnit;
57
import eu.etaxonomy.cdm.model.occurrence.SpecimenOrObservationBase;
58
import eu.etaxonomy.cdm.model.occurrence.SpecimenOrObservationType;
59
import eu.etaxonomy.cdm.model.reference.Reference;
60
import eu.etaxonomy.cdm.model.reference.ReferenceFactory;
61
import eu.etaxonomy.cdm.strategy.exceptions.UnknownCdmTypeException;
62
import eu.etaxonomy.cdm.strategy.parser.SpecimenTypeParser;
63
import eu.etaxonomy.cdm.strategy.parser.SpecimenTypeParser.TypeInfo;
64
import eu.etaxonomy.cdm.strategy.parser.TimePeriodParser;
65

    
66
/**
67
 * @author a.mueller
68
 * @created 30.05.2012
69
 *
70
 */
71
public class MarkupSpecimenImport extends MarkupImportBase  {
72
	@SuppressWarnings("unused")
73
	private static final Logger logger = Logger.getLogger(MarkupSpecimenImport.class);
74

    
75
	private static final String ALTERNATIVE_COLLECTION_TYPE_STATUS = "alternativeCollectionTypeStatus";
76
	private static final String ALTERNATIVE_COLLECTOR = "alternativeCollector";
77
	private static final String ALTERNATIVE_FIELD_NUM = "alternativeFieldNum";
78
	private static final String COLLECTOR = "collector";
79
	private static final String COLLECTION = "collection";
80
	private static final String COLLECTION_AND_TYPE = "collectionAndType";
81
	private static final String COLLECTION_TYPE_STATUS = "collectionTypeStatus";
82
	private static final String DAY = "day";
83
	private static final String DESTROYED = "destroyed";
84
	private static final String FIELD_NUM = "fieldNum";
85
	private static final String FULL_TYPE = "fullType";
86
	private static final String FULL_DATE = "fullDate";
87
	private static final String GATHERING_NOTES = "gatheringNotes";
88
	private static final String LOST = "lost";
89
	private static final String MONTH = "month";
90
	private static final String SUB_GATHERING = "subGathering";
91
	private static final String NOT_FOUND = "notFound";
92
	private static final String NOT_SEEN = "notSeen";
93
	private static final String ORIGINAL_DETERMINATION = "originalDetermination";
94

    
95
	private static final String UNKNOWN = "unknown";
96
	private static final String YEAR = "year";
97

    
98

    
99

    
100
	public MarkupSpecimenImport(MarkupDocumentImport docImport) {
101
		super(docImport);
102
	}
103

    
104

    
105
	public void handleSpecimenType(MarkupImportState state, XMLEventReader reader, XMLEvent parentEvent,
106
				HomotypicalGroup homotypicalGroup) throws XMLStreamException {
107

    
108
		// attributes
109
		Map<String, Attribute> attributes = getAttributes(parentEvent);
110
		String typeStatus = getAndRemoveAttributeValue(attributes, TYPE_STATUS);
111
		String notSeen = getAndRemoveAttributeValue(attributes, NOT_SEEN);
112
		String unknown = getAndRemoveAttributeValue(attributes, UNKNOWN);
113
		String notFound = getAndRemoveAttributeValue(attributes, NOT_FOUND);
114
		String destroyed = getAndRemoveAttributeValue(attributes, DESTROYED);
115
		String lost = getAndRemoveAttributeValue(attributes, LOST);
116
		checkNoAttributes(attributes, parentEvent);
117
		if (StringUtils.isNotEmpty(typeStatus)) {
118
			// TODO
119
			// currently not needed
120
			fireWarningEvent("Type status not yet used", parentEvent, 4);
121
		} else if (StringUtils.isNotEmpty(notSeen)) {
122
			handleNotYetImplementedAttribute(attributes, NOT_SEEN);
123
		} else if (StringUtils.isNotEmpty(unknown)) {
124
			handleNotYetImplementedAttribute(attributes, UNKNOWN);
125
		} else if (StringUtils.isNotEmpty(notFound)) {
126
			handleNotYetImplementedAttribute(attributes, NOT_FOUND);
127
		} else if (StringUtils.isNotEmpty(destroyed)) {
128
			handleNotYetImplementedAttribute(attributes, DESTROYED);
129
		} else if (StringUtils.isNotEmpty(lost)) {
130
			handleNotYetImplementedAttribute(attributes, LOST);
131
		}
132

    
133
		NonViralName<?> firstName = null;
134
		Set<TaxonNameBase> names = homotypicalGroup.getTypifiedNames();
135
		if (names.isEmpty()) {
136
			String message = "There is no name in a homotypical group. Can't create the specimen type";
137
			fireWarningEvent(message, parentEvent, 8);
138
		} else {
139
			firstName = CdmBase.deproxy(names.iterator().next(),NonViralName.class);
140
		}
141

    
142
		DerivedUnitFacade facade = DerivedUnitFacade.NewInstance(SpecimenOrObservationType.PreservedSpecimen);
143
		String text = "";
144
		state.resetCollectionAndType();
145
		state.setSpecimenType(true);
146
		boolean isFullType = false;
147
		// elements
148
		while (reader.hasNext()) {
149
			XMLEvent next = readNoWhitespace(reader);
150
			if (isMyEndingElement(next, parentEvent)) {
151
				if (! isFullType){
152
					makeSpecimenType(state, facade, text, state.getCollectionAndType(), firstName, parentEvent);
153
				}
154
				state.setSpecimenType(false);
155
				state.resetCollectionAndType();
156
				return;
157
			} else if (isStartingElement(next, FULL_TYPE)) {
158
				handleAmbigousManually(state, reader, next.asStartElement());
159
				isFullType = true;
160
			} else if (isStartingElement(next, TYPE_STATUS)) {
161
				handleNotYetImplementedElement(next);
162
			} else if (isStartingElement(next, GATHERING)) {
163
				handleGathering(state, reader, next, facade);
164
			} else if (isStartingElement(next, ORIGINAL_DETERMINATION)) {
165
				handleNotYetImplementedElement(next);
166
			} else if (isStartingElement(next, SPECIMEN_TYPE)) {
167
				handleNotYetImplementedElement(next);
168
			} else if (isStartingElement(next, COLLECTION_AND_TYPE)) {
169
				String colAndType = getCData(state, reader, next, true);
170
				state.addCollectionAndType(colAndType);
171
			} else if (isStartingElement(next, CITATION)) {
172
				handleNotYetImplementedElement(next);
173
			} else if (isStartingElement(next, NOTES)) {
174
				handleNotYetImplementedElement(next);
175
			} else if (isStartingElement(next, ANNOTATION)) {
176
				handleNotYetImplementedElement(next);
177
			} else if (next.isCharacters()) {
178
				text += next.asCharacters().getData();
179
			} else {
180
				handleUnexpectedElement(next);
181
			}
182
		}
183
		throw new IllegalStateException("Specimen type has no closing tag");
184
	}
185

    
186

    
187

    
188
	private void makeSpecimenType(MarkupImportState state, DerivedUnitFacade facade, String text, String collectionAndType,
189
			NonViralName<?> name, XMLEvent parentEvent) {
190
		text = text.trim();
191
		if (isPunctuation(text)){
192
			//do nothing
193
		}else{
194
			String message = "Text '%s' not handled for <SpecimenType>";
195
			this.fireWarningEvent(String.format(message, text), parentEvent, 4);
196
		}
197

    
198
		if (makeFotgSpecimenType(state, collectionAndType, facade, name, parentEvent) || state.getConfig().isUseFotGSpecimenTypeCollectionAndTypeOnly()){
199
			return;
200
		}else{
201
			// remove brackets
202
			if (collectionAndType.matches("^\\(.*\\)\\.?$")) {
203
				collectionAndType = collectionAndType.replaceAll("\\.$", "");
204
				collectionAndType = collectionAndType.substring(1, collectionAndType.length() - 1);
205
			}
206

    
207
			String[] split = collectionAndType.split("[;,]");
208
			for (String str : split) {
209
				str = str.trim();
210
				boolean addToAllNamesInGroup = true;
211
				TypeInfo typeInfo = makeSpecimenTypeTypeInfo(str, parentEvent);
212
				SpecimenTypeDesignationStatus typeStatus = typeInfo.status;
213
				Collection collection = this.getCollection(state, typeInfo.collectionString);
214

    
215
				// TODO improve cache strategy handling
216
				DerivedUnit typeSpecimen = facade.addDuplicate(collection, null, null, null, null);
217
				typeSpecimen.setCacheStrategy(new DerivedUnitFacadeCacheStrategy());
218
				name.addSpecimenTypeDesignation(typeSpecimen, typeStatus, null, null, null, false, addToAllNamesInGroup);
219
			}
220
		}
221

    
222
	}
223

    
224

    
225
	private Pattern fotgTypePattern = null;
226
	/**
227
	 * Implemented for Flora of the Guyanas this may include duplicated code from similar places
228
	 * @param state
229
	 * @param collectionAndTypeOrig
230
	 * @param facade
231
	 * @param name
232
	 * @param parentEvent
233
	 * @return
234
	 */
235
	private boolean makeFotgSpecimenType(MarkupImportState state, final String collectionAndTypeOrig, DerivedUnitFacade facade, NonViralName<?> name, XMLEvent parentEvent) {
236
		String collectionAndType = collectionAndTypeOrig;
237

    
238
		String notDesignatedRE = "not\\s+designated";
239
		String designatedByRE = "\\s*\\(((designated\\s+by\\s+|according\\s+to\\s+)[^\\)]+|here\\s+designated)\\)";
240
		String typesRE = "(holotype|isotypes?|neotype|isoneotype|syntype|lectotype|isolectotypes?|typ\\.\\scons\\.,?)";
241
		String collectionRE = "[A-Z\\-]{1,5}!?";
242
		String collectionsRE = String.format("%s(,\\s+%s)*",collectionRE, collectionRE);
243
		String addInfoRE = "(not\\s+seen|(presumed\\s+)?destroyed)";
244
		String singleTypeTypeRE = String.format("(%s\\s)?%s(,\\s+%s)*", typesRE, collectionsRE, addInfoRE);
245
		String allTypesRE = String.format("(\\(not\\s+seen\\)|\\(%s([,;]\\s%s)?\\))", singleTypeTypeRE, singleTypeTypeRE);
246
		String designatedRE = String.format("%s(%s)?", allTypesRE, designatedByRE);
247
		if (fotgTypePattern == null){
248

    
249
			String pattern = String.format("(%s|%s)", notDesignatedRE, designatedRE );
250
			fotgTypePattern = Pattern.compile(pattern);
251
		}
252
		Matcher matcher = fotgTypePattern.matcher(collectionAndType);
253

    
254
		if (matcher.matches()){
255
			if (collectionAndType.matches(notDesignatedRE)){
256
				SpecimenTypeDesignation desig = SpecimenTypeDesignation.NewInstance();
257
				desig.setNotDesignated(true);
258
//				name.addSpecimenTypeDesignation(typeSpecimen, status, citation, citationMicroReference, originalNameString, isNotDesignated, addToAllHomotypicNames)
259
				name.addTypeDesignation(desig, true);
260
			}else if(collectionAndType.matches(designatedRE)){
261
				String designatedBy = null;
262
				Matcher desigMatcher = Pattern.compile(designatedByRE).matcher(collectionAndType);
263
				boolean hasDesignatedBy = desigMatcher.find();
264
				if (hasDesignatedBy){
265
					designatedBy = desigMatcher.group(0);
266
					collectionAndType = collectionAndType.replace(designatedBy, "");
267
				}
268

    
269
				//remove brackets
270
				collectionAndType = collectionAndType.substring(1, collectionAndType.length() -1);
271
				List<String> singleTypes = new ArrayList<String>();
272
				Pattern singleTypePattern = Pattern.compile("^" + singleTypeTypeRE);
273
				matcher = singleTypePattern.matcher(collectionAndType);
274
				while (matcher.find()){
275
					String match = matcher.group(0);
276
					singleTypes.add(match);
277
					collectionAndType = collectionAndType.substring(match.length());
278
					if (!collectionAndType.isEmpty()){
279
						collectionAndType = collectionAndType.substring(1).trim();
280
					}else{
281
						break;
282
					}
283
					matcher = singleTypePattern.matcher(collectionAndType);
284
				}
285

    
286
				List<SpecimenTypeDesignation> designations = new ArrayList<SpecimenTypeDesignation>();
287

    
288
				//single types
289
				for (String singleTypeOrig : singleTypes){
290
					String singleType = singleTypeOrig;
291
					//type
292
					Pattern typePattern = Pattern.compile("^" + typesRE);
293
					matcher = typePattern.matcher(singleType);
294
					SpecimenTypeDesignationStatus typeStatus = null;
295
					if (matcher.find()){
296
						String typeStr = matcher.group(0);
297
						singleType = singleType.substring(typeStr.length()).trim();
298
						try {
299
							typeStatus = SpecimenTypeParser.parseSpecimenTypeStatus(typeStr);
300
						} catch (UnknownCdmTypeException e) {
301
							fireWarningEvent("specimen type not recognized. Use generic type instead", parentEvent, 4);
302
							typeStatus = SpecimenTypeDesignationStatus.TYPE();
303
							//TODO use also type info from state
304
						}
305
					}else{
306
						typeStatus = SpecimenTypeDesignationStatus.TYPE();
307
						//TODO use also type info from state
308
					}
309

    
310

    
311
					//collection
312
					Pattern collectionPattern = Pattern.compile("^" + collectionsRE);
313
					matcher = collectionPattern.matcher(singleType);
314
					String[] collectionStrings = new String[0];
315
					if (matcher.find()){
316
						String collectionStr = matcher.group(0);
317
						singleType = singleType.substring(collectionStr.length());
318
						collectionStr = collectionStr.replace("(", "").replace(")", "").replaceAll("\\s", "");
319
						collectionStrings = collectionStr.split(",");
320
					}
321

    
322
					//addInfo
323
					if (!singleType.isEmpty() && singleType.startsWith(", ")){
324
						singleType = singleType.substring(2);
325
					}
326

    
327
					boolean notSeen = false;
328
					if (singleType.equals("not seen")){
329
						singleType = singleType.replace("not seen", "");
330
						notSeen = true;
331
					}
332
					if (singleType.startsWith("not seen, ")){
333
						singleType = singleType.replace("not seen, ", "");
334
						notSeen = true;
335
					}
336
					boolean destroyed = false;
337
					if (singleType.equals("destroyed")){
338
						destroyed = true;
339
						singleType = singleType.replace("destroyed", "");
340
					}
341
					boolean presumedDestroyed = false;
342
					if (singleType.equals("presumed destroyed")){
343
						presumedDestroyed = true;
344
						singleType = singleType.replace("presumed destroyed", "");
345
					}
346
					boolean hasAddInfo = notSeen || destroyed || presumedDestroyed;
347

    
348

    
349
					if (!singleType.isEmpty()){
350
						String message = "SingleType was not fully read. Remaining: " + singleType + ". Original singleType was: " + singleTypeOrig;
351
						fireWarningEvent(message, parentEvent, 6);
352
						System.out.println(message);
353
					}
354

    
355
					if (collectionStrings.length > 0){
356
						boolean isFirst = true;
357
						for (String collStr : collectionStrings){
358
							Collection collection = getCollection(state, collStr);
359
							DerivedUnit unit = isFirst ? facade.innerDerivedUnit()
360
									: facade.addDuplicate(collection, null, null, null, null);
361
							SpecimenTypeDesignation desig = SpecimenTypeDesignation.NewInstance();
362
							designations.add(desig);
363
							desig.setTypeSpecimen(unit);
364
							desig.setTypeStatus(typeStatus);
365
							handleSpecimenTypeAddInfo(state, notSeen, destroyed,
366
									presumedDestroyed, desig);
367
							name.addTypeDesignation(desig, true);
368
							isFirst = false;
369
						}
370
					}else if (hasAddInfo){  //handle addInfo if no collection data available
371
						SpecimenTypeDesignation desig = SpecimenTypeDesignation.NewInstance();
372
						designations.add(desig);
373
						desig.setTypeStatus(typeStatus);
374
						handleSpecimenTypeAddInfo(state, notSeen, destroyed,
375
								presumedDestroyed, desig);
376
						name.addTypeDesignation(desig, true);
377
					}else{
378
						fireWarningEvent("No type designation could be created as collection info was not recognized", parentEvent, 4);
379
					}
380
				}
381

    
382
				if (designatedBy != null){
383
					if (designations.size() != 1){
384
						fireWarningEvent("Size of type designations is not exactly 1, which is expected for 'designated by'", parentEvent, 2);
385
					}
386
					designatedBy = designatedBy.trim();
387
					if (designatedBy.startsWith("(") && designatedBy.endsWith(")") ){
388
						designatedBy = designatedBy.substring(1, designatedBy.length() - 1);
389
					}
390

    
391
					for (SpecimenTypeDesignation desig : designations){
392
						if (designatedBy.startsWith("designated by")){
393
							String titleCache = designatedBy.replace("designated by", "").trim();
394
							Reference reference = ReferenceFactory.newGeneric();
395
							reference.setTitleCache(titleCache, true);
396
							desig.setCitation(reference);
397
							//in future we could also try to parse it automatically
398
							fireWarningEvent("MANUALLY: Designated by should be parsed manually: " + titleCache, parentEvent, 1);
399
						}else if (designatedBy.equals("designated here")){
400
							Reference ref = state.getConfig().getSourceReference();
401
							desig.setCitation(ref);
402
							fireWarningEvent("MANUALLY: Microcitation should be added to 'designated here", parentEvent, 1);
403
						}else if (designatedBy.startsWith("according to")){
404
							String annotationStr = designatedBy.replace("according to", "").trim();
405
							Annotation annotation = Annotation.NewInstance(annotationStr, AnnotationType.EDITORIAL(), Language.ENGLISH());
406
							desig.addAnnotation(annotation);
407
						}else{
408
							fireWarningEvent("Designated by does not match known pattern: " + designatedBy, parentEvent, 6);
409
						}
410
					}
411
				}
412
			}else{
413
				fireWarningEvent("CollectionAndType unexpectedly not matching: " + collectionAndTypeOrig, parentEvent, 6);
414
			}
415
			return true;
416
		}else{
417
			if (state.getConfig().isUseFotGSpecimenTypeCollectionAndTypeOnly()){
418
				fireWarningEvent("NO MATCH: " + collectionAndTypeOrig, parentEvent, 4);
419
			}
420
			return false;
421
		}
422

    
423
//		// remove brackets
424
//		if (collectionAndType.matches("^\\(.*\\)\\.?$")) {
425
//			collectionAndType = collectionAndType.replaceAll("\\.$", "");
426
//			collectionAndType = collectionAndType.substring(1, collectionAndType.length() - 1);
427
//		}
428
//
429
//		String[] split = collectionAndType.split("[;,]");
430
//		for (String str : split) {
431
//			str = str.trim();
432
//			boolean addToAllNamesInGroup = true;
433
//			TypeInfo typeInfo = makeSpecimenTypeTypeInfo(str, parentEvent);
434
//			SpecimenTypeDesignationStatus typeStatus = typeInfo.status;
435
//			Collection collection = this.getCollection(state, typeInfo.collectionString);
436
//
437
//			// TODO improve cache strategy handling
438
//			DerivedUnit typeSpecimen = facade.addDuplicate(collection, null, null, null, null);
439
//			typeSpecimen.setCacheStrategy(new DerivedUnitFacadeCacheStrategy());
440
//			name.addSpecimenTypeDesignation(typeSpecimen, typeStatus, null, null, null, false, addToAllNamesInGroup);
441
//		}
442
	}
443

    
444

    
445
	/**
446
	 * @param notSeen
447
	 * @param destroyed
448
	 * @param presumedDestroyed
449
	 * @param desig
450
	 */
451
	private void handleSpecimenTypeAddInfo(MarkupImportState state, boolean notSeen, boolean destroyed,
452
			boolean presumedDestroyed, SpecimenTypeDesignation desig) {
453
		if (notSeen){
454
			UUID uuidNotSeenMarker = MarkupTransformer.uuidNotSeen;
455
			MarkerType notSeenMarkerType = getMarkerType(state, uuidNotSeenMarker, "Not seen", "Not seen", null, null);
456
			Marker marker = Marker.NewInstance(notSeenMarkerType, true);
457
			desig.addMarker(marker);
458
			fireWarningEvent("not seen not yet implemented", "handleSpecimenTypeAddInfo", 4);
459
		}
460
		if (destroyed){
461
			UUID uuidDestroyedMarker = MarkupTransformer.uuidDestroyed;
462
			MarkerType destroyedMarkerType = getMarkerType(state, uuidDestroyedMarker, "Destroyed", "Destroyed", null, null);
463
			Marker marker = Marker.NewInstance(destroyedMarkerType, true);
464
			desig.addMarker(marker);
465
			fireWarningEvent("'destroyed' not yet fully implemented", "handleSpecimenTypeAddInfo", 4);
466
		}
467
		if (presumedDestroyed){
468
			Annotation annotation = Annotation.NewInstance("presumably destroyed", Language.ENGLISH());
469
			annotation.setAnnotationType(AnnotationType.EDITORIAL());
470
			desig.addAnnotation(annotation);
471
		}
472
	}
473

    
474

    
475
	private TypeInfo makeSpecimenTypeTypeInfo(String originalString, XMLEvent event) {
476
		TypeInfo result = new TypeInfo();
477
		String[] split = originalString.split("\\s+");
478
		if ("not designated".equals(originalString)){
479
			result.notDesignated = true;
480
			return result;
481
		}
482

    
483
		for (String str : split) {
484
			if (str.matches(SpecimenTypeParser.typeTypePattern)) {
485
				SpecimenTypeDesignationStatus status;
486
				try {
487
					status = SpecimenTypeParser.parseSpecimenTypeStatus(str);
488
				} catch (UnknownCdmTypeException e) {
489
					String message = "Specimen type status '%s' not recognized by parser";
490
					fireWarningEvent(String.format(message, str), event, 4);
491
					status = null;
492
				}
493
				result.status = status;
494
			} else if (str.matches(SpecimenTypeParser.collectionPattern)) {
495
				result.collectionString = str;
496
			} else {
497
				String message = "Type part '%s' could not be recognized";
498
				fireWarningEvent(String.format(message, str), event, 2);
499
			}
500
		}
501

    
502
		return result;
503
	}
504

    
505

    
506
	private void handleGathering(MarkupImportState state, XMLEventReader readerOrig, XMLEvent parentEvent , DerivedUnitFacade facade) throws XMLStreamException {
507
		checkNoAttributes(parentEvent);
508
		boolean hasCollector = false;
509
		boolean hasFieldNum = false;
510

    
511
		LookAheadEventReader reader = new LookAheadEventReader(parentEvent.asStartElement(), readerOrig);
512

    
513
		// elements
514
		while (reader.hasNext()) {
515
			XMLEvent next = readNoWhitespace(reader);
516
			if (isMyEndingElement(next, parentEvent)) {
517
				if (! hasCollector){
518
					if (state.getCurrentCollector() == null){
519
						checkMandatoryElement(hasCollector,parentEvent.asStartElement(), COLLECTOR);
520
					}else{
521
						facade.setCollector(state.getCurrentCollector());
522
					}
523
				}
524
				checkMandatoryElement(hasFieldNum,parentEvent.asStartElement(), FIELD_NUM);
525
				return;
526
			}else if (isStartingElement(next, COLLECTOR)) {
527
				hasCollector = true;
528
				String collectorStr = getCData(state, reader, next);
529
				TeamOrPersonBase<?> collector = createCollector(collectorStr);
530
				facade.setCollector(collector);
531
				state.setCurrentCollector(collector);
532
			} else if (isStartingElement(next, ALTERNATIVE_COLLECTOR)) {
533
				handleNotYetImplementedElement(next);
534
			} else if (isStartingElement(next, FIELD_NUM)) {
535
				hasFieldNum = true;
536
				String fieldNumStr = getCData(state, reader, next);
537
				facade.setFieldNumber(fieldNumStr);
538
			} else if (isStartingElement(next, ALTERNATIVE_FIELD_NUM)) {
539
				handleAlternativeFieldNumber(state, reader, next, facade.innerFieldUnit());
540
			} else if (isStartingElement(next, COLLECTION_TYPE_STATUS)) {
541
				handleNotYetImplementedElement(next);
542
			} else if (isStartingElement(next, COLLECTION_AND_TYPE)) {
543
				handleGatheringCollectionAndType(state, reader, next, facade);
544
			} else if (isStartingElement(next, ALTERNATIVE_COLLECTION_TYPE_STATUS)) {
545
				handleNotYetImplementedElement(next);
546
			} else if (isStartingElement(next, SUB_GATHERING)) {
547
				handleNotYetImplementedElement(next);
548
			} else if (isStartingElement(next, COLLECTION)) {
549
				handleNotYetImplementedElement(next);
550
			} else if (isStartingElement(next, LOCALITY)) {
551
				handleLocality(state, reader, next, facade);
552
			} else if (isStartingElement(next, FULL_NAME)) {
553
				Rank defaultRank = Rank.SPECIES(); // can be any
554
				NonViralName<?> name = createNameByCode(state, defaultRank);
555
				handleFullName(state, reader, name, next);
556
				DeterminationEvent.NewInstance(name, facade.innerDerivedUnit() != null ? facade.innerDerivedUnit() : facade.innerFieldUnit());
557
			} else if (isStartingElement(next, DATES)) {
558
				TimePeriod timePeriod = handleDates(state, reader, next);
559
				facade.setGatheringPeriod(timePeriod);
560
			} else if (isStartingElement(next, GATHERING_NOTES)) {
561
				handleAmbigousManually(state, reader, next.asStartElement());
562
			} else if (isStartingElement(next, NOTES)) {
563
				handleNotYetImplementedElement(next);
564
			}else if (next.isCharacters()) {
565
				String text = next.asCharacters().getData().trim();
566
				if (isPunctuation(text)){
567
					//do nothing
568
				}else if (state.isSpecimenType() && charIsSimpleType(text) ){
569
						//do nothing
570
				}else if ( (text.equals("=") || text.equals("(") ) && reader.nextIsStart(ALTERNATIVE_FIELD_NUM)){
571
					//do nothing
572
				}else if ( (text.equals(").") || text.equals(")")) && reader.previousWasEnd(ALTERNATIVE_FIELD_NUM)){
573
					//do nothing
574
				}else if ( charIsOpeningOrClosingBracket(text) ){
575
					//for now we don't do anything, however in future brackets may have semantics
576
				}else{
577
					//TODO
578
					String message = "Unrecognized text: %s";
579
					fireWarningEvent(String.format(message, text), next, 6);
580
				}
581
			} else {
582
				handleUnexpectedElement(next);
583
			}
584
		}
585
		throw new IllegalStateException("Collection has no closing tag.");
586

    
587
	}
588

    
589

    
590
	private final String fotgPattern = "^\\(([A-Z]{1,3})(?:,\\s?([A-Z]{1,3}))*\\)"; // eg. (US, B, CAN)
591
	private void handleGatheringCollectionAndType(MarkupImportState state, XMLEventReader reader, XMLEvent parent, DerivedUnitFacade facade) throws XMLStreamException {
592
		checkNoAttributes(parent);
593

    
594
		XMLEvent next = readNoWhitespace(reader);
595

    
596
		if (next.isCharacters()){
597
			String txt = next.asCharacters().getData().trim();
598
			if (state.isSpecimenType()){
599
				state.addCollectionAndType(txt);
600
			}else{
601

    
602
				Matcher fotgMatcher = Pattern.compile(fotgPattern).matcher(txt);
603

    
604
				if (fotgMatcher.matches()){
605
					txt = txt.substring(1, txt.length() - 1);  //remove bracket
606
					String[] splits = txt.split(",");
607
					for (String split : splits ){
608
						Collection collection = getCollection(state, split.trim());
609
						if (facade.innerDerivedUnit() == null){
610
						    String message = "Adding a duplicate to a non derived unit based facade is not possible. Please check why no derived unit exists yet in facade!";
611
						    this.fireWarningEvent(message, next, -6);
612
						}else{
613
						    facade.addDuplicate(collection, null, null, null, null);
614
						}
615
					}
616
					//FIXME 9
617
					//create derived units and and add collections
618

    
619
				}else{
620
					fireWarningEvent("Collection and type pattern for gathering not recognized: " + txt, next, 4);
621
				}
622
			}
623

    
624
		}else{
625
			fireUnexpectedEvent(next, 0);
626
		}
627

    
628
		if (isMyEndingElement(next, parent)){
629
			return;  //in case we have a completely empty element
630
		}
631
		next = readNoWhitespace(reader);
632
		if (isMyEndingElement(next, parent)){
633
			return;
634
		}else{
635
			fireUnexpectedEvent(next, 0);
636
			return;
637
		}
638
	}
639

    
640

    
641
	private Collection getCollection(MarkupImportState state, String code) {
642
		Collection collection = state.getCollectionByCode(code);
643
		if (collection == null){
644
			List<Collection> list = this.docImport.getCollectionService().searchByCode(code);
645
			if (list.size() == 1){
646
				collection = list.get(0);
647
			}else if (list.size() > 1){
648
				fireWarningEvent("More then one occurrence for collection " + code +  " in database. Collection not reused" , "", 1);
649
			}
650

    
651
			if (collection == null){
652
				collection = Collection.NewInstance();
653
				collection.setCode(code);
654
				this.docImport.getCollectionService().saveOrUpdate(collection);
655
			}
656
			state.putCollectionByCode(code, collection);
657
		}
658
		return collection;
659
	}
660

    
661

    
662
	private void handleAlternativeFieldNumber(MarkupImportState state, XMLEventReader reader, XMLEvent parent, FieldUnit fieldUnit) throws XMLStreamException {
663
		Map<String, Attribute> attrs = getAttributes(parent);
664
		Boolean doubtful = this.getAndRemoveBooleanAttributeValue(parent, attrs, "doubful", false);
665

    
666
		//for now we do not handle annotation and typeNotes
667
		String altFieldNum = getCData(state, reader, parent, false).trim();
668
		DefinedTerm type = this.getIdentifierType(state, MarkupTransformer.uuidIdentTypeAlternativeFieldNumber, "Alternative field number", "Alternative field number", "alt. field no.", null);
669
		fieldUnit.addIdentifier(altFieldNum, type);
670
		if (doubtful){
671
			fireWarningEvent("Marking alternative field numbers as doubtful not yet possible, see #4673", parent,4);
672
//			Marker.NewInstance(identifier, "true", MarkerType.IS_DOUBTFUL());
673
		}
674

    
675
	}
676

    
677

    
678
	private boolean charIsOpeningOrClosingBracket(String text) {
679
		return text.equals("(") || text.equals(")");
680
	}
681

    
682

    
683
	private TimePeriod handleDates(MarkupImportState state, XMLEventReader reader, XMLEvent parent) throws XMLStreamException {
684
		checkNoAttributes(parent);
685
		TimePeriod result = TimePeriod.NewInstance();
686
		String parseMessage = "%s can not be parsed: %s";
687
		boolean hasFullDate = false;
688
		boolean hasAtomised = false;
689
		boolean hasUnparsedAtomised = false;
690
		while (reader.hasNext()) {
691
			XMLEvent next = readNoWhitespace(reader);
692
			if (isMyEndingElement(next, parent)) {
693
				if (! isAlternative(hasFullDate, hasAtomised, hasUnparsedAtomised)){
694
					String message = "Some problems exist when defining the date";
695
					fireWarningEvent(message, parent, 4);
696
				}
697
				return result;
698
			} else if (isStartingElement(next, FULL_DATE)) {
699
				String fullDate = getCData(state, reader, next, true);
700
				result = TimePeriodParser.parseString(fullDate);
701
				if (result.getFreeText() != null){
702
					fireWarningEvent(String.format(parseMessage, FULL_DATE, fullDate), parent, 1);
703
				}
704
				hasFullDate = true;
705
			} else if (isStartingElement(next, DAY)) {
706
				String day = getCData(state, reader, next, true).trim();
707
				day = normalizeDate(day);
708
				if (CdmUtils.isNumeric(day)){
709
					result.setStartDay(Integer.valueOf(day));
710
					hasAtomised = true;
711
				}else{
712
					fireWarningEvent(String.format(parseMessage,"Day", day), parent, 2);
713
					hasUnparsedAtomised = true;
714
				}
715
			} else if (isStartingElement(next, MONTH)) {
716
				String month = getCData(state, reader, next, true).trim();
717
				month = normalizeDate(month);
718
				if (CdmUtils.isNumeric(month)){
719
					result.setStartMonth(Integer.valueOf(month));
720
					hasAtomised = true;
721
				}else{
722
					fireWarningEvent(String.format(parseMessage,"Month", month), parent, 2);
723
					hasUnparsedAtomised = true;
724
				}
725
			} else if (isStartingElement(next, YEAR)) {
726
				String year = getCData(state, reader, next, true).trim();
727
				year = normalizeDate(year);
728
				if (CdmUtils.isNumeric(year)){
729
					result.setStartYear(Integer.valueOf(year));
730
					hasAtomised = true;
731
				}else{
732
					fireWarningEvent(String.format(parseMessage,"Year", year), parent, 2);
733
					hasUnparsedAtomised = true;
734
				}
735
			} else {
736
				handleUnexpectedElement(next);
737
			}
738
		}
739
		throw new IllegalStateException("Dates has no closing tag.");
740
	}
741

    
742

    
743
	private String normalizeDate(String partOfDate) {
744
		if (isBlank(partOfDate)){
745
			return null;
746
		}
747
		partOfDate = partOfDate.trim();
748
		while (partOfDate.startsWith("-")){
749
			partOfDate = partOfDate.substring(1);
750
		}
751
		return partOfDate;
752
	}
753

    
754

    
755
	private boolean isAlternative(boolean first, boolean second, boolean third) {
756
		return ( (first ^ second) && !third)  ||
757
				(! first && ! second && third) ;
758
	}
759

    
760

    
761
	private void handleLocality(MarkupImportState state, XMLEventReader reader,XMLEvent parentEvent, DerivedUnitFacade facade)throws XMLStreamException {
762
		String classValue = getClassOnlyAttribute(parentEvent);
763
		boolean isLocality = false;
764
		NamedAreaLevel areaLevel = null;
765
		if ("locality".equalsIgnoreCase(classValue)) {
766
			isLocality = true;
767
		} else {
768
			areaLevel = makeNamedAreaLevel(state, classValue, parentEvent);
769
		}
770

    
771
		String text = "";
772
		// elements
773
		while (reader.hasNext()) {
774
			XMLEvent next = readNoWhitespace(reader);
775
			if (isMyEndingElement(next, parentEvent)) {
776
				if (StringUtils.isNotBlank(text)) {
777
					text = normalize(text);
778
					if (isLocality) {
779
						facade.setLocality(text, getDefaultLanguage(state));
780
					} else {
781
						text = CdmUtils.removeTrailingDot(text);
782
						NamedArea area = makeArea(state, text, areaLevel);
783
						facade.addCollectingArea(area);
784
					}
785
				}
786
				// TODO
787
				return;
788
			}else if (isStartingElement(next, ALTITUDE)) {
789
				handleNotYetImplementedElement(next);
790
				// homotypicalGroup = handleNom(state, reader, next, taxon,
791
				// homotypicalGroup);
792
			} else if (isStartingElement(next, COORDINATES)) {
793
				handleNotYetImplementedElement(next);
794
			} else if (isStartingElement(next, ANNOTATION)) {
795
				handleNotYetImplementedElement(next);
796
			} else if (next.isCharacters()) {
797
				text += next.asCharacters().getData();
798
			} else {
799
				handleUnexpectedElement(next);
800
			}
801
		}
802
		throw new IllegalStateException("<SpecimenType> has no closing tag");
803
	}
804

    
805

    
806

    
807
	private TeamOrPersonBase<?> createCollector(String collectorStr) {
808
		return createAuthor(collectorStr);
809
	}
810

    
811

    
812
	public List<DescriptionElementBase> handleMaterialsExamined(MarkupImportState state, XMLEventReader reader, XMLEvent parentEvent, Feature feature, TaxonDescription defaultDescription) throws XMLStreamException {
813
		List<DescriptionElementBase> result = new ArrayList<DescriptionElementBase>();
814
		//reset current areas
815
		state.removeCurrentAreas();
816
		while (reader.hasNext()) {
817
			XMLEvent next = readNoWhitespace(reader);
818
			if (isMyEndingElement(next, parentEvent)) {
819
				if (result.isEmpty()){
820
					fireWarningEvent("Materials examined created empty Individual Associations list", parentEvent, 4);
821
				}
822
				state.removeCurrentAreas();
823
				return result;
824
			} else if (isStartingElement(next, SUB_HEADING)) {
825
//				Map<String, Object> inlineMarkup = new HashMap<String, Object>();
826
				String text = getCData(state, reader, next, true);
827
				if (isFeatureHeading(state, next, text)){
828
					feature = makeHeadingFeature(state, next, text, feature);
829
				}else{
830
					String message = "Unhandled subheading: %s";
831
					fireWarningEvent(String.format(message,  text), next, 4);
832
				}
833
//				for (String key : inlineMarkup.keySet()){
834
//					handleInlineMarkup(state, key, inlineMarkup);
835
//				}
836

    
837
			} else if (isStartingElement(next, BR) || isEndingElement(next, BR)) {
838
				//do nothing
839
			} else if (isStartingElement(next, GATHERING)) {
840
				DerivedUnitFacade facade = DerivedUnitFacade.NewInstance(SpecimenOrObservationType.DerivedUnit);
841
				addCurrentAreas(state, next, facade);
842
				handleGathering(state, reader, next, facade);
843
				SpecimenOrObservationBase<?> specimen;
844
				if (facade.innerDerivedUnit() != null){
845
					specimen = facade.innerDerivedUnit();
846
				}else{
847
					specimen = facade.innerFieldUnit();
848
				}
849
				IndividualsAssociation individualsAssociation = IndividualsAssociation.NewInstance();
850
				individualsAssociation.setAssociatedSpecimenOrObservation(specimen);
851
				result.add(individualsAssociation);
852
			} else if (isStartingElement(next, GATHERING_GROUP)) {
853
				List<DescriptionElementBase> list = getGatheringGroupDescription(state, reader, next);
854
				result.addAll(list);
855
			}else if (next.isCharacters()) {
856
				String text = next.asCharacters().getData().trim();
857
				if (isPunctuation(text)){
858
					//do nothing
859
				}else{
860
					String message = "Unrecognized text: %s";
861
					fireWarningEvent(String.format(message, text), next, 6);
862
				}
863
			} else {
864
				handleUnexpectedElement(next);
865
			}
866
		}
867
		throw new IllegalStateException("<String> has no closing tag");
868

    
869
	}
870

    
871

    
872

    
873
	private List<DescriptionElementBase> getGatheringGroupDescription(MarkupImportState state, XMLEventReader reader, XMLEvent parentEvent) throws XMLStreamException {
874
		Map<String, Attribute> attributes = getAttributes(parentEvent);
875
		String geoScope = getAndRemoveAttributeValue(attributes, "geoscope");
876
		Boolean doubtful = getAndRemoveBooleanAttributeValue(parentEvent, attributes, DOUBTFUL, null);
877
		checkNoAttributes(attributes, parentEvent);
878

    
879
		List<DescriptionElementBase> result = new ArrayList<DescriptionElementBase>();
880

    
881

    
882
		TaxonDescription td = null;
883

    
884
		if (isNotBlank(geoScope)){
885
			NamedArea area = Country.getCountryByLabel(geoScope);
886
			if (area == null){
887
				try {
888
					area = state.getTransformer().getNamedAreaByKey(geoScope);
889
				} catch (Exception e) {
890
					fireWarningEvent("getNamedArea not supported", parentEvent, 16);
891
				}
892
			}
893
			if (area == null){
894
				fireWarningEvent("Area for geoscope not found: " +  geoScope +"; add specimen group to ordinary description", parentEvent, 4);
895
			}else{
896
				state.addCurrentArea(area);
897
				Set<TaxonDescription> descs = state.getCurrentTaxon().getDescriptions();
898
				for (TaxonDescription desc : descs){
899
					Set<NamedArea> scopes = desc.getGeoScopes();
900
					if (scopes.size() == 1 && scopes.iterator().next().equals(area)){
901
						td = desc;
902
						break;
903
					}
904
				}
905
				if (td == null){
906
					TaxonDescription desc = TaxonDescription.NewInstance(state.getCurrentTaxon());
907
					desc.addGeoScope(area);
908
					if (doubtful != null){
909
						desc.addMarker(Marker.NewInstance(MarkerType.IS_DOUBTFUL(), doubtful));
910
					}
911
					td = desc;
912
				}
913
			}
914
		}
915

    
916
		while (reader.hasNext()) {
917
			XMLEvent next = readNoWhitespace(reader);
918
			if (isMyEndingElement(next, parentEvent)) {
919
				if (result.isEmpty()){
920
					fireWarningEvent("Gathering group created empty Individual Associations list", parentEvent, 4);
921
				}
922
				state.removeCurrentAreas();
923
				return result;
924
			} else if (isStartingElement(next, GATHERING)) {
925
				DerivedUnitFacade facade = DerivedUnitFacade.NewInstance(SpecimenOrObservationType.DerivedUnit);
926
				addCurrentAreas(state, next, facade);
927
				handleGathering(state, reader, next, facade);
928
				SpecimenOrObservationBase<?> specimen;
929
				if (facade.innerDerivedUnit() != null){
930
					specimen = facade.innerDerivedUnit();
931
				}else{
932
					specimen = facade.innerFieldUnit();
933
				}
934
				IndividualsAssociation individualsAssociation = IndividualsAssociation.NewInstance();
935
				individualsAssociation.setAssociatedSpecimenOrObservation(specimen);
936
				result.add(individualsAssociation);
937

    
938
			}else if (next.isCharacters()) {
939
				String text = next.asCharacters().getData().trim();
940
				if (isPunctuation(text)){
941
					//do nothing
942
				}else{
943
					//TODO
944
					String message = "Unrecognized text: %s";
945
					fireWarningEvent(String.format(message, text), next, 6);
946
				}
947
			} else {
948
				handleUnexpectedElement(next);
949
			}
950
		}
951
		throw new IllegalStateException("<Gathering group> has no closing tag");
952

    
953
	}
954

    
955
	private void addCurrentAreas(MarkupImportState state, XMLEvent event, DerivedUnitFacade facade) {
956
		for (NamedArea area : state.getCurrentAreas()){
957
			if (area == null){
958
				continue;
959
			}else if (area.isInstanceOf(Country.class)){
960
				facade.setCountry(area);
961
			}else{
962
				String message = "Current area %s is not country. This is not expected for currently known data.";
963
				fireWarningEvent(String.format(message, area.getTitleCache()), event, 2);
964
				facade.addCollectingArea(area);
965
			}
966
		}
967

    
968
	}
969

    
970

    
971
//	private void handleInlineMarkup(MarkupImportState state, String key, Map<String, Object> inlineMarkup) {
972
//		Object obj = inlineMarkup.get(key);
973
//		if (key.equals(LOCALITY)){
974
//			if (obj instanceof NamedArea){
975
//				NamedArea area = (NamedArea)obj;
976
//				state.addCurrentArea(area);
977
//			}
978
//		}
979
//
980
//	}
981

    
982

    
983
	/**
984
	 * Changes the feature if the (sub)-heading implies this. Also recognizes hidden country information
985
	 * @param state
986
	 * @param parent
987
	 * @param text
988
	 * @param feature
989
	 * @return
990
	 */
991
	private Feature makeHeadingFeature(MarkupImportState state, XMLEvent parent, String originalText, Feature feature) {
992
		//expand, provide by config or service
993
		String materialRegEx = "Mat[\u00E9\u00C9]riel";
994
		String examinedRegEx = "[\u00E9\u00C9]tudi[\u00E9\u00C9]";
995
		String countryRegEx = "(gabonais)";
996
		String postfixCountryRegEx = "\\s+(pour le Gabon)";
997

    
998
		String materialExaminedRegEx = "(?i)" + materialRegEx + "\\s+(" + countryRegEx +"\\s+)?" + examinedRegEx + "(" +postfixCountryRegEx + ")?:?";
999

    
1000
		String text = originalText;
1001

    
1002
		if (isBlank(text)){
1003
			return feature;
1004
		}else{
1005
			if (text.matches(materialExaminedRegEx)){
1006
				//gabon specific
1007
				if (text.contains("gabonais ")){
1008
					text = text.replace("gabonais ", "");
1009
					state.addCurrentArea(Country.GABONGABONESEREPUBLIC());
1010
				}
1011
				if (text.contains(" pour le Gabon")){
1012
					text = text.replace(" pour le Gabon", "");
1013
					state.addCurrentArea(Country.GABONGABONESEREPUBLIC());
1014
				}
1015

    
1016
				//update feature
1017
				feature = Feature.MATERIALS_EXAMINED();
1018
				state.putFeatureToGeneralSorterList(feature);
1019
				return feature;
1020
			}else{
1021
				String message = "Heading/Subheading not recognized: %s";
1022
				fireWarningEvent(String.format(message, originalText), parent, 4);
1023
				return feature;
1024
			}
1025
		}
1026
	}
1027

    
1028

    
1029
	/**
1030
	 * True if heading or subheading represents feature information
1031
	 * @param state
1032
	 * @param parent
1033
	 * @param text
1034
	 * @return
1035
	 */
1036
	private boolean isFeatureHeading(MarkupImportState state, XMLEvent parent, String text) {
1037
		return makeHeadingFeature(state, parent, text, null) != null;
1038
	}
1039

    
1040

    
1041
	public String handleInLineGathering(MarkupImportState state, XMLEventReader reader, XMLEvent parentEvent) throws XMLStreamException {
1042
		DerivedUnitFacade facade = DerivedUnitFacade.NewInstance(SpecimenOrObservationType.FieldUnit);
1043
		handleGathering(state, reader, parentEvent, facade);
1044
		SpecimenOrObservationBase<?> specimen  = facade.innerFieldUnit();
1045
		if (specimen == null){
1046
			specimen = facade.innerDerivedUnit();
1047
			String message = "Inline gaterhing has no field unit";
1048
			fireWarningEvent(message, parentEvent, 2);
1049
		}
1050

    
1051
		String result = "<cdm:specimen uuid='%s'>%s</specimen>";
1052
		if (specimen != null){
1053
			result = String.format(result, specimen.getUuid(), specimen.getTitleCache());
1054
		}else{
1055
			String message = "Inline gathering has no specimen";
1056
			fireWarningEvent(message, parentEvent, 4);
1057
		}
1058
		save(specimen, state);
1059
		return result;
1060
	}
1061

    
1062

    
1063

    
1064

    
1065

    
1066
}
(16-16/19)