Project

General

Profile

Download (146 KB) Statistics
| Branch: | Tag: | Revision:
1
/**
2
* Copyright (C) 2007 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.api.service;
11

    
12
import java.io.IOException;
13
import java.util.ArrayList;
14
import java.util.EnumSet;
15
import java.util.HashMap;
16
import java.util.HashSet;
17
import java.util.Iterator;
18
import java.util.List;
19
import java.util.Map;
20
import java.util.Set;
21
import java.util.UUID;
22

    
23
import javax.persistence.EntityNotFoundException;
24

    
25
import org.apache.commons.lang.StringUtils;
26
import org.apache.log4j.Logger;
27
import org.apache.lucene.queryparser.classic.ParseException;
28
import org.apache.lucene.sandbox.queries.DuplicateFilter;
29
import org.apache.lucene.search.BooleanClause.Occur;
30
import org.apache.lucene.search.BooleanQuery;
31
import org.apache.lucene.search.BooleanQuery.Builder;
32
import org.apache.lucene.search.Query;
33
import org.apache.lucene.search.SortField;
34
import org.apache.lucene.search.grouping.TopGroups;
35
import org.apache.lucene.search.join.ScoreMode;
36
import org.apache.lucene.util.BytesRef;
37
import org.springframework.beans.factory.annotation.Autowired;
38
import org.springframework.stereotype.Service;
39
import org.springframework.transaction.annotation.Transactional;
40

    
41
import eu.etaxonomy.cdm.api.service.config.DeleteConfiguratorBase;
42
import eu.etaxonomy.cdm.api.service.config.IFindTaxaAndNamesConfigurator;
43
import eu.etaxonomy.cdm.api.service.config.IncludedTaxonConfiguration;
44
import eu.etaxonomy.cdm.api.service.config.MatchingTaxonConfigurator;
45
import eu.etaxonomy.cdm.api.service.config.NodeDeletionConfigurator.ChildHandling;
46
import eu.etaxonomy.cdm.api.service.config.SynonymDeletionConfigurator;
47
import eu.etaxonomy.cdm.api.service.config.TaxonDeletionConfigurator;
48
import eu.etaxonomy.cdm.api.service.dto.IdentifiedEntityDTO;
49
import eu.etaxonomy.cdm.api.service.dto.IncludedTaxaDTO;
50
import eu.etaxonomy.cdm.api.service.dto.MarkedEntityDTO;
51
import eu.etaxonomy.cdm.api.service.exception.DataChangeNoRollbackException;
52
import eu.etaxonomy.cdm.api.service.exception.HomotypicalGroupChangeException;
53
import eu.etaxonomy.cdm.api.service.exception.ReferencedObjectUndeletableException;
54
import eu.etaxonomy.cdm.api.service.pager.Pager;
55
import eu.etaxonomy.cdm.api.service.pager.impl.DefaultPagerImpl;
56
import eu.etaxonomy.cdm.api.service.search.ILuceneIndexToolProvider;
57
import eu.etaxonomy.cdm.api.service.search.ISearchResultBuilder;
58
import eu.etaxonomy.cdm.api.service.search.LuceneMultiSearch;
59
import eu.etaxonomy.cdm.api.service.search.LuceneMultiSearchException;
60
import eu.etaxonomy.cdm.api.service.search.LuceneParseException;
61
import eu.etaxonomy.cdm.api.service.search.LuceneSearch;
62
import eu.etaxonomy.cdm.api.service.search.QueryFactory;
63
import eu.etaxonomy.cdm.api.service.search.SearchResult;
64
import eu.etaxonomy.cdm.api.service.search.SearchResultBuilder;
65
import eu.etaxonomy.cdm.api.service.util.TaxonRelationshipEdge;
66
import eu.etaxonomy.cdm.common.monitor.IProgressMonitor;
67
import eu.etaxonomy.cdm.hibernate.HibernateProxyHelper;
68
import eu.etaxonomy.cdm.hibernate.search.DefinedTermBaseClassBridge;
69
import eu.etaxonomy.cdm.hibernate.search.GroupByTaxonClassBridge;
70
import eu.etaxonomy.cdm.hibernate.search.MultilanguageTextFieldBridge;
71
import eu.etaxonomy.cdm.model.CdmBaseType;
72
import eu.etaxonomy.cdm.model.common.Annotation;
73
import eu.etaxonomy.cdm.model.common.AnnotationType;
74
import eu.etaxonomy.cdm.model.common.CdmBase;
75
import eu.etaxonomy.cdm.model.common.DefinedTerm;
76
import eu.etaxonomy.cdm.model.common.IdentifiableEntity;
77
import eu.etaxonomy.cdm.model.common.IdentifiableSource;
78
import eu.etaxonomy.cdm.model.common.Language;
79
import eu.etaxonomy.cdm.model.common.MarkerType;
80
import eu.etaxonomy.cdm.model.common.OriginalSourceType;
81
import eu.etaxonomy.cdm.model.common.RelationshipBase.Direction;
82
import eu.etaxonomy.cdm.model.description.CommonTaxonName;
83
import eu.etaxonomy.cdm.model.description.DescriptionBase;
84
import eu.etaxonomy.cdm.model.description.DescriptionElementBase;
85
import eu.etaxonomy.cdm.model.description.Distribution;
86
import eu.etaxonomy.cdm.model.description.Feature;
87
import eu.etaxonomy.cdm.model.description.IIdentificationKey;
88
import eu.etaxonomy.cdm.model.description.PolytomousKeyNode;
89
import eu.etaxonomy.cdm.model.description.PresenceAbsenceTerm;
90
import eu.etaxonomy.cdm.model.description.SpecimenDescription;
91
import eu.etaxonomy.cdm.model.description.TaxonDescription;
92
import eu.etaxonomy.cdm.model.description.TaxonInteraction;
93
import eu.etaxonomy.cdm.model.description.TaxonNameDescription;
94
import eu.etaxonomy.cdm.model.location.NamedArea;
95
import eu.etaxonomy.cdm.model.media.Media;
96
import eu.etaxonomy.cdm.model.media.MediaRepresentation;
97
import eu.etaxonomy.cdm.model.media.MediaUtils;
98
import eu.etaxonomy.cdm.model.name.HomotypicalGroup;
99
import eu.etaxonomy.cdm.model.name.IZoologicalName;
100
import eu.etaxonomy.cdm.model.name.Rank;
101
import eu.etaxonomy.cdm.model.name.TaxonName;
102
import eu.etaxonomy.cdm.model.name.TaxonNameFactory;
103
import eu.etaxonomy.cdm.model.occurrence.DerivedUnit;
104
import eu.etaxonomy.cdm.model.occurrence.DeterminationEvent;
105
import eu.etaxonomy.cdm.model.occurrence.SpecimenOrObservationBase;
106
import eu.etaxonomy.cdm.model.reference.Reference;
107
import eu.etaxonomy.cdm.model.taxon.Classification;
108
import eu.etaxonomy.cdm.model.taxon.HomotypicGroupTaxonComparator;
109
import eu.etaxonomy.cdm.model.taxon.ITaxonTreeNode;
110
import eu.etaxonomy.cdm.model.taxon.Synonym;
111
import eu.etaxonomy.cdm.model.taxon.SynonymType;
112
import eu.etaxonomy.cdm.model.taxon.Taxon;
113
import eu.etaxonomy.cdm.model.taxon.TaxonBase;
114
import eu.etaxonomy.cdm.model.taxon.TaxonNode;
115
import eu.etaxonomy.cdm.model.taxon.TaxonRelationship;
116
import eu.etaxonomy.cdm.model.taxon.TaxonRelationshipType;
117
import eu.etaxonomy.cdm.persistence.dao.common.ICdmGenericDao;
118
import eu.etaxonomy.cdm.persistence.dao.common.IOrderedTermVocabularyDao;
119
import eu.etaxonomy.cdm.persistence.dao.initializer.AbstractBeanInitializer;
120
import eu.etaxonomy.cdm.persistence.dao.name.ITaxonNameDao;
121
import eu.etaxonomy.cdm.persistence.dao.occurrence.IOccurrenceDao;
122
import eu.etaxonomy.cdm.persistence.dao.taxon.IClassificationDao;
123
import eu.etaxonomy.cdm.persistence.dao.taxon.ITaxonDao;
124
import eu.etaxonomy.cdm.persistence.dao.taxon.ITaxonNodeDao;
125
import eu.etaxonomy.cdm.persistence.dto.UuidAndTitleCache;
126
import eu.etaxonomy.cdm.persistence.query.MatchMode;
127
import eu.etaxonomy.cdm.persistence.query.OrderHint;
128
import eu.etaxonomy.cdm.persistence.query.OrderHint.SortOrder;
129
import eu.etaxonomy.cdm.persistence.query.TaxonTitleType;
130
import eu.etaxonomy.cdm.strategy.cache.common.IIdentifiableEntityCacheStrategy;
131

    
132

    
133
/**
134
 * @author a.kohlbecker
135
 * @date 10.09.2010
136
 */
137
@Service
138
@Transactional(readOnly = true)
139
public class TaxonServiceImpl extends IdentifiableServiceBase<TaxonBase,ITaxonDao> implements ITaxonService{
140
    private static final Logger logger = Logger.getLogger(TaxonServiceImpl.class);
141

    
142
    public static final String POTENTIAL_COMBINATION_NAMESPACE = "Potential combination";
143

    
144
    public static final String INFERRED_EPITHET_NAMESPACE = "Inferred epithet";
145

    
146
    public static final String INFERRED_GENUS_NAMESPACE = "Inferred genus";
147

    
148
    @Autowired
149
    private ITaxonNodeDao taxonNodeDao;
150

    
151
    @Autowired
152
    private ITaxonNameDao nameDao;
153

    
154
    @Autowired
155
    private INameService nameService;
156

    
157
    @Autowired
158
    private IOccurrenceService occurrenceService;
159

    
160
    @Autowired
161
    private ITaxonNodeService nodeService;
162

    
163
    @Autowired
164
    private ICdmGenericDao genericDao;
165

    
166
    @Autowired
167
    private IDescriptionService descriptionService;
168

    
169
    @Autowired
170
    private IOrderedTermVocabularyDao orderedVocabularyDao;
171

    
172
    @Autowired
173
    private IOccurrenceDao occurrenceDao;
174

    
175
    @Autowired
176
    private IClassificationDao classificationDao;
177

    
178
    @Autowired
179
    private AbstractBeanInitializer beanInitializer;
180

    
181
    @Autowired
182
    private ILuceneIndexToolProvider luceneIndexToolProvider;
183

    
184
    /**
185
     * Constructor
186
     */
187
    public TaxonServiceImpl(){
188
        if (logger.isDebugEnabled()) { logger.debug("Load TaxonService Bean"); }
189
    }
190

    
191
    /**
192
     * FIXME Candidate for harmonization
193
     * rename searchByName ?
194
     */
195
    @Override
196
    public List<TaxonBase> searchTaxaByName(String name, Reference sec) {
197
        return dao.getTaxaByName(name, sec);
198
    }
199

    
200
    @Override
201
    @Transactional(readOnly = false)
202
    public UpdateResult swapSynonymAndAcceptedTaxon(Synonym synonym, Taxon acceptedTaxon){
203
    	UpdateResult result = new UpdateResult();
204
        TaxonName synonymName = synonym.getName();
205
        synonymName.removeTaxonBase(synonym);
206
        TaxonName taxonName = acceptedTaxon.getName();
207
        taxonName.removeTaxonBase(acceptedTaxon);
208

    
209
        synonym.setName(taxonName);
210
        acceptedTaxon.setName(synonymName);
211
        saveOrUpdate(synonym);
212
        saveOrUpdate(acceptedTaxon);
213
        result.addUpdatedObject(acceptedTaxon);
214
        result.addUpdatedObject(synonym);
215
		return result;
216

    
217
        // the accepted taxon needs a new uuid because the concept has changed
218
        // FIXME this leads to an error "HibernateException: immutable natural identifier of an instance of eu.etaxonomy.cdm.model.taxon.Taxon was altered"
219
        //acceptedTaxon.setUuid(UUID.randomUUID());
220
    }
221

    
222

    
223
    @Override
224
    @Transactional(readOnly = false)
225
    public UpdateResult changeSynonymToAcceptedTaxon(Synonym synonym, Taxon acceptedTaxon, boolean deleteSynonym) {
226
        UpdateResult result = new UpdateResult();
227
        TaxonName acceptedName = acceptedTaxon.getName();
228
        TaxonName synonymName = synonym.getName();
229
        HomotypicalGroup synonymHomotypicGroup = synonymName.getHomotypicalGroup();
230

    
231
        //check synonym is not homotypic
232
        if (acceptedName.getHomotypicalGroup().equals(synonymHomotypicGroup)){
233
            String message = "The accepted taxon and the synonym are part of the same homotypical group and therefore can not be both accepted.";
234
            result.addException(new HomotypicalGroupChangeException(message));
235
            result.setAbort();
236
            return result;
237
        }
238

    
239
        Taxon newAcceptedTaxon = Taxon.NewInstance(synonymName, acceptedTaxon.getSec());
240
        dao.save(newAcceptedTaxon);
241
        result.setCdmEntity(newAcceptedTaxon);
242
        SynonymType relTypeForGroup = SynonymType.HOMOTYPIC_SYNONYM_OF();
243
        List<Synonym> heteroSynonyms = acceptedTaxon.getSynonymsInGroup(synonymHomotypicGroup);
244

    
245
        for (Synonym heteroSynonym : heteroSynonyms){
246
            if (synonym.equals(heteroSynonym)){
247
                acceptedTaxon.removeSynonym(heteroSynonym, false);
248
            }else{
249
                //move synonyms in same homotypic group to new accepted taxon
250
                newAcceptedTaxon.addSynonym(heteroSynonym, relTypeForGroup);
251
            }
252
        }
253
        dao.saveOrUpdate(acceptedTaxon);
254
        result.addUpdatedObject(acceptedTaxon);
255
        if (deleteSynonym){
256

    
257
            try {
258
                this.dao.flush();
259
                SynonymDeletionConfigurator config = new SynonymDeletionConfigurator();
260
                config.setDeleteNameIfPossible(false);
261
                this.deleteSynonym(synonym, config);
262

    
263
            } catch (Exception e) {
264
                result.addException(e);
265
            }
266
        }
267

    
268
        return result;
269
    }
270

    
271
    @Override
272
    @Transactional(readOnly = false)
273
    public UpdateResult changeSynonymToAcceptedTaxon(UUID synonymUuid,
274
            UUID acceptedTaxonUuid,
275
            UUID newParentNodeUuid,
276
            boolean deleteSynonym)  {
277
        UpdateResult result = new UpdateResult();
278
        Synonym synonym = CdmBase.deproxy(dao.load(synonymUuid), Synonym.class);
279
        Taxon acceptedTaxon = CdmBase.deproxy(dao.load(acceptedTaxonUuid), Taxon.class);
280
        result =  changeSynonymToAcceptedTaxon(synonym, acceptedTaxon, deleteSynonym);
281
        Taxon newTaxon = (Taxon)result.getCdmEntity();
282
        TaxonNode newParentNode = taxonNodeDao.load(newParentNodeUuid);
283
        TaxonNode newNode = newParentNode.addChildTaxon(newTaxon, null, null);
284
        taxonNodeDao.save(newNode);
285
        result.addUpdatedObject(newTaxon);
286
        result.addUpdatedObject(acceptedTaxon);
287
        result.setCdmEntity(newNode);
288
        return result;
289
    }
290

    
291
    @Override
292
    @Transactional(readOnly = false)
293
    public UpdateResult changeSynonymToRelatedTaxon(UUID synonymUuid,
294
            UUID toTaxonUuid,
295
            TaxonRelationshipType taxonRelationshipType,
296
            Reference citation,
297
            String microcitation){
298

    
299
        UpdateResult result = new UpdateResult();
300
        Taxon toTaxon = (Taxon) dao.load(toTaxonUuid);
301
        Synonym synonym = (Synonym) dao.load(synonymUuid);
302
        result = changeSynonymToRelatedTaxon(synonym, toTaxon, taxonRelationshipType, citation, microcitation);
303
        Taxon relatedTaxon = (Taxon)result.getCdmEntity();
304
//        result.setCdmEntity(relatedTaxon);
305
        result.addUpdatedObject(relatedTaxon);
306
        result.addUpdatedObject(toTaxon);
307
        return result;
308
    }
309

    
310
    @Override
311
    @Transactional(readOnly = false)
312
    public UpdateResult changeSynonymToRelatedTaxon(Synonym synonym, Taxon toTaxon, TaxonRelationshipType taxonRelationshipType, Reference citation, String microcitation){
313
        UpdateResult result = new UpdateResult();
314
        // Get name from synonym
315
        if (synonym == null){
316
            return null;
317
        }
318
        TaxonName synonymName = synonym.getName();
319

    
320
      /*  // remove synonym from taxon
321
        toTaxon.removeSynonym(synonym);
322
*/
323
        // Create a taxon with synonym name
324
        Taxon fromTaxon = Taxon.NewInstance(synonymName, null);
325
        fromTaxon.setAppendedPhrase(synonym.getAppendedPhrase());
326

    
327
        // Add taxon relation
328
        fromTaxon.addTaxonRelation(toTaxon, taxonRelationshipType, citation, microcitation);
329
        result.setCdmEntity(fromTaxon);
330
        // since we are swapping names, we have to detach the name from the synonym completely.
331
        // Otherwise the synonym will still be in the list of typified names.
332
       // synonym.getName().removeTaxonBase(synonym);
333
        result.includeResult(this.deleteSynonym(synonym, null));
334

    
335
        return result;
336
    }
337

    
338
    @Transactional(readOnly = false)
339
    @Override
340
    public void changeHomotypicalGroupOfSynonym(Synonym synonym, HomotypicalGroup newHomotypicalGroup,
341
            Taxon targetTaxon, boolean setBasionymRelationIfApplicable){
342
        // Get synonym name
343
        TaxonName synonymName = synonym.getName();
344
        HomotypicalGroup oldHomotypicalGroup = synonymName.getHomotypicalGroup();
345

    
346
        // Switch groups
347
        oldHomotypicalGroup.removeTypifiedName(synonymName, false);
348
        newHomotypicalGroup.addTypifiedName(synonymName);
349

    
350
        //remove existing basionym relationships
351
        synonymName.removeBasionyms();
352

    
353
        //add basionym relationship
354
        if (setBasionymRelationIfApplicable){
355
            Set<TaxonName> basionyms = newHomotypicalGroup.getBasionyms();
356
            for (TaxonName basionym : basionyms){
357
                synonymName.addBasionym(basionym);
358
            }
359
        }
360

    
361
        //set synonym relationship correctly
362
        Taxon acceptedTaxon = synonym.getAcceptedTaxon();
363

    
364
        boolean hasNewTargetTaxon = targetTaxon != null && !targetTaxon.equals(acceptedTaxon);
365
        if (acceptedTaxon != null){
366

    
367
            HomotypicalGroup acceptedGroup = acceptedTaxon.getHomotypicGroup();
368
            boolean isHomotypicToTaxon = acceptedGroup.equals(newHomotypicalGroup);
369
            SynonymType newRelationType = isHomotypicToTaxon? SynonymType.HOMOTYPIC_SYNONYM_OF() : SynonymType.HETEROTYPIC_SYNONYM_OF();
370
            synonym.setType(newRelationType);
371

    
372
            if (hasNewTargetTaxon){
373
                acceptedTaxon.removeSynonym(synonym, false);
374
            }
375
        }
376
        if (hasNewTargetTaxon ){
377
            @SuppressWarnings("null")
378
            HomotypicalGroup acceptedGroup = targetTaxon.getHomotypicGroup();
379
            boolean isHomotypicToTaxon = acceptedGroup.equals(newHomotypicalGroup);
380
            SynonymType relType = isHomotypicToTaxon? SynonymType.HOMOTYPIC_SYNONYM_OF() : SynonymType.HETEROTYPIC_SYNONYM_OF();
381
            targetTaxon.addSynonym(synonym, relType);
382
        }
383

    
384
    }
385

    
386
    @Override
387
    @Transactional(readOnly = false)
388
    public void updateTitleCache(Class<? extends TaxonBase> clazz, Integer stepSize, IIdentifiableEntityCacheStrategy<TaxonBase> cacheStrategy, IProgressMonitor monitor) {
389
        if (clazz == null){
390
            clazz = TaxonBase.class;
391
        }
392
        super.updateTitleCacheImpl(clazz, stepSize, cacheStrategy, monitor);
393
    }
394

    
395
    @Override
396
    @Autowired
397
    protected void setDao(ITaxonDao dao) {
398
        this.dao = dao;
399
    }
400

    
401
    @Override
402
    public Pager<TaxonBase> findTaxaByName(Class<? extends TaxonBase> clazz, String uninomial,	String infragenericEpithet, String specificEpithet,	String infraspecificEpithet, String authorship, Rank rank, Integer pageSize,Integer pageNumber) {
403
        if (clazz == null){
404
            clazz = TaxonBase.class;
405
        }
406
        Integer numberOfResults = dao.countTaxaByName(clazz, uninomial, infragenericEpithet, specificEpithet, infraspecificEpithet, rank);
407

    
408
        List<TaxonBase> results = new ArrayList<>();
409
        if(numberOfResults > 0) { // no point checking again
410
            results = dao.findTaxaByName(clazz, uninomial, infragenericEpithet, specificEpithet, infraspecificEpithet, authorship, rank, pageSize, pageNumber);
411
        }
412

    
413
        return new DefaultPagerImpl<TaxonBase>(pageNumber, numberOfResults, pageSize, results);
414
    }
415

    
416
    @Override
417
    public List<TaxonBase> listTaxaByName(Class<? extends TaxonBase> clazz, String uninomial, String infragenericEpithet, String specificEpithet,	String infraspecificEpithet, String authorship, Rank rank, Integer pageSize,Integer pageNumber) {
418
        if (clazz == null){
419
            clazz = TaxonBase.class;
420
        }
421
        Integer numberOfResults = dao.countTaxaByName(clazz, uninomial, infragenericEpithet, specificEpithet, infraspecificEpithet, rank);
422

    
423
        List<TaxonBase> results = new ArrayList<>();
424
        if(numberOfResults > 0) { // no point checking again
425
            results = dao.findTaxaByName(clazz, uninomial, infragenericEpithet, specificEpithet, infraspecificEpithet, authorship, rank, pageSize, pageNumber);
426
        }
427

    
428
        return results;
429
    }
430

    
431
    @Override
432
    public List<TaxonRelationship> listToTaxonRelationships(Taxon taxon, TaxonRelationshipType type, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths){
433
        Integer numberOfResults = dao.countTaxonRelationships(taxon, type, TaxonRelationship.Direction.relatedTo);
434

    
435
        List<TaxonRelationship> results = new ArrayList<>();
436
        if(numberOfResults > 0) { // no point checking again
437
            results = dao.getTaxonRelationships(taxon, type, pageSize, pageNumber, orderHints, propertyPaths, TaxonRelationship.Direction.relatedTo);
438
        }
439
        return results;
440
    }
441

    
442
    @Override
443
    public Pager<TaxonRelationship> pageToTaxonRelationships(Taxon taxon, TaxonRelationshipType type, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) {
444
        Integer numberOfResults = dao.countTaxonRelationships(taxon, type, TaxonRelationship.Direction.relatedTo);
445

    
446
        List<TaxonRelationship> results = new ArrayList<>();
447
        if(numberOfResults > 0) { // no point checking again
448
            results = dao.getTaxonRelationships(taxon, type, pageSize, pageNumber, orderHints, propertyPaths, TaxonRelationship.Direction.relatedTo);
449
        }
450
        return new DefaultPagerImpl<TaxonRelationship>(pageNumber, numberOfResults, pageSize, results);
451
    }
452

    
453
    @Override
454
    public List<TaxonRelationship> listFromTaxonRelationships(Taxon taxon, TaxonRelationshipType type, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths){
455
        Integer numberOfResults = dao.countTaxonRelationships(taxon, type, TaxonRelationship.Direction.relatedFrom);
456

    
457
        List<TaxonRelationship> results = new ArrayList<>();
458
        if(numberOfResults > 0) { // no point checking again
459
            results = dao.getTaxonRelationships(taxon, type, pageSize, pageNumber, orderHints, propertyPaths, TaxonRelationship.Direction.relatedFrom);
460
        }
461
        return results;
462
    }
463

    
464
    @Override
465
    public Pager<TaxonRelationship> pageFromTaxonRelationships(Taxon taxon, TaxonRelationshipType type, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) {
466
        Integer numberOfResults = dao.countTaxonRelationships(taxon, type, TaxonRelationship.Direction.relatedFrom);
467

    
468
        List<TaxonRelationship> results = new ArrayList<>();
469
        if(numberOfResults > 0) { // no point checking again
470
            results = dao.getTaxonRelationships(taxon, type, pageSize, pageNumber, orderHints, propertyPaths, TaxonRelationship.Direction.relatedFrom);
471
        }
472
        return new DefaultPagerImpl<TaxonRelationship>(pageNumber, numberOfResults, pageSize, results);
473
    }
474

    
475
    @Override
476
    public List<TaxonRelationship> listTaxonRelationships(Set<TaxonRelationshipType> types,
477
            Integer pageSize, Integer pageStart, List<OrderHint> orderHints, List<String> propertyPaths) {
478
        Long numberOfResults = dao.countTaxonRelationships(types);
479

    
480
        List<TaxonRelationship> results = new ArrayList<>();
481
        if(numberOfResults > 0) {
482
            results = dao.getTaxonRelationships(types, pageSize, pageStart, orderHints, propertyPaths);
483
        }
484
        return results;
485
    }
486

    
487
    @Override
488
    public Taxon findAcceptedTaxonFor(UUID synonymUuid, UUID classificationUuid, List<String> propertyPaths){
489

    
490
        Taxon result = null;
491
        Long count = 0l;
492

    
493
        Synonym synonym = null;
494

    
495
        try {
496
            synonym = (Synonym) dao.load(synonymUuid);
497
        } catch (ClassCastException e){
498
            throw new EntityNotFoundException("The TaxonBase entity referenced by " + synonymUuid + " is not a Synonmy");
499
        } catch (NullPointerException e){
500
            throw new EntityNotFoundException("No TaxonBase entity found for " + synonymUuid);
501
        }
502

    
503
        Classification classificationFilter = null;
504
        if(classificationUuid != null){
505
            try {
506
            classificationFilter = classificationDao.load(classificationUuid);
507
            } catch (NullPointerException e){
508
                throw new EntityNotFoundException("No Classification entity found for " + classificationUuid);
509
            }
510
            if(classificationFilter == null){
511

    
512
            }
513
        }
514

    
515
        count = dao.countAcceptedTaxonFor(synonym, classificationFilter) ;
516
        if(count > 0){
517
            result = dao.acceptedTaxonFor(synonym, classificationFilter, propertyPaths);
518
        }
519

    
520
        return result;
521
    }
522

    
523

    
524
    @Override
525
    public Set<Taxon> listRelatedTaxa(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships, Integer maxDepth,
526
            Integer limit, Integer start, List<String> propertyPaths) {
527

    
528
        Set<Taxon> relatedTaxa = collectRelatedTaxa(taxon, includeRelationships, new HashSet<>(), maxDepth);
529
        relatedTaxa.remove(taxon);
530
        beanInitializer.initializeAll(relatedTaxa, propertyPaths);
531
        return relatedTaxa;
532
    }
533

    
534

    
535
    /**
536
     * recursively collect related taxa for the given <code>taxon</code> . The returned list will also include the
537
     *  <code>taxon</code> supplied as parameter.
538
     *
539
     * @param taxon
540
     * @param includeRelationships
541
     * @param taxa
542
     * @param maxDepth can be <code>null</code> for infinite depth
543
     * @return
544
     */
545
    private Set<Taxon> collectRelatedTaxa(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships, Set<Taxon> taxa, Integer maxDepth) {
546

    
547
        if(taxa.isEmpty()) {
548
            taxa.add(taxon);
549
        }
550

    
551
        if(includeRelationships.isEmpty()){
552
            return taxa;
553
        }
554

    
555
        if(maxDepth != null) {
556
            maxDepth--;
557
        }
558
        if(logger.isDebugEnabled()){
559
            logger.debug("collecting related taxa for " + taxon + " with maxDepth=" + maxDepth);
560
        }
561
        List<TaxonRelationship> taxonRelationships = dao.getTaxonRelationships(taxon, null, null, null, null, null, null);
562
        for (TaxonRelationship taxRel : taxonRelationships) {
563

    
564
            // skip invalid data
565
            if (taxRel.getToTaxon() == null || taxRel.getFromTaxon() == null || taxRel.getType() == null) {
566
                continue;
567
            }
568
            // filter by includeRelationships
569
            for (TaxonRelationshipEdge relationshipEdgeFilter : includeRelationships) {
570
                if ( relationshipEdgeFilter.getTaxonRelationshipType().equals(taxRel.getType()) ) {
571
                    if (relationshipEdgeFilter.getDirections().contains(Direction.relatedTo) && !taxa.contains(taxRel.getToTaxon())) {
572
                        if(logger.isDebugEnabled()){
573
                            logger.debug(maxDepth + ": " + taxon.getTitleCache() + " --[" + taxRel.getType().getLabel() + "]--> " + taxRel.getToTaxon().getTitleCache());
574
                        }
575
                        taxa.add(taxRel.getToTaxon());
576
                        if(maxDepth == null || maxDepth > 0) {
577
                            taxa.addAll(collectRelatedTaxa(taxRel.getToTaxon(), includeRelationships, taxa, maxDepth));
578
                        }
579
                    }
580
                    if(relationshipEdgeFilter.getDirections().contains(Direction.relatedFrom) && !taxa.contains(taxRel.getFromTaxon())) {
581
                        taxa.add(taxRel.getFromTaxon());
582
                        if(logger.isDebugEnabled()){
583
                            logger.debug(maxDepth + ": " +taxRel.getFromTaxon().getTitleCache() + " --[" + taxRel.getType().getLabel() + "]--> " + taxon.getTitleCache() );
584
                        }
585
                        if(maxDepth == null || maxDepth > 0) {
586
                            taxa.addAll(collectRelatedTaxa(taxRel.getFromTaxon(), includeRelationships, taxa, maxDepth));
587
                        }
588
                    }
589
                }
590
            }
591
        }
592
        return taxa;
593
    }
594

    
595
    @Override
596
    public Pager<Synonym> getSynonyms(Taxon taxon,	SynonymType type, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) {
597
        Long numberOfResults = dao.countSynonyms(taxon, type);
598

    
599
        List<Synonym> results = new ArrayList<>();
600
        if(numberOfResults > 0) { // no point checking again
601
            results = dao.getSynonyms(taxon, type, pageSize, pageNumber, orderHints, propertyPaths);
602
        }
603

    
604
        return new DefaultPagerImpl<Synonym>(pageNumber, numberOfResults, pageSize, results);
605
    }
606

    
607
    @Override
608
    public List<List<Synonym>> getSynonymsByHomotypicGroup(Taxon taxon, List<String> propertyPaths){
609
        List<List<Synonym>> result = new ArrayList<>();
610
        taxon = (Taxon)dao.load(taxon.getUuid(), propertyPaths);
611
        HomotypicGroupTaxonComparator comparator = new HomotypicGroupTaxonComparator(taxon);
612

    
613

    
614
        //homotypic
615
        result.add(taxon.getHomotypicSynonymsByHomotypicGroup(comparator));
616

    
617
        //heterotypic
618
        List<HomotypicalGroup> homotypicalGroups = taxon.getHeterotypicSynonymyGroups();  //currently the list is sorted by the Taxon.defaultTaxonComparator
619
        for(HomotypicalGroup homotypicalGroup : homotypicalGroups){
620
            result.add(taxon.getSynonymsInGroup(homotypicalGroup, comparator));
621
        }
622

    
623
        return result;
624

    
625
    }
626

    
627
    @Override
628
    public List<Synonym> getHomotypicSynonymsByHomotypicGroup(Taxon taxon, List<String> propertyPaths){
629
        Taxon t = (Taxon)dao.load(taxon.getUuid(), propertyPaths);
630
        HomotypicGroupTaxonComparator comparator = new HomotypicGroupTaxonComparator(taxon);
631

    
632
        return t.getHomotypicSynonymsByHomotypicGroup(comparator);
633
    }
634

    
635
    @Override
636
    public List<List<Synonym>> getHeterotypicSynonymyGroups(Taxon taxon, List<String> propertyPaths){
637
        Taxon t = (Taxon)dao.load(taxon.getUuid(), propertyPaths);
638
        List<HomotypicalGroup> homotypicalGroups = t.getHeterotypicSynonymyGroups();
639
        List<List<Synonym>> heterotypicSynonymyGroups = new ArrayList<>(homotypicalGroups.size());
640
        for(HomotypicalGroup homotypicalGroup : homotypicalGroups){
641
            heterotypicSynonymyGroups.add(t.getSynonymsInGroup(homotypicalGroup));
642
        }
643
        return heterotypicSynonymyGroups;
644
    }
645

    
646
    @Override
647
    public List<UuidAndTitleCache<IdentifiableEntity>> findTaxaAndNamesForEditor(IFindTaxaAndNamesConfigurator configurator){
648

    
649
        List<UuidAndTitleCache<IdentifiableEntity>> results = new ArrayList<UuidAndTitleCache<IdentifiableEntity>>();
650

    
651

    
652
        if (configurator.isDoSynonyms() || configurator.isDoTaxa() || configurator.isDoNamesWithoutTaxa() || configurator.isDoTaxaByCommonNames()){
653
        	results = dao.getTaxaByNameForEditor(configurator.isDoTaxa(), configurator.isDoSynonyms(), configurator.isDoNamesWithoutTaxa(), configurator.isDoMisappliedNames(), configurator.isDoTaxaByCommonNames(), configurator.getTitleSearchStringSqlized(), configurator.getClassification(), configurator.getMatchMode(), configurator.getNamedAreas(), configurator.getOrder());
654
        }
655

    
656
        return results;
657
    }
658

    
659
    @Override
660
    public Pager<IdentifiableEntity> findTaxaAndNames(IFindTaxaAndNamesConfigurator configurator) {
661

    
662
        List<IdentifiableEntity> results = new ArrayList<>();
663
        long numberOfResults = 0; // overall number of results (as opposed to number of results per page)
664
        List<TaxonBase> taxa = null;
665

    
666
        // Taxa and synonyms
667
        long numberTaxaResults = 0L;
668

    
669

    
670
        List<String> propertyPath = new ArrayList<String>();
671
        if(configurator.getTaxonPropertyPath() != null){
672
            propertyPath.addAll(configurator.getTaxonPropertyPath());
673
        }
674

    
675

    
676
       if (configurator.isDoMisappliedNames() || configurator.isDoSynonyms() || configurator.isDoTaxa() || configurator.isDoTaxaByCommonNames()){
677
            if(configurator.getPageSize() != null){ // no point counting if we need all anyway
678
                numberTaxaResults =
679
                    dao.countTaxaByName(configurator.isDoTaxa(),configurator.isDoSynonyms(), configurator.isDoMisappliedNames(),
680
                        configurator.isDoTaxaByCommonNames(), configurator.isDoIncludeAuthors(), configurator.getTitleSearchStringSqlized(),
681
                        configurator.getClassification(), configurator.getMatchMode(),
682
                        configurator.getNamedAreas());
683
            }
684

    
685
            if(configurator.getPageSize() == null || numberTaxaResults > configurator.getPageSize() * configurator.getPageNumber()){ // no point checking again if less results
686
                taxa = dao.getTaxaByName(configurator.isDoTaxa(), configurator.isDoSynonyms(),
687
                    configurator.isDoMisappliedNames(), configurator.isDoTaxaByCommonNames(), configurator.isDoIncludeAuthors(),
688
                    configurator.getTitleSearchStringSqlized(), configurator.getClassification(),
689
                    configurator.getMatchMode(), configurator.getNamedAreas(), configurator.getOrder(),
690
                    configurator.getPageSize(), configurator.getPageNumber(), propertyPath);
691
            }
692
       }
693

    
694
        if (logger.isDebugEnabled()) { logger.debug(numberTaxaResults + " matching taxa counted"); }
695

    
696
        if(taxa != null){
697
            results.addAll(taxa);
698
        }
699

    
700
        numberOfResults += numberTaxaResults;
701

    
702
        // Names without taxa
703
        if (configurator.isDoNamesWithoutTaxa()) {
704
            int numberNameResults = 0;
705

    
706
            List<? extends TaxonName> names =
707
                nameDao.findByName(configurator.isDoIncludeAuthors(), configurator.getTitleSearchStringSqlized(), configurator.getMatchMode(),
708
                        configurator.getPageSize(), configurator.getPageNumber(), null, configurator.getTaxonNamePropertyPath());
709
            if (logger.isDebugEnabled()) { logger.debug(names.size() + " matching name(s) found"); }
710
            if (names.size() > 0) {
711
                for (TaxonName taxonName : names) {
712
                    if (taxonName.getTaxonBases().size() == 0) {
713
                        results.add(taxonName);
714
                        numberNameResults++;
715
                    }
716
                }
717
                if (logger.isDebugEnabled()) { logger.debug(numberNameResults + " matching name(s) without taxa found"); }
718
                numberOfResults += numberNameResults;
719
            }
720
        }
721

    
722

    
723

    
724
       return new DefaultPagerImpl<>
725
            (configurator.getPageNumber(), numberOfResults, configurator.getPageSize(), results);
726
    }
727

    
728
    public List<UuidAndTitleCache<TaxonBase>> getTaxonUuidAndTitleCache(Integer limit, String pattern){
729
        return dao.getUuidAndTitleCache(limit, pattern);
730
    }
731

    
732
    @Override
733
    public List<MediaRepresentation> getAllMedia(Taxon taxon, int size, int height, int widthOrDuration, String[] mimeTypes){
734
        List<MediaRepresentation> medRep = new ArrayList<>();
735
        taxon = (Taxon)dao.load(taxon.getUuid());
736
        Set<TaxonDescription> descriptions = taxon.getDescriptions();
737
        for (TaxonDescription taxDesc: descriptions){
738
            Set<DescriptionElementBase> elements = taxDesc.getElements();
739
            for (DescriptionElementBase descElem: elements){
740
                for(Media media : descElem.getMedia()){
741

    
742
                    //find the best matching representation
743
                    medRep.add(MediaUtils.findBestMatchingRepresentation(media, null, size, height, widthOrDuration, mimeTypes));
744

    
745
                }
746
            }
747
        }
748
        return medRep;
749
    }
750

    
751
    @Override
752
    public List<Media> listTaxonDescriptionMedia(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships, boolean limitToGalleries, List<String> propertyPath){
753
        return listMedia(taxon, includeRelationships, limitToGalleries, true, false, false, propertyPath);
754
    }
755

    
756
    @Override
757
    public List<Media> listMedia(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships,
758
            Boolean limitToGalleries, Boolean includeTaxonDescriptions, Boolean includeOccurrences,
759
            Boolean includeTaxonNameDescriptions, List<String> propertyPath) {
760

    
761
    //    logger.setLevel(Level.TRACE);
762
//        Logger.getLogger("org.hibernate.SQL").setLevel(Level.TRACE);
763

    
764
        logger.trace("listMedia() - START");
765

    
766
        Set<Taxon> taxa = new HashSet<>();
767
        List<Media> taxonMedia = new ArrayList<>();
768
        List<Media> nonImageGalleryImages = new ArrayList<>();
769

    
770
        if (limitToGalleries == null) {
771
            limitToGalleries = false;
772
        }
773

    
774
        // --- resolve related taxa
775
        if (includeRelationships != null && ! includeRelationships.isEmpty()) {
776
            logger.trace("listMedia() - resolve related taxa");
777
            taxa = listRelatedTaxa(taxon, includeRelationships, null, null, null, null);
778
        }
779

    
780
        taxa.add((Taxon) dao.load(taxon.getUuid()));
781

    
782
        if(includeTaxonDescriptions != null && includeTaxonDescriptions){
783
            logger.trace("listMedia() - includeTaxonDescriptions");
784
            List<TaxonDescription> taxonDescriptions = new ArrayList<>();
785
            // --- TaxonDescriptions
786
            for (Taxon t : taxa) {
787
                taxonDescriptions.addAll(descriptionService.listTaxonDescriptions(t, null, null, null, null, propertyPath));
788
            }
789
            for (TaxonDescription taxonDescription : taxonDescriptions) {
790
                if (!limitToGalleries || taxonDescription.isImageGallery()) {
791
                    for (DescriptionElementBase element : taxonDescription.getElements()) {
792
                        for (Media media : element.getMedia()) {
793
                            if(taxonDescription.isImageGallery()){
794
                                taxonMedia.add(media);
795
                            }
796
                            else{
797
                                nonImageGalleryImages.add(media);
798
                            }
799
                        }
800
                    }
801
                }
802
            }
803
            //put images from image gallery first (#3242)
804
            taxonMedia.addAll(nonImageGalleryImages);
805
        }
806

    
807

    
808
        if(includeOccurrences != null && includeOccurrences) {
809
            logger.trace("listMedia() - includeOccurrences");
810
            Set<SpecimenOrObservationBase> specimensOrObservations = new HashSet<>();
811
            // --- Specimens
812
            for (Taxon t : taxa) {
813
                specimensOrObservations.addAll(occurrenceDao.listByAssociatedTaxon(null, t, null, null, null, null));
814
            }
815
            for (SpecimenOrObservationBase occurrence : specimensOrObservations) {
816

    
817
//            	direct media removed from specimen #3597
818
//              taxonMedia.addAll(occurrence.getMedia());
819

    
820
                // SpecimenDescriptions
821
                Set<SpecimenDescription> specimenDescriptions = occurrence.getSpecimenDescriptions();
822
                for (DescriptionBase specimenDescription : specimenDescriptions) {
823
                    if (!limitToGalleries || specimenDescription.isImageGallery()) {
824
                        Set<DescriptionElementBase> elements = specimenDescription.getElements();
825
                        for (DescriptionElementBase element : elements) {
826
                            for (Media media : element.getMedia()) {
827
                                taxonMedia.add(media);
828
                            }
829
                        }
830
                    }
831
                }
832

    
833
                if (occurrence.isInstanceOf(DerivedUnit.class)) {
834
                    DerivedUnit derivedUnit = CdmBase.deproxy(occurrence, DerivedUnit.class);
835
                    // Collection
836
                    //TODO why may collections have media attached? #
837
                    if (derivedUnit.getCollection() != null){
838
                        taxonMedia.addAll(derivedUnit.getCollection().getMedia());
839
                    }
840
                }
841
                //media in hierarchy
842
                taxonMedia.addAll(occurrenceService.getMediainHierarchy(occurrence, null, null, propertyPath).getRecords());
843
            }
844
        }
845

    
846
        if(includeTaxonNameDescriptions != null && includeTaxonNameDescriptions) {
847
            logger.trace("listMedia() - includeTaxonNameDescriptions");
848
            // --- TaxonNameDescription
849
            Set<TaxonNameDescription> nameDescriptions = new HashSet<>();
850
            for (Taxon t : taxa) {
851
                nameDescriptions .addAll(t.getName().getDescriptions());
852
            }
853
            for(TaxonNameDescription nameDescription: nameDescriptions){
854
                if (!limitToGalleries || nameDescription.isImageGallery()) {
855
                    Set<DescriptionElementBase> elements = nameDescription.getElements();
856
                    for (DescriptionElementBase element : elements) {
857
                        for (Media media : element.getMedia()) {
858
                            taxonMedia.add(media);
859
                        }
860
                    }
861
                }
862
            }
863
        }
864

    
865

    
866
        logger.trace("listMedia() - initialize");
867
        beanInitializer.initializeAll(taxonMedia, propertyPath);
868

    
869
        logger.trace("listMedia() - END");
870

    
871
        return taxonMedia;
872
    }
873

    
874
    @Override
875
    public List<TaxonBase> findTaxaByID(Set<Integer> listOfIDs) {
876
        return this.dao.loadList(listOfIDs, null);
877
    }
878

    
879
    @Override
880
    public TaxonBase findTaxonByUuid(UUID uuid, List<String> propertyPaths){
881
        return this.dao.findByUuid(uuid, null ,propertyPaths);
882
    }
883

    
884
    @Override
885
    public int countSynonyms(boolean onlyAttachedToTaxon){
886
        return this.dao.countSynonyms(onlyAttachedToTaxon);
887
    }
888

    
889
    @Override
890
    public List<TaxonName> findIdenticalTaxonNames(List<String> propertyPath) {
891
        return this.dao.findIdenticalTaxonNames(propertyPath);
892
    }
893

    
894
    @Override
895
    @Transactional(readOnly=false)
896
    public DeleteResult deleteTaxon(UUID taxonUUID, TaxonDeletionConfigurator config, UUID classificationUuid)  {
897

    
898
    	if (config == null){
899
            config = new TaxonDeletionConfigurator();
900
        }
901
    	Taxon taxon = (Taxon)dao.load(taxonUUID);
902
    	DeleteResult result = new DeleteResult();
903
    	if (taxon == null){
904
    	    result.setAbort();
905
    	    result.addException(new Exception ("The taxon was already deleted."));
906
    	    return result;
907
    	}
908
    	taxon = HibernateProxyHelper.deproxy(taxon);
909
    	Classification classification = HibernateProxyHelper.deproxy(classificationDao.load(classificationUuid), Classification.class);
910
        result = isDeletable(taxonUUID, config);
911

    
912
        if (result.isOk()){
913
            // --- DeleteSynonymRelations
914
            if (config.isDeleteSynonymRelations()){
915
                boolean removeSynonymNameFromHomotypicalGroup = false;
916
                // use tmp Set to avoid concurrent modification
917
                Set<Synonym> synsToDelete = new HashSet<>();
918
                synsToDelete.addAll(taxon.getSynonyms());
919
                for (Synonym synonym : synsToDelete){
920
                    taxon.removeSynonym(synonym, removeSynonymNameFromHomotypicalGroup);
921

    
922
                    // --- DeleteSynonymsIfPossible
923
                    if (config.isDeleteSynonymsIfPossible()){
924
                        //TODO which value
925
                        boolean newHomotypicGroupIfNeeded = true;
926
                        SynonymDeletionConfigurator synConfig = new SynonymDeletionConfigurator();
927
                        deleteSynonym(synonym, synConfig);
928
                    }
929
                }
930
            }
931

    
932
            // --- DeleteTaxonRelationships
933
            if (! config.isDeleteTaxonRelationships()){
934
                if (taxon.getTaxonRelations().size() > 0){
935
                    result.setAbort();
936
                    result.addException(new Exception("Taxon can't be deleted as it is related to another taxon. " +
937
                            "Remove taxon from all relations to other taxa prior to deletion."));
938

    
939
                }
940
            } else{
941
                TaxonDeletionConfigurator configRelTaxon = new TaxonDeletionConfigurator();
942
                configRelTaxon.setDeleteTaxonNodes(false);
943
                configRelTaxon.setDeleteConceptRelationships(true);
944

    
945
                for (TaxonRelationship taxRel: taxon.getTaxonRelations()){
946
                    if (config.isDeleteMisappliedNamesAndInvalidDesignations()){
947
                        if (taxRel.getType().equals(TaxonRelationshipType.MISAPPLIED_NAME_FOR()) || taxRel.getType().equals(TaxonRelationshipType.INVALID_DESIGNATION_FOR())){
948
                            if (taxon.equals(taxRel.getToTaxon())){
949
                                this.deleteTaxon(taxRel.getFromTaxon().getUuid(), config, classificationUuid);
950
                            }
951
                        }
952
                    } else if (config.isDeleteConceptRelationships() && taxRel.getType().isConceptRelationship()){
953

    
954
                        if (taxon.equals(taxRel.getToTaxon()) && isDeletable(taxRel.getFromTaxon().getUuid(), configRelTaxon).isOk()){
955
                            this.deleteTaxon(taxRel.getFromTaxon().getUuid(), configRelTaxon, classificationUuid);
956
                        }else if (isDeletable(taxRel.getToTaxon().getUuid(), configRelTaxon).isOk()){
957
                            this.deleteTaxon(taxRel.getToTaxon().getUuid(), configRelTaxon, classificationUuid);
958
                        }
959
                    }
960
                    taxon.removeTaxonRelation(taxRel);
961

    
962
                }
963
            }
964

    
965
            //    	TaxonDescription
966
            if (config.isDeleteDescriptions()){
967
                Set<TaxonDescription> descriptions = taxon.getDescriptions();
968
                List<TaxonDescription> removeDescriptions = new ArrayList<TaxonDescription>();
969
                for (TaxonDescription desc: descriptions){
970
                    //TODO use description delete configurator ?
971
                    //FIXME check if description is ALWAYS deletable
972
                    if (desc.getDescribedSpecimenOrObservation() != null){
973
                        result.setAbort();
974
                        result.addException(new Exception("Taxon can't be deleted as it is used in a TaxonDescription" +
975
                                " which also describes specimens or observations"));
976
                        break;
977
                    }
978
                    removeDescriptions.add(desc);
979

    
980

    
981
                }
982
                if (result.isOk()){
983
                    for (TaxonDescription desc: removeDescriptions){
984
                        taxon.removeDescription(desc);
985
                        descriptionService.delete(desc);
986
                    }
987
                } else {
988
                    return result;
989
                }
990
            }
991

    
992

    
993
         if (! config.isDeleteTaxonNodes() || (!config.isDeleteInAllClassifications() && classification == null && taxon.getTaxonNodes().size() > 1)){
994
             result.addException(new Exception( "Taxon can't be deleted as it is used in more than one classification."));
995
         }else{
996
             if (taxon.getTaxonNodes().size() != 0){
997
                Set<TaxonNode> nodes = taxon.getTaxonNodes();
998
                Iterator<TaxonNode> iterator = nodes.iterator();
999
                TaxonNode node = null;
1000
                boolean deleteChildren;
1001
                if (config.getTaxonNodeConfig().getChildHandling().equals(ChildHandling.DELETE)){
1002
                    deleteChildren = true;
1003
                }else {
1004
                    deleteChildren = false;
1005
                }
1006
                boolean success = true;
1007
                if (!config.isDeleteInAllClassifications() && !(classification == null)){
1008
                    while (iterator.hasNext()){
1009
                        node = iterator.next();
1010
                        if (node.getClassification().equals(classification)){
1011
                            break;
1012
                        }
1013
                        node = null;
1014
                    }
1015
                    if (node != null){
1016
                        HibernateProxyHelper.deproxy(node, TaxonNode.class);
1017
                        success =taxon.removeTaxonNode(node, deleteChildren);
1018
                        nodeService.delete(node);
1019
                    } else {
1020
                    	result.setError();
1021
                    	result.addException(new Exception("The taxon can not be deleted because it is not used in defined classification."));
1022
                    }
1023
                } else if (config.isDeleteInAllClassifications()){
1024
                    List<TaxonNode> nodesList = new ArrayList<TaxonNode>();
1025
                    nodesList.addAll(taxon.getTaxonNodes());
1026
                    for (ITaxonTreeNode treeNode: nodesList){
1027
                        TaxonNode taxonNode = (TaxonNode) treeNode;
1028
                        if(!deleteChildren){
1029
                            Object[] childNodes = taxonNode.getChildNodes().toArray();
1030
                            for (Object childNode: childNodes){
1031
                                TaxonNode childNodeCast = (TaxonNode) childNode;
1032
                                taxonNode.getParent().addChildNode(childNodeCast, childNodeCast.getReference(), childNodeCast.getMicroReference());
1033
                            }
1034
                        }
1035
                    }
1036
                    config.getTaxonNodeConfig().setDeleteElement(false);
1037
                    DeleteResult resultNodes = nodeService.deleteTaxonNodes(nodesList, config);
1038
                    if (!resultNodes.isOk()){
1039
                    	result.addExceptions(resultNodes.getExceptions());
1040
                    	result.setStatus(resultNodes.getStatus());
1041
                    } else {
1042
                        result.addUpdatedObjects(resultNodes.getUpdatedObjects());
1043
                    }
1044
                }
1045
                if (!success){
1046
                    result.setError();
1047
                    result.addException(new Exception("The taxon can not be deleted because the taxon node can not be removed."));
1048
                }
1049
            }
1050
         }
1051
         TaxonName name = taxon.getName();
1052
         taxon.setName(null);
1053
         this.saveOrUpdate(taxon);
1054

    
1055
         if ((taxon.getTaxonNodes() == null || taxon.getTaxonNodes().size()== 0)  && result.isOk()){
1056
             try{
1057
                 //taxon.setName(null);
1058
                 UUID uuid = dao.delete(taxon);
1059

    
1060
             }catch(Exception e){
1061
                 result.addException(e);
1062
                 result.setError();
1063

    
1064
             }
1065
         } else {
1066
             result.setError();
1067
             result.addException(new Exception("The Taxon can't be deleted because it is used in a classification."));
1068

    
1069
         }
1070
            //TaxonName
1071
        if (config.isDeleteNameIfPossible() && result.isOk()){
1072
           // name = HibernateProxyHelper.deproxy(name);
1073

    
1074
            DeleteResult nameResult = new DeleteResult();
1075
            //remove name if possible (and required)
1076
            if (name != null ){
1077
                nameResult = nameService.delete(name.getUuid(), config.getNameDeletionConfig());
1078
            }
1079
            if (nameResult.isError() || nameResult.isAbort()){
1080
                result.addRelatedObject(name);
1081
                result.addExceptions(nameResult.getExceptions());
1082
            }
1083

    
1084

    
1085
            }
1086

    
1087

    
1088

    
1089

    
1090

    
1091
        }
1092

    
1093
        return result;
1094

    
1095
    }
1096

    
1097
    private String checkForReferences(Taxon taxon){
1098
        Set<CdmBase> referencingObjects = genericDao.getReferencingObjects(taxon);
1099
        for (CdmBase referencingObject : referencingObjects){
1100
            //IIdentificationKeys (Media, Polytomous, MultiAccess)
1101
            if (HibernateProxyHelper.isInstanceOf(referencingObject, IIdentificationKey.class)){
1102
                String message = "Taxon" + taxon.getTitleCache() + "can't be deleted as it is used in an identification key. Remove from identification key prior to deleting this name";
1103

    
1104
                return message;
1105
            }
1106

    
1107

    
1108
           /* //PolytomousKeyNode
1109
            if (referencingObject.isInstanceOf(PolytomousKeyNode.class)){
1110
                String message = "Taxon" + taxon.getTitleCache() + " can't be deleted as it is used in polytomous key node";
1111
                return message;
1112
            }*/
1113

    
1114
            //TaxonInteraction
1115
            if (referencingObject.isInstanceOf(TaxonInteraction.class)){
1116
                String message = "Taxon can't be deleted as it is used in taxonInteraction#taxon2";
1117
                return message;
1118
            }
1119

    
1120
          //TaxonInteraction
1121
            if (referencingObject.isInstanceOf(DeterminationEvent.class)){
1122
                String message = "Taxon can't be deleted as it is used in a determination event";
1123
                return message;
1124
            }
1125

    
1126
        }
1127

    
1128
        referencingObjects = null;
1129
        return null;
1130
    }
1131

    
1132
    private boolean checkForPolytomousKeys(Taxon taxon){
1133
        boolean result = false;
1134
        List<CdmBase> list = genericDao.getCdmBasesByFieldAndClass(PolytomousKeyNode.class, "taxon", taxon, null);
1135
        if (!list.isEmpty()) {
1136
            result = true;
1137
        }
1138
        return result;
1139
    }
1140

    
1141
    @Override
1142
    @Transactional(readOnly = false)
1143
    public DeleteResult delete(UUID synUUID){
1144
    	DeleteResult result = new DeleteResult();
1145
    	Synonym syn = (Synonym)dao.load(synUUID);
1146

    
1147
        return this.deleteSynonym(syn, null);
1148
    }
1149

    
1150
    @Override
1151
    @Transactional(readOnly = false)
1152
    public DeleteResult deleteSynonym(UUID synonymUuid, SynonymDeletionConfigurator config) {
1153
        return deleteSynonym((Synonym)dao.load(synonymUuid), config);
1154

    
1155
    }
1156

    
1157

    
1158
    @Override
1159
    @Transactional(readOnly = false)
1160
    public DeleteResult deleteSynonym(Synonym synonym, SynonymDeletionConfigurator config) {
1161
        DeleteResult result = new DeleteResult();
1162
    	if (synonym == null){
1163
    		result.setAbort();
1164
    		result.addException(new Exception("The synonym was already deleted."));
1165
    		return result;
1166
        }
1167

    
1168
        if (config == null){
1169
            config = new SynonymDeletionConfigurator();
1170
        }
1171

    
1172
        result = isDeletable(synonym.getUuid(), config);
1173

    
1174

    
1175
        if (result.isOk()){
1176

    
1177
            synonym = HibernateProxyHelper.deproxy(this.load(synonym.getUuid()), Synonym.class);
1178

    
1179
            //remove synonym
1180
            Taxon accTaxon = synonym.getAcceptedTaxon();
1181

    
1182
            if (accTaxon != null){
1183
                accTaxon = HibernateProxyHelper.deproxy(accTaxon, Taxon.class);
1184
                accTaxon.removeSynonym(synonym, false);
1185
                this.saveOrUpdate(accTaxon);
1186
                result.addUpdatedObject(accTaxon);
1187
            }
1188
            this.saveOrUpdate(synonym);
1189
            //#6281
1190
            dao.flush();
1191

    
1192
            TaxonName name = synonym.getName();
1193
            synonym.setName(null);
1194

    
1195
            dao.delete(synonym);
1196

    
1197

    
1198
            //remove name if possible (and required)
1199
            if (name != null && config.isDeleteNameIfPossible()){
1200

    
1201
                    DeleteResult nameDeleteResult = nameService.delete(name, config.getNameDeletionConfig());
1202
                    if (nameDeleteResult.isAbort() || nameDeleteResult.isError()){
1203
                    	result.addExceptions(nameDeleteResult.getExceptions());
1204
                    	result.addRelatedObject(name);
1205
                    }
1206
            }
1207

    
1208
        }
1209
        return result;
1210
    }
1211

    
1212
    @Override
1213
    public List<TaxonName> findIdenticalTaxonNameIds(List<String> propertyPath) {
1214

    
1215
        return this.dao.findIdenticalNamesNew(propertyPath);
1216
    }
1217
//
1218
//    @Override
1219
//    public String getPhylumName(TaxonName name){
1220
//        return this.dao.getPhylumName(name);
1221
//    }
1222

    
1223
    @Override
1224
    public Taxon findBestMatchingTaxon(String taxonName) {
1225
        MatchingTaxonConfigurator config = MatchingTaxonConfigurator.NewInstance();
1226
        config.setTaxonNameTitle(taxonName);
1227
        return findBestMatchingTaxon(config);
1228
    }
1229

    
1230
    @Override
1231
    public Taxon findBestMatchingTaxon(MatchingTaxonConfigurator config) {
1232

    
1233
        Taxon bestCandidate = null;
1234
        try{
1235
            // 1. search for accepted taxa
1236
            List<TaxonBase> taxonList = dao.findByNameTitleCache(true, false, config.getTaxonNameTitle(), null, MatchMode.EXACT, null, null, 0, null, null);
1237
            boolean bestCandidateMatchesSecUuid = false;
1238
            boolean bestCandidateIsInClassification = false;
1239
            int countEqualCandidates = 0;
1240
            for(TaxonBase taxonBaseCandidate : taxonList){
1241
                if(taxonBaseCandidate instanceof Taxon){
1242
                    Taxon newCanditate = CdmBase.deproxy(taxonBaseCandidate, Taxon.class);
1243
                    boolean newCandidateMatchesSecUuid = isMatchesSecUuid(newCanditate, config);
1244
                    if (! newCandidateMatchesSecUuid && config.isOnlyMatchingSecUuid() ){
1245
                        continue;
1246
                    }else if(newCandidateMatchesSecUuid && ! bestCandidateMatchesSecUuid){
1247
                        bestCandidate = newCanditate;
1248
                        countEqualCandidates = 1;
1249
                        bestCandidateMatchesSecUuid = true;
1250
                        continue;
1251
                    }
1252

    
1253
                    boolean newCandidateInClassification = isInClassification(newCanditate, config);
1254
                    if (! newCandidateInClassification && config.isOnlyMatchingClassificationUuid()){
1255
                        continue;
1256
                    }else if (newCandidateInClassification && ! bestCandidateIsInClassification){
1257
                        bestCandidate = newCanditate;
1258
                        countEqualCandidates = 1;
1259
                        bestCandidateIsInClassification = true;
1260
                        continue;
1261
                    }
1262
                    if (bestCandidate == null){
1263
                        bestCandidate = newCanditate;
1264
                        countEqualCandidates = 1;
1265
                        continue;
1266
                    }
1267

    
1268
                }else{  //not Taxon.class
1269
                    continue;
1270
                }
1271
                countEqualCandidates++;
1272

    
1273
            }
1274
            if (bestCandidate != null){
1275
                if(countEqualCandidates > 1){
1276
                    logger.info(countEqualCandidates + " equally matching TaxonBases found, using first accepted Taxon: " + bestCandidate.getTitleCache());
1277
                    return bestCandidate;
1278
                } else {
1279
                    logger.info("using accepted Taxon: " + bestCandidate.getTitleCache());
1280
                    return bestCandidate;
1281
                }
1282
            }
1283

    
1284

    
1285
            // 2. search for synonyms
1286
            if (config.isIncludeSynonyms()){
1287
                List<TaxonBase> synonymList = dao.findByNameTitleCache(false, true, config.getTaxonNameTitle(), null, MatchMode.EXACT, null, null, 0, null, null);
1288
                for(TaxonBase taxonBase : synonymList){
1289
                    if(taxonBase instanceof Synonym){
1290
                        Synonym synonym = CdmBase.deproxy(taxonBase, Synonym.class);
1291
                        bestCandidate = synonym.getAcceptedTaxon();
1292
                        if(bestCandidate != null){
1293
                            logger.info("using accepted Taxon " +  bestCandidate.getTitleCache() + " for synonym " + taxonBase.getTitleCache());
1294
                            return bestCandidate;
1295
                        }
1296
                        //TODO extend method: search using treeUUID, using SecUUID, first find accepted then include synonyms until a matching taxon is found
1297
                    }
1298
                }
1299
            }
1300

    
1301
        } catch (Exception e){
1302
            logger.error(e);
1303
            e.printStackTrace();
1304
        }
1305

    
1306
        return bestCandidate;
1307
    }
1308

    
1309
    private boolean isInClassification(Taxon taxon, MatchingTaxonConfigurator config) {
1310
        UUID configClassificationUuid = config.getClassificationUuid();
1311
        if (configClassificationUuid == null){
1312
            return false;
1313
        }
1314
        for (TaxonNode node : taxon.getTaxonNodes()){
1315
            UUID classUuid = node.getClassification().getUuid();
1316
            if (configClassificationUuid.equals(classUuid)){
1317
                return true;
1318
            }
1319
        }
1320
        return false;
1321
    }
1322

    
1323
    private boolean isMatchesSecUuid(Taxon taxon, MatchingTaxonConfigurator config) {
1324
        UUID configSecUuid = config.getSecUuid();
1325
        if (configSecUuid == null){
1326
            return false;
1327
        }
1328
        UUID taxonSecUuid = (taxon.getSec() == null)? null : taxon.getSec().getUuid();
1329
        return configSecUuid.equals(taxonSecUuid);
1330
    }
1331

    
1332
    @Override
1333
    public Synonym findBestMatchingSynonym(String taxonName) {
1334
        List<TaxonBase> synonymList = dao.findByNameTitleCache(false, true, taxonName, null, MatchMode.EXACT, null, null, 0, null, null);
1335
        if(! synonymList.isEmpty()){
1336
            Synonym result = CdmBase.deproxy(synonymList.iterator().next(), Synonym.class);
1337
            if(synonymList.size() == 1){
1338
                logger.info(synonymList.size() + " Synonym found " + result.getTitleCache() );
1339
                return result;
1340
            } else {
1341
                logger.info("Several matching synonyms found. Using first: " +  result.getTitleCache());
1342
                return result;
1343
            }
1344
        }
1345
        return null;
1346
    }
1347

    
1348
    @Override
1349
    @Transactional(readOnly = false)
1350
    public UpdateResult moveSynonymToAnotherTaxon(Synonym oldSynonym,
1351
            Taxon newTaxon,
1352
            boolean moveHomotypicGroup,
1353
            SynonymType newSynonymType) throws HomotypicalGroupChangeException {
1354
        return moveSynonymToAnotherTaxon(oldSynonym, newTaxon, moveHomotypicGroup,
1355
                newSynonymType,
1356
                oldSynonym.getSec(),
1357
                oldSynonym.getSecMicroReference(),
1358
                true);
1359
    }
1360

    
1361
    @Override
1362
    @Transactional(readOnly = false)
1363
    public UpdateResult moveSynonymToAnotherTaxon(Synonym oldSynonym,
1364
            Taxon newTaxon,
1365
            boolean moveHomotypicGroup,
1366
            SynonymType newSynonymType,
1367
            Reference newSecundum,
1368
            String newSecundumDetail,
1369
            boolean keepSecundumIfUndefined) throws HomotypicalGroupChangeException {
1370

    
1371
        Synonym synonym = CdmBase.deproxy(dao.load(oldSynonym.getUuid()), Synonym.class);
1372
        Taxon oldTaxon = CdmBase.deproxy(dao.load(synonym.getAcceptedTaxon().getUuid()), Taxon.class);
1373
        //TODO what if there is no name ?? Concepts may be cached (e.g. via TCS import)
1374
        TaxonName synonymName = synonym.getName();
1375
        TaxonName fromTaxonName = oldTaxon.getName();
1376
        //set default relationship type
1377
        if (newSynonymType == null){
1378
            newSynonymType = SynonymType.HETEROTYPIC_SYNONYM_OF();
1379
        }
1380
        boolean newRelTypeIsHomotypic = newSynonymType.equals(SynonymType.HOMOTYPIC_SYNONYM_OF());
1381

    
1382
        HomotypicalGroup homotypicGroup = synonymName.getHomotypicalGroup();
1383
        int hgSize = homotypicGroup.getTypifiedNames().size();
1384
        boolean isSingleInGroup = !(hgSize > 1);
1385

    
1386
        if (! isSingleInGroup){
1387
            boolean isHomotypicToAccepted = synonymName.isHomotypic(fromTaxonName);
1388
            boolean hasHomotypicSynonymRelatives = isHomotypicToAccepted ? hgSize > 2 : hgSize > 1;
1389
            if (isHomotypicToAccepted){
1390
                String message = "Synonym is in homotypic group with accepted taxon%s. First remove synonym from homotypic group of accepted taxon before moving to other taxon.";
1391
                String homotypicRelatives = hasHomotypicSynonymRelatives ? " and other synonym(s)":"";
1392
                message = String.format(message, homotypicRelatives);
1393
                throw new HomotypicalGroupChangeException(message);
1394
            }
1395
            if (! moveHomotypicGroup){
1396
                String message = "Synonym is in homotypic group with other synonym(s). Either move complete homotypic group or remove synonym from homotypic group prior to moving to other taxon.";
1397
                throw new HomotypicalGroupChangeException(message);
1398
            }
1399
        }else{
1400
            moveHomotypicGroup = true;  //single synonym always allows to moveCompleteGroup
1401
        }
1402
//        Assert.assertTrue("Synonym can only be moved with complete homotypic group", moveHomotypicGroup);
1403

    
1404
        UpdateResult result = new UpdateResult();
1405
        //move all synonyms to new taxon
1406
        List<Synonym> homotypicSynonyms = oldTaxon.getSynonymsInGroup(homotypicGroup);
1407
        for (Synonym synRelation: homotypicSynonyms){
1408

    
1409
            newTaxon = HibernateProxyHelper.deproxy(newTaxon, Taxon.class);
1410
            oldTaxon = HibernateProxyHelper.deproxy(oldTaxon, Taxon.class);
1411
            newTaxon.addSynonym(synRelation, newSynonymType);
1412
            oldTaxon.removeSynonym(synRelation, false);
1413
            if (newSecundum != null || !keepSecundumIfUndefined){
1414
                synRelation.setSec(newSecundum);
1415
            }
1416
            if (newSecundumDetail != null || !keepSecundumIfUndefined){
1417
                synRelation.setSecMicroReference(newSecundumDetail);
1418
            }
1419

    
1420
            //set result  //why is this needed? Seems wrong to me (AM 10.10.2016)
1421
            if (!synRelation.equals(oldSynonym)){
1422
                result.setError();
1423
            }
1424
        }
1425

    
1426
        result.addUpdatedObject(oldTaxon);
1427
        result.addUpdatedObject(newTaxon);
1428
        saveOrUpdate(oldTaxon);
1429
        saveOrUpdate(newTaxon);
1430

    
1431
        return result;
1432
    }
1433

    
1434
    @Override
1435
    public <T extends TaxonBase> List<UuidAndTitleCache<T>> getUuidAndTitleCache(Class<T> clazz, Integer limit, String pattern) {
1436
        return dao.getUuidAndTitleCache(clazz, limit, pattern);
1437
    }
1438

    
1439
    @Override
1440
    public Pager<SearchResult<TaxonBase>> findByFullText(
1441
            Class<? extends TaxonBase> clazz, String queryString,
1442
            Classification classification, List<Language> languages,
1443
            boolean highlightFragments, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException {
1444

    
1445

    
1446
        LuceneSearch luceneSearch = prepareFindByFullTextSearch(clazz, queryString, classification, languages, highlightFragments, null);
1447

    
1448
        // --- execute search
1449
        TopGroups<BytesRef> topDocsResultSet;
1450
        try {
1451
            topDocsResultSet = luceneSearch.executeSearch(pageSize, pageNumber);
1452
        } catch (ParseException e) {
1453
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
1454
            luceneParseException.setStackTrace(e.getStackTrace());
1455
            throw luceneParseException;
1456
        }
1457

    
1458
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
1459
        idFieldMap.put(CdmBaseType.TAXON, "id");
1460

    
1461
        // ---  initialize taxa, thighlight matches ....
1462
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
1463
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
1464
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
1465

    
1466
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
1467
        return new DefaultPagerImpl<SearchResult<TaxonBase>>(pageNumber, totalHits, pageSize, searchResults);
1468
    }
1469

    
1470
    @Override
1471
    public Pager<SearchResult<TaxonBase>> findByDistribution(List<NamedArea> areaFilter, List<PresenceAbsenceTerm> statusFilter,
1472
            Classification classification,
1473
            Integer pageSize, Integer pageNumber,
1474
            List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException {
1475

    
1476
        LuceneSearch luceneSearch = prepareByDistributionSearch(areaFilter, statusFilter, classification);
1477

    
1478
        // --- execute search
1479
        TopGroups<BytesRef> topDocsResultSet;
1480
        try {
1481
            topDocsResultSet = luceneSearch.executeSearch(pageSize, pageNumber);
1482
        } catch (ParseException e) {
1483
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
1484
            luceneParseException.setStackTrace(e.getStackTrace());
1485
            throw luceneParseException;
1486
        }
1487

    
1488
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
1489
        idFieldMap.put(CdmBaseType.TAXON, "id");
1490

    
1491
        // ---  initialize taxa, thighlight matches ....
1492
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
1493
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
1494
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
1495

    
1496
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
1497
        return new DefaultPagerImpl<SearchResult<TaxonBase>>(pageNumber, totalHits, pageSize, searchResults);
1498
    }
1499

    
1500
    /**
1501
     * @param clazz
1502
     * @param queryString
1503
     * @param classification
1504
     * @param languages
1505
     * @param highlightFragments
1506
     * @param sortFields TODO
1507
     * @param directorySelectClass
1508
     * @return
1509
     */
1510
    protected LuceneSearch prepareFindByFullTextSearch(Class<? extends CdmBase> clazz, String queryString, Classification classification, List<Language> languages,
1511
            boolean highlightFragments, SortField[] sortFields) {
1512
        Builder finalQueryBuilder = new Builder();
1513
        Builder textQueryBuilder = new Builder();
1514

    
1515
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, TaxonBase.class);
1516
        QueryFactory taxonBaseQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(TaxonBase.class);
1517

    
1518
        if(sortFields == null){
1519
            sortFields = new  SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING,  false)};
1520
        }
1521
        luceneSearch.setSortFields(sortFields);
1522

    
1523
        // ---- search criteria
1524
        luceneSearch.setCdmTypRestriction(clazz);
1525

    
1526
        if(!queryString.isEmpty() && !queryString.equals("*") && !queryString.equals("?") ) {
1527
            textQueryBuilder.add(taxonBaseQueryFactory.newTermQuery("titleCache", queryString), Occur.SHOULD);
1528
            textQueryBuilder.add(taxonBaseQueryFactory.newDefinedTermQuery("name.rank", queryString, languages), Occur.SHOULD);
1529
        }
1530

    
1531
        BooleanQuery textQuery = textQueryBuilder.build();
1532
        if(textQuery.clauses().size() > 0) {
1533
            finalQueryBuilder.add(textQuery, Occur.MUST);
1534
        }
1535

    
1536

    
1537
        if(classification != null){
1538
            finalQueryBuilder.add(taxonBaseQueryFactory.newEntityIdQuery("taxonNodes.classification.id", classification), Occur.MUST);
1539
        }
1540
        luceneSearch.setQuery(finalQueryBuilder.build());
1541

    
1542
        if(highlightFragments){
1543
            luceneSearch.setHighlightFields(taxonBaseQueryFactory.getTextFieldNamesAsArray());
1544
        }
1545
        return luceneSearch;
1546
    }
1547

    
1548
    /**
1549
     * Uses org.apache.lucene.search.join.JoinUtil for query time joining, alternatively
1550
     * the BlockJoinQuery could be used. The latter might be more memory save but has the
1551
     * drawback of requiring to do the join an indexing time.
1552
     * see  http://dev.e-taxonomy.eu/trac/wiki/LuceneNotes#JoinsinLucene for more information on this.
1553
     *
1554
     * Joins TaxonRelationShip with Taxon depending on the direction of the given edge:
1555
     * <ul>
1556
     * <li>direct, everted: {@link Direction.relatedTo}: TaxonRelationShip.relatedTo.id --&gt; Taxon.id </li>
1557
     * <li>inverse: {@link Direction.relatedFrom}:  TaxonRelationShip.relatedFrom.id --&gt; Taxon.id </li>
1558
     * <ul>
1559
     * @param queryString
1560
     * @param classification
1561
     * @param languages
1562
     * @param highlightFragments
1563
     * @param sortFields TODO
1564
     *
1565
     * @return
1566
     * @throws IOException
1567
     */
1568
    protected LuceneSearch prepareFindByTaxonRelationFullTextSearch(TaxonRelationshipEdge edge, String queryString, Classification classification, List<Language> languages,
1569
            boolean highlightFragments, SortField[] sortFields) throws IOException {
1570

    
1571
        String fromField;
1572
        String queryTermField;
1573
        String toField = "id"; // TaxonBase.uuid
1574

    
1575
        if(edge.isBidirectional()){
1576
            throw new RuntimeException("Bidirectional joining not supported!");
1577
        }
1578
        if(edge.isEvers()){
1579
            fromField = "relatedFrom.id";
1580
            queryTermField = "relatedFrom.titleCache";
1581
        } else if(edge.isInvers()) {
1582
            fromField = "relatedTo.id";
1583
            queryTermField = "relatedTo.titleCache";
1584
        } else {
1585
            throw new RuntimeException("Invalid direction: " + edge.getDirections());
1586
        }
1587

    
1588
        Builder finalQueryBuilder = new Builder();
1589

    
1590
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, TaxonBase.class);
1591
        QueryFactory taxonBaseQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(TaxonBase.class);
1592

    
1593
        Builder joinFromQueryBuilder = new Builder();
1594
        joinFromQueryBuilder.add(taxonBaseQueryFactory.newTermQuery(queryTermField, queryString), Occur.MUST);
1595
        joinFromQueryBuilder.add(taxonBaseQueryFactory.newEntityIdQuery("type.id", edge.getTaxonRelationshipType()), Occur.MUST);
1596
        Query joinQuery = taxonBaseQueryFactory.newJoinQuery(TaxonRelationship.class, fromField, false, joinFromQueryBuilder.build(), toField, null, ScoreMode.Max);
1597

    
1598
        if(sortFields == null){
1599
            sortFields = new  SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING,  false)};
1600
        }
1601
        luceneSearch.setSortFields(sortFields);
1602

    
1603
        finalQueryBuilder.add(joinQuery, Occur.MUST);
1604

    
1605
        if(classification != null){
1606
            finalQueryBuilder.add(taxonBaseQueryFactory.newEntityIdQuery("taxonNodes.classification.id", classification), Occur.MUST);
1607
        }
1608
        luceneSearch.setQuery(finalQueryBuilder.build());
1609

    
1610
        if(highlightFragments){
1611
            luceneSearch.setHighlightFields(taxonBaseQueryFactory.getTextFieldNamesAsArray());
1612
        }
1613
        return luceneSearch;
1614
    }
1615

    
1616
    @Override
1617
    public Pager<SearchResult<TaxonBase>> findTaxaAndNamesByFullText(
1618
            EnumSet<TaxaAndNamesSearchMode> searchModes, String queryString, Classification classification,
1619
            Set<NamedArea> namedAreas, Set<PresenceAbsenceTerm> distributionStatus, List<Language> languages,
1620
            boolean highlightFragments, Integer pageSize,
1621
            Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths)
1622
            throws IOException, LuceneParseException, LuceneMultiSearchException {
1623

    
1624
        // FIXME: allow taxonomic ordering
1625
        //  hql equivalent:  order by t.name.genusOrUninomial, case when t.name.specificEpithet like '\"%\"' then 1 else 0 end, t.name.specificEpithet, t.name.rank desc, t.name.nameCache";
1626
        // this require building a special sort column by a special classBridge
1627
        if(highlightFragments){
1628
            logger.warn("findTaxaAndNamesByFullText() : fragment highlighting is " +
1629
                    "currently not fully supported by this method and thus " +
1630
                    "may not work with common names and misapplied names.");
1631
        }
1632

    
1633
        // convert sets to lists
1634
        List<NamedArea> namedAreaList = null;
1635
        List<PresenceAbsenceTerm>distributionStatusList = null;
1636
        if(namedAreas != null){
1637
            namedAreaList = new ArrayList<NamedArea>(namedAreas.size());
1638
            namedAreaList.addAll(namedAreas);
1639
        }
1640
        if(distributionStatus != null){
1641
            distributionStatusList = new ArrayList<PresenceAbsenceTerm>(distributionStatus.size());
1642
            distributionStatusList.addAll(distributionStatus);
1643
        }
1644

    
1645
        // set default if parameter is null
1646
        if(searchModes == null){
1647
            searchModes = EnumSet.of(TaxaAndNamesSearchMode.doTaxa);
1648
        }
1649

    
1650
        // set sort order and thus override any sort orders which may have been
1651
        // defined by prepare*Search methods
1652
        if(orderHints == null){
1653
            orderHints = OrderHint.NOMENCLATURAL_SORT_ORDER.asList();
1654
        }
1655
        SortField[] sortFields = new SortField[orderHints.size()];
1656
        int i = 0;
1657
        for(OrderHint oh : orderHints){
1658
            sortFields[i++] = oh.toSortField();
1659
        }
1660
//        SortField[] sortFields = new SortField[]{SortField.FIELD_SCORE, new SortField("id", SortField.STRING, false)};
1661
//        SortField[] sortFields = new SortField[]{new SortField(NomenclaturalSortOrderBrigde.NAME_SORT_FIELD_NAME, SortField.STRING, false)};
1662

    
1663

    
1664
        boolean addDistributionFilter = namedAreas != null && namedAreas.size() > 0;
1665

    
1666
        List<LuceneSearch> luceneSearches = new ArrayList<LuceneSearch>();
1667
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
1668

    
1669
        /*
1670
          ======== filtering by distribution , HOWTO ========
1671

    
1672
           - http://www.javaranch.com/journal/2009/02/filtering-a-lucene-search.html
1673
           - http://stackoverflow.com/questions/17709256/lucene-solr-using-complex-filters -> QueryWrapperFilter
1674
          add Filter to search as http://lucene.apache.org/core/3_6_0/api/all/org/apache/lucene/search/Filter.html
1675
          which will be put into a FilteredQuersy  in the end ?
1676

    
1677

    
1678
          3. how does it work in spatial?
1679
          see
1680
           - http://www.nsshutdown.com/projects/lucene/whitepaper/locallucene_v2.html
1681
           - http://www.infoq.com/articles/LuceneSpatialSupport
1682
           - http://www.mhaller.de/archives/156-Spatial-search-with-Lucene.html
1683
          ------------------------------------------------------------------------
1684

    
1685
          filter strategies:
1686
          A) use a separate distribution filter per index sub-query/search:
1687
           - byTaxonSyonym (query TaxaonBase):
1688
               use a join area filter (Distribution -> TaxonBase)
1689
           - byCommonName (query DescriptionElementBase): use an area filter on
1690
               DescriptionElementBase !!! PROBLEM !!!
1691
               This cannot work since the distributions are different entities than the
1692
               common names and thus these are different lucene documents.
1693
           - byMisaplliedNames (join query TaxonRelationship -> TaxaonBase):
1694
               use a join area filter (Distribution -> TaxonBase)
1695

    
1696
          B) use a common distribution filter for all index sub-query/searches:
1697
           - use a common join area filter (Distribution -> TaxonBase)
1698
           - also implement the byCommonName as join query (CommonName -> TaxonBase)
1699
           PROBLEM in this case: we are losing the fragment highlighting for the
1700
           common names, since the returned documents are always TaxonBases
1701
        */
1702

    
1703
        /* The QueryFactory for creating filter queries on Distributions should
1704
         * The query factory used for the common names query cannot be reused
1705
         * for this case, since we want to only record the text fields which are
1706
         * actually used in the primary query
1707
         */
1708
        QueryFactory distributionFilterQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(Distribution.class);
1709

    
1710
        Builder multiIndexByAreaFilterBuilder = new Builder();
1711

    
1712
        // search for taxa or synonyms
1713
        if(searchModes.contains(TaxaAndNamesSearchMode.doTaxa) || searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)) {
1714
            Class taxonBaseSubclass = TaxonBase.class;
1715
            if(searchModes.contains(TaxaAndNamesSearchMode.doTaxa) && !searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)){
1716
                taxonBaseSubclass = Taxon.class;
1717
            } else if (!searchModes.contains(TaxaAndNamesSearchMode.doTaxa) && searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)) {
1718
                taxonBaseSubclass = Synonym.class;
1719
            }
1720
            luceneSearches.add(prepareFindByFullTextSearch(taxonBaseSubclass, queryString, classification, languages, highlightFragments, sortFields));
1721
            idFieldMap.put(CdmBaseType.TAXON, "id");
1722
            /* A) does not work!!!!
1723
            if(addDistributionFilter){
1724
                // in this case we need a filter which uses a join query
1725
                // to get the TaxonBase documents for the DescriptionElementBase documents
1726
                // which are matching the areas in question
1727
                Query taxonAreaJoinQuery = createByDistributionJoinQuery(
1728
                        namedAreaList,
1729
                        distributionStatusList,
1730
                        distributionFilterQueryFactory
1731
                        );
1732
                multiIndexByAreaFilter.add(new QueryWrapperFilter(taxonAreaJoinQuery), Occur.SHOULD);
1733
            }
1734
            */
1735
            if(addDistributionFilter && searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)){
1736
                // add additional area filter for synonyms
1737
                String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
1738
                String toField = "accTaxon.id"; // id in TaxonBase index (is multivalued)
1739

    
1740
                //TODO replace by createByDistributionJoinQuery
1741
                BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, distributionFilterQueryFactory);
1742
                Query taxonAreaJoinQuery = distributionFilterQueryFactory.newJoinQuery(Distribution.class, fromField, true, byDistributionQuery, toField, Taxon.class, ScoreMode.None);
1743
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
1744

    
1745
            }
1746
        }
1747

    
1748
        // search by CommonTaxonName
1749
        if(searchModes.contains(TaxaAndNamesSearchMode.doTaxaByCommonNames)) {
1750
            // B)
1751
            QueryFactory descriptionElementQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(DescriptionElementBase.class);
1752
            Query byCommonNameJoinQuery = descriptionElementQueryFactory.newJoinQuery(
1753
                    CommonTaxonName.class,
1754
                    "inDescription.taxon.id",
1755
                    true,
1756
                    QueryFactory.addTypeRestriction(
1757
                                createByDescriptionElementFullTextQuery(queryString, classification, null, languages, descriptionElementQueryFactory)
1758
                                , CommonTaxonName.class
1759
                                ).build(), "id", null, ScoreMode.Max);
1760
            logger.debug("byCommonNameJoinQuery: " + byCommonNameJoinQuery.toString());
1761
            LuceneSearch byCommonNameSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, Taxon.class);
1762
            byCommonNameSearch.setCdmTypRestriction(Taxon.class);
1763
            byCommonNameSearch.setQuery(byCommonNameJoinQuery);
1764
            byCommonNameSearch.setSortFields(sortFields);
1765

    
1766
            DuplicateFilter df = new DuplicateFilter("inDescription.taxon.id");
1767
            Set<String> results=new HashSet<>();
1768
//            ScoreDoc[] hits = searcher.search(tq,df, 1000).scoreDocs;
1769
//
1770
//            byCommonNameSearch.setFilter(df);
1771
            idFieldMap.put(CdmBaseType.TAXON, "id");
1772

    
1773
            luceneSearches.add(byCommonNameSearch);
1774

    
1775
            /* A) does not work!!!!
1776
            luceneSearches.add(
1777
                    prepareByDescriptionElementFullTextSearch(CommonTaxonName.class,
1778
                            queryString, classification, null, languages, highlightFragments)
1779
                        );
1780
            idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
1781
            if(addDistributionFilter){
1782
                // in this case we are able to use DescriptionElementBase documents
1783
                // which are matching the areas in question directly
1784
                BooleanQuery byDistributionQuery = createByDistributionQuery(
1785
                        namedAreaList,
1786
                        distributionStatusList,
1787
                        distributionFilterQueryFactory
1788
                        );
1789
                multiIndexByAreaFilter.add(new QueryWrapperFilter(byDistributionQuery), Occur.SHOULD);
1790
            } */
1791
        }
1792

    
1793
        // search by misapplied names
1794
        if(searchModes.contains(TaxaAndNamesSearchMode.doMisappliedNames)) {
1795
            // NOTE:
1796
            // prepareFindByTaxonRelationFullTextSearch() is making use of JoinUtil.createJoinQuery()
1797
            // which allows doing query time joins
1798
            // finds the misapplied name (Taxon B) which is an misapplication for
1799
            // a related Taxon A.
1800
            //
1801
            luceneSearches.add(prepareFindByTaxonRelationFullTextSearch(
1802
                    new TaxonRelationshipEdge(TaxonRelationshipType.MISAPPLIED_NAME_FOR(), Direction.relatedTo),
1803
                    queryString, classification, languages, highlightFragments, sortFields));
1804
            idFieldMap.put(CdmBaseType.TAXON, "id");
1805

    
1806
            if(addDistributionFilter){
1807
                String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
1808

    
1809
                /*
1810
                 * Here i was facing wired and nasty bug which took me bugging be really for hours until I found this solution.
1811
                 * Maybe this is a bug in java itself java.
1812
                 *
1813
                 * When the string toField is constructed by using the expression TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString()
1814
                 * directly:
1815
                 *
1816
                 *    String toField = "relation." + TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString() +".to.id";
1817
                 *
1818
                 * The byDistributionQuery fails, however when the uuid is first stored in another string variable the query
1819
                 * will execute as expected:
1820
                 *
1821
                 *    String misappliedNameForUuid = TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString();
1822
                 *    String toField = "relation." + misappliedNameForUuid +".to.id";
1823
                 *
1824
                 * Comparing both strings by the String.equals method returns true, so both String are identical.
1825
                 *
1826
                 * The bug occurs when running eu.etaxonomy.cdm.api.service.TaxonServiceSearchTest in eclipse and in maven and seems to to be
1827
                 * dependent from a specific jvm (openjdk6  6b27-1.12.6-1ubuntu0.13.04.2, openjdk7 7u25-2.3.10-1ubuntu0.13.04.2,  oracle jdk1.7.0_25 tested)
1828
                 * The bug is persistent after a reboot of the development computer.
1829
                 */
1830
//                String misappliedNameForUuid = TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString();
1831
//                String toField = "relation." + misappliedNameForUuid +".to.id";
1832
                String toField = "relation.1ed87175-59dd-437e-959e-0d71583d8417.to.id";
1833
//                System.out.println("relation.1ed87175-59dd-437e-959e-0d71583d8417.to.id".equals("relation." + misappliedNameForUuid +".to.id") ? " > identical" : " > different");
1834
//                System.out.println("relation.1ed87175-59dd-437e-959e-0d71583d8417.to.id".equals("relation." + TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString() +".to.id") ? " > identical" : " > different");
1835

    
1836
                //TODO replace by createByDistributionJoinQuery
1837
                BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, distributionFilterQueryFactory);
1838
                Query taxonAreaJoinQuery = distributionFilterQueryFactory.newJoinQuery(Distribution.class, fromField, true, byDistributionQuery, toField, null, ScoreMode.None);
1839

    
1840
//                debug code for bug described above
1841
                //does not compile anymore since changing from lucene 3.6.2 to lucene 4.10+
1842
//                DocIdSet filterMatchSet = filter.getDocIdSet(luceneIndexToolProvider.getIndexReaderFor(Taxon.class));
1843
//                System.err.println(DocIdBitSetPrinter.docsAsString(filterMatchSet, 100));
1844

    
1845
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
1846
            }
1847
        }
1848

    
1849
        LuceneMultiSearch multiSearch = new LuceneMultiSearch(luceneIndexToolProvider,
1850
                luceneSearches.toArray(new LuceneSearch[luceneSearches.size()]));
1851

    
1852

    
1853
        if(addDistributionFilter){
1854

    
1855
            // B)
1856
            // in this case we need a filter which uses a join query
1857
            // to get the TaxonBase documents for the DescriptionElementBase documents
1858
            // which are matching the areas in question
1859
            //
1860
            // for toTaxa, doByCommonName
1861
            Query taxonAreaJoinQuery = createByDistributionJoinQuery(
1862
                    namedAreaList,
1863
                    distributionStatusList,
1864
                    distributionFilterQueryFactory,
1865
                    Taxon.class, true
1866
                    );
1867
            multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
1868
        }
1869

    
1870
        if (addDistributionFilter){
1871
            multiSearch.setFilter(multiIndexByAreaFilterBuilder.build());
1872
        }
1873

    
1874

    
1875
        // --- execute search
1876
        TopGroups<BytesRef> topDocsResultSet;
1877
        try {
1878
            topDocsResultSet = multiSearch.executeSearch(pageSize, pageNumber);
1879
        } catch (ParseException e) {
1880
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
1881
            luceneParseException.setStackTrace(e.getStackTrace());
1882
            throw luceneParseException;
1883
        }
1884

    
1885
        // --- initialize taxa, highlight matches ....
1886
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(multiSearch, multiSearch.getQuery());
1887

    
1888

    
1889
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
1890
                topDocsResultSet, multiSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
1891

    
1892
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
1893
        return new DefaultPagerImpl<SearchResult<TaxonBase>>(pageNumber, totalHits, pageSize, searchResults);
1894
    }
1895

    
1896
    /**
1897
     * @param namedAreaList at least one area must be in the list
1898
     * @param distributionStatusList optional
1899
     * @param toType toType
1900
     *      Optional parameter. Only used for debugging to print the toType documents
1901
     * @param asFilter TODO
1902
     * @return
1903
     * @throws IOException
1904
     */
1905
    protected Query createByDistributionJoinQuery(
1906
            List<NamedArea> namedAreaList,
1907
            List<PresenceAbsenceTerm> distributionStatusList,
1908
            QueryFactory queryFactory, Class<? extends CdmBase> toType, boolean asFilter
1909
            ) throws IOException {
1910

    
1911
        String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
1912
        String toField = "id"; // id in toType usually this is the TaxonBase index
1913

    
1914
        BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, queryFactory);
1915

    
1916
        ScoreMode scoreMode = asFilter ?  ScoreMode.None : ScoreMode.Max;
1917

    
1918
        Query taxonAreaJoinQuery = queryFactory.newJoinQuery(Distribution.class, fromField, false, byDistributionQuery, toField, toType, scoreMode);
1919

    
1920
        return taxonAreaJoinQuery;
1921
    }
1922

    
1923
    /**
1924
     * @param namedAreaList
1925
     * @param distributionStatusList
1926
     * @param queryFactory
1927
     * @return
1928
     */
1929
    private BooleanQuery createByDistributionQuery(List<NamedArea> namedAreaList,
1930
            List<PresenceAbsenceTerm> distributionStatusList, QueryFactory queryFactory) {
1931
        Builder areaQueryBuilder = new Builder();
1932
        // area field from Distribution
1933
        areaQueryBuilder.add(queryFactory.newEntityIdsQuery("area.id", namedAreaList), Occur.MUST);
1934

    
1935
        // status field from Distribution
1936
        if(distributionStatusList != null && distributionStatusList.size() > 0){
1937
            areaQueryBuilder.add(queryFactory.newEntityIdsQuery("status.id", distributionStatusList), Occur.MUST);
1938
        }
1939

    
1940
        BooleanQuery areaQuery = areaQueryBuilder.build();
1941
        logger.debug("createByDistributionQuery() query: " + areaQuery.toString());
1942
        return areaQuery;
1943
    }
1944

    
1945
    /**
1946
     * This method has been primarily created for testing the area join query but might
1947
     * also be useful in other situations
1948
     *
1949
     * @param namedAreaList
1950
     * @param distributionStatusList
1951
     * @param classification
1952
     * @param highlightFragments
1953
     * @return
1954
     * @throws IOException
1955
     */
1956
    protected LuceneSearch prepareByDistributionSearch(
1957
            List<NamedArea> namedAreaList, List<PresenceAbsenceTerm> distributionStatusList,
1958
            Classification classification) throws IOException {
1959

    
1960
        Builder finalQueryBuilder = new Builder();
1961

    
1962
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, Taxon.class);
1963

    
1964
        // FIXME is this query factory using the wrong type?
1965
        QueryFactory taxonQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(Taxon.class);
1966

    
1967
        SortField[] sortFields = new  SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING, false)};
1968
        luceneSearch.setSortFields(sortFields);
1969

    
1970

    
1971
        Query byAreaQuery = createByDistributionJoinQuery(namedAreaList, distributionStatusList, taxonQueryFactory, null, false);
1972

    
1973
        finalQueryBuilder.add(byAreaQuery, Occur.MUST);
1974

    
1975
        if(classification != null){
1976
            finalQueryBuilder.add(taxonQueryFactory.newEntityIdQuery("taxonNodes.classification.id", classification), Occur.MUST);
1977
        }
1978
        BooleanQuery finalQuery = finalQueryBuilder.build();
1979
        logger.info("prepareByAreaSearch() query: " + finalQuery.toString());
1980
        luceneSearch.setQuery(finalQuery);
1981

    
1982
        return luceneSearch;
1983
    }
1984

    
1985
    @Override
1986
    public Pager<SearchResult<TaxonBase>> findByDescriptionElementFullText(
1987
            Class<? extends DescriptionElementBase> clazz, String queryString,
1988
            Classification classification, List<Feature> features, List<Language> languages,
1989
            boolean highlightFragments, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException {
1990

    
1991

    
1992
        LuceneSearch luceneSearch = prepareByDescriptionElementFullTextSearch(clazz, queryString, classification, features, languages, highlightFragments);
1993

    
1994
        // --- execute search
1995
        TopGroups<BytesRef> topDocsResultSet;
1996
        try {
1997
            topDocsResultSet = luceneSearch.executeSearch(pageSize, pageNumber);
1998
        } catch (ParseException e) {
1999
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
2000
            luceneParseException.setStackTrace(e.getStackTrace());
2001
            throw luceneParseException;
2002
        }
2003

    
2004
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
2005
        idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
2006

    
2007
        // --- initialize taxa, highlight matches ....
2008
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
2009
        @SuppressWarnings("rawtypes")
2010
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
2011
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
2012

    
2013
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
2014
        return new DefaultPagerImpl<SearchResult<TaxonBase>>(pageNumber, totalHits, pageSize, searchResults);
2015

    
2016
    }
2017

    
2018

    
2019
    @Override
2020
    public Pager<SearchResult<TaxonBase>> findByEverythingFullText(String queryString,
2021
            Classification classification, List<Language> languages, boolean highlightFragments,
2022
            Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException, LuceneMultiSearchException {
2023

    
2024
        LuceneSearch luceneSearchByDescriptionElement = prepareByDescriptionElementFullTextSearch(null, queryString, classification, null, languages, highlightFragments);
2025
        LuceneSearch luceneSearchByTaxonBase = prepareFindByFullTextSearch(null, queryString, classification, languages, highlightFragments, null);
2026

    
2027
        LuceneMultiSearch multiSearch = new LuceneMultiSearch(luceneIndexToolProvider, luceneSearchByDescriptionElement, luceneSearchByTaxonBase);
2028

    
2029
        // --- execute search
2030
        TopGroups<BytesRef> topDocsResultSet;
2031
        try {
2032
            topDocsResultSet = multiSearch.executeSearch(pageSize, pageNumber);
2033
        } catch (ParseException e) {
2034
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
2035
            luceneParseException.setStackTrace(e.getStackTrace());
2036
            throw luceneParseException;
2037
        }
2038

    
2039
        // --- initialize taxa, highlight matches ....
2040
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(multiSearch, multiSearch.getQuery());
2041

    
2042
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
2043
        idFieldMap.put(CdmBaseType.TAXON, "id");
2044
        idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
2045

    
2046
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
2047
                topDocsResultSet, multiSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
2048

    
2049
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
2050
        return new DefaultPagerImpl<SearchResult<TaxonBase>>(pageNumber, totalHits, pageSize, searchResults);
2051

    
2052
    }
2053

    
2054

    
2055
    /**
2056
     * @param clazz
2057
     * @param queryString
2058
     * @param classification
2059
     * @param features
2060
     * @param languages
2061
     * @param highlightFragments
2062
     * @param directorySelectClass
2063
     * @return
2064
     */
2065
    protected LuceneSearch prepareByDescriptionElementFullTextSearch(Class<? extends CdmBase> clazz,
2066
            String queryString, Classification classification, List<Feature> features,
2067
            List<Language> languages, boolean highlightFragments) {
2068

    
2069
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, DescriptionElementBase.class);
2070
        QueryFactory descriptionElementQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(DescriptionElementBase.class);
2071

    
2072
        SortField[] sortFields = new  SortField[]{SortField.FIELD_SCORE, new SortField("inDescription.taxon.titleCache__sort", SortField.Type.STRING, false)};
2073

    
2074
        BooleanQuery finalQuery = createByDescriptionElementFullTextQuery(queryString, classification, features,
2075
                languages, descriptionElementQueryFactory);
2076

    
2077
        luceneSearch.setSortFields(sortFields);
2078
        luceneSearch.setCdmTypRestriction(clazz);
2079
        luceneSearch.setQuery(finalQuery);
2080
        if(highlightFragments){
2081
            luceneSearch.setHighlightFields(descriptionElementQueryFactory.getTextFieldNamesAsArray());
2082
        }
2083

    
2084
        return luceneSearch;
2085
    }
2086

    
2087
    /**
2088
     * @param queryString
2089
     * @param classification
2090
     * @param features
2091
     * @param languages
2092
     * @param descriptionElementQueryFactory
2093
     * @return
2094
     */
2095
    private BooleanQuery createByDescriptionElementFullTextQuery(String queryString, Classification classification,
2096
            List<Feature> features, List<Language> languages, QueryFactory descriptionElementQueryFactory) {
2097
        Builder finalQueryBuilder = new Builder();
2098
        Builder textQueryBuilder = new Builder();
2099
        textQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("titleCache", queryString), Occur.SHOULD);
2100

    
2101
        // common name
2102
        Builder nameQueryBuilder = new Builder();
2103
        if(languages == null || languages.size() == 0){
2104
            nameQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("name", queryString), Occur.MUST);
2105
        } else {
2106
            Builder languageSubQueryBuilder = new Builder();
2107
            for(Language lang : languages){
2108
                languageSubQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("language.uuid",  lang.getUuid().toString(), false), Occur.SHOULD);
2109
            }
2110
            nameQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("name", queryString), Occur.MUST);
2111
            nameQueryBuilder.add(languageSubQueryBuilder.build(), Occur.MUST);
2112
        }
2113
        textQueryBuilder.add(nameQueryBuilder.build(), Occur.SHOULD);
2114

    
2115

    
2116
        // text field from TextData
2117
        textQueryBuilder.add(descriptionElementQueryFactory.newMultilanguageTextQuery("text", queryString, languages), Occur.SHOULD);
2118

    
2119
        // --- TermBase fields - by representation ----
2120
        // state field from CategoricalData
2121
        textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("stateData.state", queryString, languages), Occur.SHOULD);
2122

    
2123
        // state field from CategoricalData
2124
        textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("stateData.modifyingText", queryString, languages), Occur.SHOULD);
2125

    
2126
        // area field from Distribution
2127
        textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("area", queryString, languages), Occur.SHOULD);
2128

    
2129
        // status field from Distribution
2130
        textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("status", queryString, languages), Occur.SHOULD);
2131

    
2132
        finalQueryBuilder.add(textQueryBuilder.build(), Occur.MUST);
2133
        // --- classification ----
2134

    
2135
        if(classification != null){
2136
            finalQueryBuilder.add(descriptionElementQueryFactory.newEntityIdQuery("inDescription.taxon.taxonNodes.classification.id", classification), Occur.MUST);
2137
        }
2138

    
2139
        // --- IdentifieableEntity fields - by uuid
2140
        if(features != null && features.size() > 0 ){
2141
            finalQueryBuilder.add(descriptionElementQueryFactory.newEntityUuidsQuery("feature.uuid", features), Occur.MUST);
2142
        }
2143

    
2144
        // the description must be associated with a taxon
2145
        finalQueryBuilder.add(descriptionElementQueryFactory.newIsNotNullQuery("inDescription.taxon.id"), Occur.MUST);
2146

    
2147
        BooleanQuery finalQuery = finalQueryBuilder.build();
2148
        logger.info("prepareByDescriptionElementFullTextSearch() query: " + finalQuery.toString());
2149
        return finalQuery;
2150
    }
2151

    
2152
    /**
2153
     * DefinedTerm representations and MultilanguageString maps are stored in the Lucene index by the {@link DefinedTermBaseClassBridge}
2154
     * and {@link MultilanguageTextFieldBridge } in a consistent way. One field per language and also in one additional field for all languages.
2155
     * This method is a convenient means to retrieve a Lucene query string for such the fields.
2156
     *
2157
     * @param name name of the term field as in the Lucene index. Must be field created by {@link DefinedTermBaseClassBridge}
2158
     * or {@link MultilanguageTextFieldBridge }
2159
     * @param languages the languages to search for exclusively. Can be <code>null</code> to search in all languages
2160
     * @param stringBuilder a StringBuilder to be reused, if <code>null</code> a new StringBuilder will be instantiated and is returned
2161
     * @return the StringBuilder given a parameter or a new one if the stringBuilder parameter was null.
2162
     *
2163
     * TODO move to utiliy class !!!!!!!!
2164
     */
2165
    private StringBuilder appendLocalizedFieldQuery(String name, List<Language> languages, StringBuilder stringBuilder) {
2166

    
2167
        if(stringBuilder == null){
2168
            stringBuilder = new StringBuilder();
2169
        }
2170
        if(languages == null || languages.size() == 0){
2171
            stringBuilder.append(name + ".ALL:(%1$s) ");
2172
        } else {
2173
            for(Language lang : languages){
2174
                stringBuilder.append(name + "." + lang.getUuid().toString() + ":(%1$s) ");
2175
            }
2176
        }
2177
        return stringBuilder;
2178
    }
2179

    
2180
    @Override
2181
    public List<Synonym> createInferredSynonyms(Taxon taxon, Classification classification, SynonymType type, boolean doWithMisappliedNames){
2182
        List <Synonym> inferredSynonyms = new ArrayList<>();
2183
        List<Synonym> inferredSynonymsToBeRemoved = new ArrayList<>();
2184

    
2185
        HashMap <UUID, IZoologicalName> zooHashMap = new HashMap<>();
2186

    
2187

    
2188
        UUID nameUuid= taxon.getName().getUuid();
2189
        IZoologicalName taxonName = getZoologicalName(nameUuid, zooHashMap);
2190
        String epithetOfTaxon = null;
2191
        String infragenericEpithetOfTaxon = null;
2192
        String infraspecificEpithetOfTaxon = null;
2193
        if (taxonName.isSpecies()){
2194
             epithetOfTaxon= taxonName.getSpecificEpithet();
2195
        } else if (taxonName.isInfraGeneric()){
2196
            infragenericEpithetOfTaxon = taxonName.getInfraGenericEpithet();
2197
        } else if (taxonName.isInfraSpecific()){
2198
            infraspecificEpithetOfTaxon = taxonName.getInfraSpecificEpithet();
2199
        }
2200
        String genusOfTaxon = taxonName.getGenusOrUninomial();
2201
        Set<TaxonNode> nodes = taxon.getTaxonNodes();
2202
        List<String> taxonNames = new ArrayList<String>();
2203

    
2204
        for (TaxonNode node: nodes){
2205
           // Map<String, String> synonymsGenus = new HashMap<>(); // Changed this to be able to store the idInSource to a genusName
2206
           // List<String> synonymsEpithet = new ArrayList<>();
2207

    
2208
            if (node.getClassification().equals(classification)){
2209
                if (!node.isTopmostNode()){
2210
                    TaxonNode parent = node.getParent();
2211
                    parent = CdmBase.deproxy(parent);
2212
                    TaxonName parentName =  parent.getTaxon().getName();
2213
                    IZoologicalName zooParentName = CdmBase.deproxy(parentName);
2214
                    Taxon parentTaxon = CdmBase.deproxy(parent.getTaxon());
2215
                    Rank rankOfTaxon = taxonName.getRank();
2216

    
2217

    
2218
                    //create inferred synonyms for species, subspecies
2219
                    if ((parentName.isGenus() || parentName.isSpecies() || parentName.getRank().equals(Rank.SUBGENUS())) ){
2220

    
2221
                        Synonym inferredEpithet = null;
2222
                        Synonym inferredGenus = null;
2223
                        Synonym potentialCombination = null;
2224

    
2225
                        List<String> propertyPaths = new ArrayList<String>();
2226
                        propertyPaths.add("synonym");
2227
                        propertyPaths.add("synonym.name");
2228
                        List<OrderHint> orderHintsSynonyms = new ArrayList<>();
2229
                        orderHintsSynonyms.add(new OrderHint("titleCache", SortOrder.ASCENDING));
2230

    
2231
                        List<Synonym> synonyMsOfParent = dao.getSynonyms(parentTaxon, SynonymType.HETEROTYPIC_SYNONYM_OF(), null, null,orderHintsSynonyms,propertyPaths);
2232
                        List<Synonym> synonymsOfTaxon= dao.getSynonyms(taxon, SynonymType.HETEROTYPIC_SYNONYM_OF(), null, null,orderHintsSynonyms,propertyPaths);
2233

    
2234
                        List<TaxonRelationship> taxonRelListParent = null;
2235
                        List<TaxonRelationship> taxonRelListTaxon = null;
2236
                        if (doWithMisappliedNames){
2237
                            List<OrderHint> orderHintsMisapplied = new ArrayList<>();
2238
                            orderHintsMisapplied.add(new OrderHint("relatedFrom.titleCache", SortOrder.ASCENDING));
2239
                            taxonRelListParent = dao.getTaxonRelationships(parentTaxon, TaxonRelationshipType.MISAPPLIED_NAME_FOR(), null, null, orderHintsMisapplied, propertyPaths, Direction.relatedTo);
2240
                            taxonRelListTaxon = dao.getTaxonRelationships(taxon, TaxonRelationshipType.MISAPPLIED_NAME_FOR(), null, null, orderHintsMisapplied, propertyPaths, Direction.relatedTo);
2241
                        }
2242

    
2243
                        if (type.equals(SynonymType.INFERRED_EPITHET_OF())){
2244

    
2245
                            for (Synonym synonymRelationOfParent:synonyMsOfParent){
2246

    
2247
                                inferredEpithet = createInferredEpithets(taxon,
2248
                                        zooHashMap, taxonName, epithetOfTaxon,
2249
                                        infragenericEpithetOfTaxon,
2250
                                        infraspecificEpithetOfTaxon,
2251
                                        taxonNames, parentName,
2252
                                        synonymRelationOfParent);
2253

    
2254
                                inferredSynonyms.add(inferredEpithet);
2255
                                zooHashMap.put(inferredEpithet.getName().getUuid(), inferredEpithet.getName());
2256
                                taxonNames.add(inferredEpithet.getName().getNameCache());
2257
                            }
2258

    
2259
                            if (doWithMisappliedNames){
2260

    
2261
                                for (TaxonRelationship taxonRelationship: taxonRelListParent){
2262
                                     Taxon misappliedName = taxonRelationship.getFromTaxon();
2263

    
2264
                                     inferredEpithet = createInferredEpithets(taxon,
2265
                                             zooHashMap, taxonName, epithetOfTaxon,
2266
                                             infragenericEpithetOfTaxon,
2267
                                             infraspecificEpithetOfTaxon,
2268
                                             taxonNames, parentName,
2269
                                             misappliedName);
2270

    
2271
                                    inferredSynonyms.add(inferredEpithet);
2272
                                    zooHashMap.put(inferredEpithet.getName().getUuid(), inferredEpithet.getName());
2273
                                     taxonNames.add(inferredEpithet.getName().getNameCache());
2274
                                }
2275
                            }
2276

    
2277
                            if (!taxonNames.isEmpty()){
2278
                            List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2279
                            IZoologicalName name;
2280
                            if (!synNotInCDM.isEmpty()){
2281
                                inferredSynonymsToBeRemoved.clear();
2282

    
2283
                                for (Synonym syn :inferredSynonyms){
2284
                                    name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2285
                                    if (!synNotInCDM.contains(name.getNameCache())){
2286
                                        inferredSynonymsToBeRemoved.add(syn);
2287
                                    }
2288
                                }
2289

    
2290
                                // Remove identified Synonyms from inferredSynonyms
2291
                                for (Synonym synonym : inferredSynonymsToBeRemoved) {
2292
                                    inferredSynonyms.remove(synonym);
2293
                                }
2294
                            }
2295
                        }
2296

    
2297
                    }else if (type.equals(SynonymType.INFERRED_GENUS_OF())){
2298

    
2299
                        for (Synonym synonymRelationOfTaxon:synonymsOfTaxon){
2300
                            TaxonName synName;
2301
                            IZoologicalName inferredSynName;
2302

    
2303
                            inferredGenus = createInferredGenus(taxon,
2304
                                    zooHashMap, taxonName, epithetOfTaxon,
2305
                                    genusOfTaxon, taxonNames, zooParentName, synonymRelationOfTaxon);
2306

    
2307
                            inferredSynonyms.add(inferredGenus);
2308
                            zooHashMap.put(inferredGenus.getName().getUuid(), inferredGenus.getName());
2309
                            taxonNames.add(inferredGenus.getName().getNameCache());
2310

    
2311

    
2312
                        }
2313

    
2314
                        if (doWithMisappliedNames){
2315

    
2316
                            for (TaxonRelationship taxonRelationship: taxonRelListTaxon){
2317
                                Taxon misappliedName = taxonRelationship.getFromTaxon();
2318
                                inferredGenus = createInferredGenus(taxon, zooHashMap, taxonName, infraspecificEpithetOfTaxon, genusOfTaxon, taxonNames, zooParentName,  misappliedName);
2319

    
2320
                                inferredSynonyms.add(inferredGenus);
2321
                                zooHashMap.put(inferredGenus.getName().getUuid(), inferredGenus.getName());
2322
                                 taxonNames.add(inferredGenus.getName().getNameCache());
2323
                            }
2324
                        }
2325

    
2326

    
2327
                        if (!taxonNames.isEmpty()){
2328
                            List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2329
                            IZoologicalName name;
2330
                            if (!synNotInCDM.isEmpty()){
2331
                                inferredSynonymsToBeRemoved.clear();
2332

    
2333
                                for (Synonym syn :inferredSynonyms){
2334
                                    name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2335
                                    if (!synNotInCDM.contains(name.getNameCache())){
2336
                                        inferredSynonymsToBeRemoved.add(syn);
2337
                                    }
2338
                                }
2339

    
2340
                                // Remove identified Synonyms from inferredSynonyms
2341
                                for (Synonym synonym : inferredSynonymsToBeRemoved) {
2342
                                    inferredSynonyms.remove(synonym);
2343
                                }
2344
                            }
2345
                        }
2346

    
2347
                    }else if (type.equals(SynonymType.POTENTIAL_COMBINATION_OF())){
2348

    
2349
                        Reference sourceReference = null; // TODO: Determination of sourceReference is redundant
2350
                        IZoologicalName inferredSynName;
2351
                        //for all synonyms of the parent...
2352
                        for (Synonym synonymRelationOfParent:synonyMsOfParent){
2353
                            TaxonName synName;
2354
                            HibernateProxyHelper.deproxy(synonymRelationOfParent);
2355

    
2356
                            synName = synonymRelationOfParent.getName();
2357

    
2358
                            // Set the sourceReference
2359
                            sourceReference = synonymRelationOfParent.getSec();
2360

    
2361
                            // Determine the idInSource
2362
                            String idInSourceParent = getIdInSource(synonymRelationOfParent);
2363

    
2364
                            IZoologicalName parentSynZooName = getZoologicalName(synName.getUuid(), zooHashMap);
2365
                            String synParentGenus = parentSynZooName.getGenusOrUninomial();
2366
                            String synParentInfragenericName = null;
2367
                            String synParentSpecificEpithet = null;
2368

    
2369
                            if (parentSynZooName.isInfraGeneric()){
2370
                                synParentInfragenericName = parentSynZooName.getInfraGenericEpithet();
2371
                            }
2372
                            if (parentSynZooName.isSpecies()){
2373
                                synParentSpecificEpithet = parentSynZooName.getSpecificEpithet();
2374
                            }
2375

    
2376
                           /* if (synGenusName != null && !synonymsGenus.containsKey(synGenusName)){
2377
                                synonymsGenus.put(synGenusName, idInSource);
2378
                            }*/
2379

    
2380
                            //for all synonyms of the taxon
2381

    
2382
                            for (Synonym synonymRelationOfTaxon:synonymsOfTaxon){
2383

    
2384
                                IZoologicalName zooSynName = getZoologicalName(synonymRelationOfTaxon.getName().getUuid(), zooHashMap);
2385
                                potentialCombination = createPotentialCombination(idInSourceParent, parentSynZooName, zooSynName,
2386
                                        synParentGenus,
2387
                                        synParentInfragenericName,
2388
                                        synParentSpecificEpithet, synonymRelationOfTaxon, zooHashMap);
2389

    
2390
                                taxon.addSynonym(potentialCombination, SynonymType.POTENTIAL_COMBINATION_OF());
2391
                                inferredSynonyms.add(potentialCombination);
2392
                                zooHashMap.put(potentialCombination.getName().getUuid(), potentialCombination.getName());
2393
                                 taxonNames.add(potentialCombination.getName().getNameCache());
2394

    
2395
                            }
2396

    
2397

    
2398
                        }
2399

    
2400
                        if (doWithMisappliedNames){
2401

    
2402
                            for (TaxonRelationship parentRelationship: taxonRelListParent){
2403

    
2404
                                TaxonName misappliedParentName;
2405

    
2406
                                Taxon misappliedParent = parentRelationship.getFromTaxon();
2407
                                misappliedParentName = misappliedParent.getName();
2408

    
2409
                                HibernateProxyHelper.deproxy(misappliedParent);
2410

    
2411
                                // Set the sourceReference
2412
                                sourceReference = misappliedParent.getSec();
2413

    
2414
                                // Determine the idInSource
2415
                                String idInSourceParent = getIdInSource(misappliedParent);
2416

    
2417
                                IZoologicalName parentSynZooName = getZoologicalName(misappliedParentName.getUuid(), zooHashMap);
2418
                                String synParentGenus = parentSynZooName.getGenusOrUninomial();
2419
                                String synParentInfragenericName = null;
2420
                                String synParentSpecificEpithet = null;
2421

    
2422
                                if (parentSynZooName.isInfraGeneric()){
2423
                                    synParentInfragenericName = parentSynZooName.getInfraGenericEpithet();
2424
                                }
2425
                                if (parentSynZooName.isSpecies()){
2426
                                    synParentSpecificEpithet = parentSynZooName.getSpecificEpithet();
2427
                                }
2428

    
2429

    
2430
                                for (TaxonRelationship taxonRelationship: taxonRelListTaxon){
2431
                                    Taxon misappliedName = taxonRelationship.getFromTaxon();
2432
                                    IZoologicalName zooMisappliedName = getZoologicalName(misappliedName.getName().getUuid(), zooHashMap);
2433
                                    potentialCombination = createPotentialCombination(
2434
                                            idInSourceParent, parentSynZooName, zooMisappliedName,
2435
                                            synParentGenus,
2436
                                            synParentInfragenericName,
2437
                                            synParentSpecificEpithet, misappliedName, zooHashMap);
2438

    
2439

    
2440
                                    taxon.addSynonym(potentialCombination, SynonymType.POTENTIAL_COMBINATION_OF());
2441
                                    inferredSynonyms.add(potentialCombination);
2442
                                    zooHashMap.put(potentialCombination.getName().getUuid(), potentialCombination.getName());
2443
                                     taxonNames.add(potentialCombination.getName().getNameCache());
2444
                                }
2445
                            }
2446
                        }
2447

    
2448
                        if (!taxonNames.isEmpty()){
2449
                            List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2450
                            IZoologicalName name;
2451
                            if (!synNotInCDM.isEmpty()){
2452
                                inferredSynonymsToBeRemoved.clear();
2453
                                for (Synonym syn :inferredSynonyms){
2454
                                    try{
2455
                                        name = syn.getName();
2456
                                    }catch (ClassCastException e){
2457
                                        name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2458
                                    }
2459
                                    if (!synNotInCDM.contains(name.getNameCache())){
2460
                                        inferredSynonymsToBeRemoved.add(syn);
2461
                                    }
2462
                                 }
2463
                                // Remove identified Synonyms from inferredSynonyms
2464
                                for (Synonym synonym : inferredSynonymsToBeRemoved) {
2465
                                    inferredSynonyms.remove(synonym);
2466
                                }
2467
                            }
2468
                         }
2469
                        }
2470
                    }else {
2471
                        logger.info("The synonym type is not defined.");
2472
                        return inferredSynonyms;
2473
                    }
2474
                }
2475
            }
2476

    
2477
        }
2478

    
2479
        return inferredSynonyms;
2480
    }
2481

    
2482
    private Synonym createPotentialCombination(String idInSourceParent,
2483
            IZoologicalName parentSynZooName, 	IZoologicalName zooSynName, String synParentGenus,
2484
            String synParentInfragenericName, String synParentSpecificEpithet,
2485
            TaxonBase syn, Map<UUID, IZoologicalName> zooHashMap) {
2486
        Synonym potentialCombination;
2487
        Reference sourceReference;
2488
        IZoologicalName inferredSynName;
2489
        HibernateProxyHelper.deproxy(syn);
2490

    
2491
        // Set sourceReference
2492
        sourceReference = syn.getSec();
2493
        if (sourceReference == null){
2494
            logger.warn("The synonym has no sec reference because it is a misapplied name! Take the sec reference of taxon");
2495
            //TODO:Remove
2496
            if (!parentSynZooName.getTaxa().isEmpty()){
2497
                TaxonBase taxon = parentSynZooName.getTaxa().iterator().next();
2498

    
2499
                sourceReference = taxon.getSec();
2500
            }
2501
        }
2502
        String synTaxonSpecificEpithet = zooSynName.getSpecificEpithet();
2503

    
2504
        String synTaxonInfraSpecificName= null;
2505

    
2506
        if (parentSynZooName.isSpecies()){
2507
            synTaxonInfraSpecificName = zooSynName.getInfraSpecificEpithet();
2508
        }
2509

    
2510
        /*if (epithetName != null && !synonymsEpithet.contains(epithetName)){
2511
            synonymsEpithet.add(epithetName);
2512
        }*/
2513

    
2514
        //create potential combinations...
2515
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(syn.getName().getRank());
2516

    
2517
        inferredSynName.setGenusOrUninomial(synParentGenus);
2518
        if (zooSynName.isSpecies()){
2519
              inferredSynName.setSpecificEpithet(synTaxonSpecificEpithet);
2520
              if (parentSynZooName.isInfraGeneric()){
2521
                  inferredSynName.setInfraGenericEpithet(synParentInfragenericName);
2522
              }
2523
        }
2524
        if (zooSynName.isInfraSpecific()){
2525
            inferredSynName.setSpecificEpithet(synParentSpecificEpithet);
2526
            inferredSynName.setInfraSpecificEpithet(synTaxonInfraSpecificName);
2527
        }
2528
        if (parentSynZooName.isInfraGeneric()){
2529
            inferredSynName.setInfraGenericEpithet(synParentInfragenericName);
2530
        }
2531

    
2532

    
2533
        potentialCombination = Synonym.NewInstance(inferredSynName, null);
2534

    
2535
        // Set the sourceReference
2536
        potentialCombination.setSec(sourceReference);
2537

    
2538

    
2539
        // Determine the idInSource
2540
        String idInSourceSyn= getIdInSource(syn);
2541

    
2542
        if (idInSourceParent != null && idInSourceSyn != null) {
2543
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation, idInSourceSyn + "; " + idInSourceParent, POTENTIAL_COMBINATION_NAMESPACE, sourceReference, null);
2544
            inferredSynName.addSource(originalSource);
2545
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation, idInSourceSyn + "; " + idInSourceParent, POTENTIAL_COMBINATION_NAMESPACE, sourceReference, null);
2546
            potentialCombination.addSource(originalSource);
2547
        }
2548

    
2549
        return potentialCombination;
2550
    }
2551

    
2552
    private Synonym createInferredGenus(Taxon taxon,
2553
            Map<UUID, IZoologicalName> zooHashMap, IZoologicalName taxonName,
2554
            String epithetOfTaxon, String genusOfTaxon,
2555
            List<String> taxonNames, IZoologicalName zooParentName,
2556
            TaxonBase syn) {
2557

    
2558
        Synonym inferredGenus;
2559
        TaxonName synName;
2560
        IZoologicalName inferredSynName;
2561
        synName =syn.getName();
2562
        HibernateProxyHelper.deproxy(syn);
2563

    
2564
        // Determine the idInSource
2565
        String idInSourceSyn = getIdInSource(syn);
2566
        String idInSourceTaxon = getIdInSource(taxon);
2567
        // Determine the sourceReference
2568
        Reference sourceReference = syn.getSec();
2569

    
2570
        //logger.warn(sourceReference.getTitleCache());
2571

    
2572
        synName = syn.getName();
2573
        IZoologicalName synZooName = getZoologicalName(synName.getUuid(), zooHashMap);
2574
        String synSpeciesEpithetName = synZooName.getSpecificEpithet();
2575
                     /* if (synonymsEpithet != null && !synonymsEpithet.contains(synSpeciesEpithetName)){
2576
            synonymsEpithet.add(synSpeciesEpithetName);
2577
        }*/
2578

    
2579
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(taxon.getName().getRank());
2580
        //TODO:differ between parent is genus and taxon is species, parent is subgenus and taxon is species, parent is species and taxon is subspecies and parent is genus and taxon is subgenus...
2581

    
2582

    
2583
        inferredSynName.setGenusOrUninomial(genusOfTaxon);
2584
        if (zooParentName.isInfraGeneric()){
2585
            inferredSynName.setInfraGenericEpithet(zooParentName.getInfraGenericEpithet());
2586
        }
2587

    
2588
        if (taxonName.isSpecies()){
2589
            inferredSynName.setSpecificEpithet(synSpeciesEpithetName);
2590
        }
2591
        if (taxonName.isInfraSpecific()){
2592
            inferredSynName.setSpecificEpithet(epithetOfTaxon);
2593
            inferredSynName.setInfraSpecificEpithet(synZooName.getInfraGenericEpithet());
2594
        }
2595

    
2596

    
2597
        inferredGenus = Synonym.NewInstance(inferredSynName, null);
2598

    
2599
        // Set the sourceReference
2600
        inferredGenus.setSec(sourceReference);
2601

    
2602
        // Add the original source
2603
        if (idInSourceSyn != null && idInSourceTaxon != null) {
2604
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2605
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2606
            inferredGenus.addSource(originalSource);
2607

    
2608
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2609
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2610
            inferredSynName.addSource(originalSource);
2611
            originalSource = null;
2612

    
2613
        }else{
2614
            logger.error("There is an idInSource missing: " + idInSourceSyn + " of Synonym or " + idInSourceTaxon + " of Taxon");
2615
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2616
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2617
            inferredGenus.addSource(originalSource);
2618

    
2619
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2620
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2621
            inferredSynName.addSource(originalSource);
2622
            originalSource = null;
2623
        }
2624

    
2625
        taxon.addSynonym(inferredGenus, SynonymType.INFERRED_GENUS_OF());
2626

    
2627
        return inferredGenus;
2628
    }
2629

    
2630
    private Synonym createInferredEpithets(Taxon taxon,
2631
            Map<UUID, IZoologicalName> zooHashMap, IZoologicalName taxonName,
2632
            String epithetOfTaxon, String infragenericEpithetOfTaxon,
2633
            String infraspecificEpithetOfTaxon, List<String> taxonNames,
2634
            TaxonName parentName, TaxonBase syn) {
2635

    
2636
        Synonym inferredEpithet;
2637
        TaxonName synName;
2638
        IZoologicalName inferredSynName;
2639
        HibernateProxyHelper.deproxy(syn);
2640

    
2641
        // Determine the idInSource
2642
        String idInSourceSyn = getIdInSource(syn);
2643
        String idInSourceTaxon =  getIdInSource(taxon);
2644
        // Determine the sourceReference
2645
        Reference sourceReference = syn.getSec();
2646

    
2647
        if (sourceReference == null){
2648
             logger.warn("The synonym has no sec reference because it is a misapplied name! Take the sec reference of taxon" + taxon.getSec());
2649
             sourceReference = taxon.getSec();
2650
        }
2651

    
2652
        synName = syn.getName();
2653
        IZoologicalName zooSynName = getZoologicalName(synName.getUuid(), zooHashMap);
2654
        String synGenusName = zooSynName.getGenusOrUninomial();
2655
        String synInfraGenericEpithet = null;
2656
        String synSpecificEpithet = null;
2657

    
2658
        if (zooSynName.getInfraGenericEpithet() != null){
2659
            synInfraGenericEpithet = zooSynName.getInfraGenericEpithet();
2660
        }
2661

    
2662
        if (zooSynName.isInfraSpecific()){
2663
            synSpecificEpithet = zooSynName.getSpecificEpithet();
2664
        }
2665

    
2666
                     /* if (synGenusName != null && !synonymsGenus.containsKey(synGenusName)){
2667
            synonymsGenus.put(synGenusName, idInSource);
2668
        }*/
2669

    
2670
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(taxon.getName().getRank());
2671

    
2672
        // DEBUG TODO: for subgenus or subspecies the infrageneric or infraspecific epithet should be used!!!
2673
        if (epithetOfTaxon == null && infragenericEpithetOfTaxon == null && infraspecificEpithetOfTaxon == null) {
2674
            logger.error("This specificEpithet is NULL" + taxon.getTitleCache());
2675
        }
2676
        inferredSynName.setGenusOrUninomial(synGenusName);
2677

    
2678
        if (parentName.isInfraGeneric()){
2679
            inferredSynName.setInfraGenericEpithet(synInfraGenericEpithet);
2680
        }
2681
        if (taxonName.isSpecies()){
2682
            inferredSynName.setSpecificEpithet(epithetOfTaxon);
2683
        }else if (taxonName.isInfraSpecific()){
2684
            inferredSynName.setSpecificEpithet(synSpecificEpithet);
2685
            inferredSynName.setInfraSpecificEpithet(infraspecificEpithetOfTaxon);
2686
        }
2687

    
2688
        inferredEpithet = Synonym.NewInstance(inferredSynName, null);
2689

    
2690
        // Set the sourceReference
2691
        inferredEpithet.setSec(sourceReference);
2692

    
2693
        /* Add the original source
2694
        if (idInSource != null) {
2695
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(idInSource, "InferredEpithetOf", syn.getSec(), null);
2696

    
2697
            // Add the citation
2698
            Reference citation = getCitation(syn);
2699
            if (citation != null) {
2700
                originalSource.setCitation(citation);
2701
                inferredEpithet.addSource(originalSource);
2702
            }
2703
        }*/
2704
        String taxonId = idInSourceTaxon+ "; " + idInSourceSyn;
2705

    
2706

    
2707
        IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2708
                taxonId, INFERRED_EPITHET_NAMESPACE, sourceReference, null);
2709

    
2710
        inferredEpithet.addSource(originalSource);
2711

    
2712
        originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2713
                taxonId, INFERRED_EPITHET_NAMESPACE, sourceReference, null);
2714

    
2715
        inferredSynName.addSource(originalSource);
2716

    
2717

    
2718

    
2719
        taxon.addSynonym(inferredEpithet, SynonymType.INFERRED_EPITHET_OF());
2720

    
2721
        return inferredEpithet;
2722
    }
2723

    
2724
    /**
2725
     * Returns an existing IZoologicalName or extends an internal hashmap if it does not exist.
2726
     * Very likely only useful for createInferredSynonyms().
2727
     * @param uuid
2728
     * @param zooHashMap
2729
     * @return
2730
     */
2731
    private IZoologicalName getZoologicalName(UUID uuid, Map <UUID, IZoologicalName> zooHashMap) {
2732
        IZoologicalName taxonName =nameDao.findZoologicalNameByUUID(uuid);
2733
        if (taxonName == null) {
2734
            taxonName = zooHashMap.get(uuid);
2735
        }
2736
        return taxonName;
2737
    }
2738

    
2739
    /**
2740
     * Returns the idInSource for a given Synonym.
2741
     * @param syn
2742
     */
2743
    private String getIdInSource(TaxonBase taxonBase) {
2744
        String idInSource = null;
2745
        Set<IdentifiableSource> sources = taxonBase.getSources();
2746
        if (sources.size() == 1) {
2747
            IdentifiableSource source = sources.iterator().next();
2748
            if (source != null) {
2749
                idInSource  = source.getIdInSource();
2750
            }
2751
        } else if (sources.size() > 1) {
2752
            int count = 1;
2753
            idInSource = "";
2754
            for (IdentifiableSource source : sources) {
2755
                idInSource += source.getIdInSource();
2756
                if (count < sources.size()) {
2757
                    idInSource += "; ";
2758
                }
2759
                count++;
2760
            }
2761
        } else if (sources.size() == 0){
2762
            logger.warn("No idInSource for TaxonBase " + taxonBase.getUuid() + " - " + taxonBase.getTitleCache());
2763
        }
2764

    
2765

    
2766
        return idInSource;
2767
    }
2768

    
2769

    
2770
    /**
2771
     * Returns the citation for a given Synonym.
2772
     * @param syn
2773
     */
2774
    private Reference getCitation(Synonym syn) {
2775
        Reference citation = null;
2776
        Set<IdentifiableSource> sources = syn.getSources();
2777
        if (sources.size() == 1) {
2778
            IdentifiableSource source = sources.iterator().next();
2779
            if (source != null) {
2780
                citation = source.getCitation();
2781
            }
2782
        } else if (sources.size() > 1) {
2783
            logger.warn("This Synonym has more than one source: " + syn.getUuid() + " (" + syn.getTitleCache() +")");
2784
        }
2785

    
2786
        return citation;
2787
    }
2788

    
2789
    @Override
2790
    public List<Synonym>  createAllInferredSynonyms(Taxon taxon, Classification tree, boolean doWithMisappliedNames){
2791
        List <Synonym> inferredSynonyms = new ArrayList<Synonym>();
2792

    
2793
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.INFERRED_EPITHET_OF(), doWithMisappliedNames));
2794
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.INFERRED_GENUS_OF(), doWithMisappliedNames));
2795
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.POTENTIAL_COMBINATION_OF(), doWithMisappliedNames));
2796

    
2797
        return inferredSynonyms;
2798
    }
2799

    
2800
    @Override
2801
    public List<Classification> listClassifications(TaxonBase taxonBase, Integer limit, Integer start, List<String> propertyPaths) {
2802

    
2803
        // TODO quickly implemented, create according dao !!!!
2804
        Set<TaxonNode> nodes = new HashSet<>();
2805
        Set<Classification> classifications = new HashSet<>();
2806
        List<Classification> list = new ArrayList<>();
2807

    
2808
        if (taxonBase == null) {
2809
            return list;
2810
        }
2811

    
2812
        taxonBase = load(taxonBase.getUuid());
2813

    
2814
        if (taxonBase instanceof Taxon) {
2815
            nodes.addAll(((Taxon)taxonBase).getTaxonNodes());
2816
        } else {
2817
            Taxon taxon = ((Synonym)taxonBase).getAcceptedTaxon();
2818
            if (taxon != null){
2819
                nodes.addAll(taxon.getTaxonNodes());
2820
            }
2821
        }
2822
        for (TaxonNode node : nodes) {
2823
            classifications.add(node.getClassification());
2824
        }
2825
        list.addAll(classifications);
2826
        return list;
2827
    }
2828

    
2829
    @Override
2830
    @Transactional(readOnly = false)
2831
    public UpdateResult changeRelatedTaxonToSynonym(UUID fromTaxonUuid,
2832
            UUID toTaxonUuid,
2833
            TaxonRelationshipType oldRelationshipType,
2834
            SynonymType synonymType) throws DataChangeNoRollbackException {
2835
        UpdateResult result = new UpdateResult();
2836
        Taxon fromTaxon = (Taxon) dao.load(fromTaxonUuid);
2837
        Taxon toTaxon = (Taxon) dao.load(toTaxonUuid);
2838
        result = changeRelatedTaxonToSynonym(fromTaxon, toTaxon, oldRelationshipType, synonymType);
2839

    
2840
        result.addUpdatedObject(fromTaxon);
2841
        result.addUpdatedObject(toTaxon);
2842
        result.addUpdatedObject(result.getCdmEntity());
2843

    
2844
        return result;
2845
    }
2846

    
2847
    @Override
2848
    @Transactional(readOnly = false)
2849
    public UpdateResult changeRelatedTaxonToSynonym(Taxon fromTaxon, Taxon toTaxon, TaxonRelationshipType oldRelationshipType,
2850
            SynonymType synonymType) throws DataChangeNoRollbackException {
2851

    
2852
        UpdateResult result = new UpdateResult();
2853
        // Create new synonym using concept name
2854
        TaxonName synonymName = fromTaxon.getName();
2855

    
2856
        // Remove concept relation from taxon
2857
        toTaxon.removeTaxon(fromTaxon, oldRelationshipType);
2858

    
2859
        // Create a new synonym for the taxon
2860
        Synonym synonym;
2861
        if (synonymType != null
2862
                && synonymType.equals(SynonymType.HOMOTYPIC_SYNONYM_OF())){
2863
            synonym = Synonym.NewInstance(synonymName, fromTaxon.getSec());
2864
            toTaxon.addHomotypicSynonym(synonym);
2865
        } else{
2866
            synonym = toTaxon.addHeterotypicSynonymName(synonymName);
2867
        }
2868

    
2869
        this.saveOrUpdate(toTaxon);
2870
        //TODO: configurator and classification
2871
        TaxonDeletionConfigurator config = new TaxonDeletionConfigurator();
2872
        config.setDeleteNameIfPossible(false);
2873
        result.includeResult(this.deleteTaxon(fromTaxon.getUuid(), config, null));
2874
        result.setCdmEntity(synonym);
2875
        result.addUpdatedObject(toTaxon);
2876
        result.addUpdatedObject(synonym);
2877
        return result;
2878
    }
2879

    
2880
    @Override
2881
    public DeleteResult isDeletable(UUID taxonBaseUuid, DeleteConfiguratorBase config){
2882
        DeleteResult result = new DeleteResult();
2883
        TaxonBase taxonBase = load(taxonBaseUuid);
2884
        Set<CdmBase> references = commonService.getReferencingObjectsForDeletion(taxonBase);
2885
        if (taxonBase instanceof Taxon){
2886
            TaxonDeletionConfigurator taxonConfig = (TaxonDeletionConfigurator) config;
2887
            result = isDeletableForTaxon(references, taxonConfig);
2888
        }else{
2889
            SynonymDeletionConfigurator synonymConfig = (SynonymDeletionConfigurator) config;
2890
            result = isDeletableForSynonym(references, synonymConfig);
2891
        }
2892
        return result;
2893
    }
2894

    
2895
    private DeleteResult isDeletableForSynonym(Set<CdmBase> references, SynonymDeletionConfigurator config){
2896
        String message;
2897
        DeleteResult result = new DeleteResult();
2898
        for (CdmBase ref: references){
2899
            if (!(ref instanceof Taxon || ref instanceof TaxonName )){
2900
                message = "The Synonym can't be deleted as long as it is referenced by " + ref.getClass().getSimpleName() + " with id "+ ref.getId();
2901
                result.addException(new ReferencedObjectUndeletableException(message));
2902
                result.addRelatedObject(ref);
2903
                result.setAbort();
2904
            }
2905
        }
2906

    
2907
        return result;
2908
    }
2909

    
2910
    private DeleteResult isDeletableForTaxon(Set<CdmBase> references, TaxonDeletionConfigurator config){
2911
        String message = null;
2912
        DeleteResult result = new DeleteResult();
2913
        for (CdmBase ref: references){
2914
            if (!(ref instanceof TaxonName)){
2915
            	message = null;
2916
                if (!config.isDeleteSynonymRelations() && (ref instanceof Synonym)){
2917
                    message = "The taxon can't be deleted as long as it has synonyms.";
2918
                }
2919
                if (!config.isDeleteDescriptions() && (ref instanceof DescriptionBase)){
2920
                    message = "The taxon can't be deleted as long as it has factual data.";
2921
                }
2922

    
2923
                if (!config.isDeleteTaxonNodes() && (ref instanceof TaxonNode)){
2924
                    message = "The taxon can't be deleted as long as it belongs to a taxon node.";
2925
                }
2926
                if (!config.isDeleteTaxonRelationships() && (ref instanceof TaxonNode)){
2927
                    if (!config.isDeleteMisappliedNamesAndInvalidDesignations() && (((TaxonRelationship)ref).getType().equals(TaxonRelationshipType.MISAPPLIED_NAME_FOR())|| ((TaxonRelationship)ref).getType().equals(TaxonRelationshipType.INVALID_DESIGNATION_FOR()))){
2928
                        message = "The taxon can't be deleted as long as it has misapplied names or invalid designations.";
2929
                    } else{
2930
                        message = "The taxon can't be deleted as long as it belongs to a taxon node.";
2931
                    }
2932
                }
2933
                if (ref instanceof PolytomousKeyNode){
2934
                    message = "The taxon can't be deleted as long as it is referenced by a polytomous key node.";
2935
                }
2936

    
2937
                if (HibernateProxyHelper.isInstanceOf(ref, IIdentificationKey.class)){
2938
                   message = "Taxon can't be deleted as it is used in an identification key. Remove from identification key prior to deleting this taxon";
2939
                }
2940

    
2941

    
2942
               /* //PolytomousKeyNode
2943
                if (referencingObject.isInstanceOf(PolytomousKeyNode.class)){
2944
                    String message = "Taxon" + taxon.getTitleCache() + " can't be deleted as it is used in polytomous key node";
2945
                    return message;
2946
                }*/
2947

    
2948
                //TaxonInteraction
2949
                if (ref.isInstanceOf(TaxonInteraction.class)){
2950
                    message = "Taxon can't be deleted as it is used in taxonInteraction#taxon2";
2951
                }
2952

    
2953
              //TaxonInteraction
2954
                if (ref.isInstanceOf(DeterminationEvent.class)){
2955
                    message = "Taxon can't be deleted as it is used in a determination event";
2956
                }
2957
            }
2958
            if (message != null){
2959
	            result.addException(new ReferencedObjectUndeletableException(message));
2960
	            result.addRelatedObject(ref);
2961
	            result.setAbort();
2962
            }
2963
        }
2964

    
2965
        return result;
2966
    }
2967

    
2968
    @Override
2969
    public IncludedTaxaDTO listIncludedTaxa(UUID taxonUuid, IncludedTaxonConfiguration config) {
2970
        IncludedTaxaDTO result = new IncludedTaxaDTO(taxonUuid);
2971

    
2972
        //preliminary implementation
2973

    
2974
        Set<Taxon> taxa = new HashSet<>();
2975
        TaxonBase taxonBase = find(taxonUuid);
2976
        if (taxonBase == null){
2977
            return new IncludedTaxaDTO();
2978
        }else if (taxonBase.isInstanceOf(Taxon.class)){
2979
            Taxon taxon = CdmBase.deproxy(taxonBase, Taxon.class);
2980
            taxa.add(taxon);
2981
        }else if (taxonBase.isInstanceOf(Synonym.class)){
2982
            //TODO partial synonyms ??
2983
            //TODO synonyms in general
2984
            Synonym syn = CdmBase.deproxy(taxonBase, Synonym.class);
2985
            taxa.add(syn.getAcceptedTaxon());
2986
        }else{
2987
            throw new IllegalArgumentException("Unhandled class " + taxonBase.getClass().getSimpleName());
2988
        }
2989

    
2990
        Set<Taxon> related = makeRelatedIncluded(taxa, result, config);
2991
        int i = 0;
2992
        while((! related.isEmpty()) && i++ < 100){  //to avoid
2993
             related = makeRelatedIncluded(related, result, config);
2994
        }
2995

    
2996
        return result;
2997
    }
2998

    
2999
    /**
3000
     * Computes all children and conceptually congruent and included taxa and adds them to the existingTaxa
3001
     * data structure.
3002
     * @return the set of conceptually related taxa for further use
3003
     */
3004
    /**
3005
     * @param uncheckedTaxa
3006
     * @param existingTaxa
3007
     * @param config
3008
     * @return
3009
     */
3010
    private Set<Taxon> makeRelatedIncluded(Set<Taxon> uncheckedTaxa, IncludedTaxaDTO existingTaxa, IncludedTaxonConfiguration config) {
3011

    
3012
        //children
3013
        Set<TaxonNode> taxonNodes = new HashSet<>();
3014
        for (Taxon taxon: uncheckedTaxa){
3015
            taxonNodes.addAll(taxon.getTaxonNodes());
3016
        }
3017

    
3018
        Set<Taxon> children = new HashSet<>();
3019
        if (! config.onlyCongruent){
3020
            for (TaxonNode node: taxonNodes){
3021
                List<TaxonNode> childNodes = nodeService.loadChildNodesOfTaxonNode(node, null, true, null);
3022
                for (TaxonNode child : childNodes){
3023
                    children.add(child.getTaxon());
3024
                }
3025
            }
3026
            children.remove(null);  // just to be on the save side
3027
        }
3028

    
3029
        Iterator<Taxon> it = children.iterator();
3030
        while(it.hasNext()){
3031
            UUID uuid = it.next().getUuid();
3032
            if (existingTaxa.contains(uuid)){
3033
                it.remove();
3034
            }else{
3035
                existingTaxa.addIncludedTaxon(uuid, new ArrayList<>(), false);
3036
            }
3037
        }
3038

    
3039
        //concept relations
3040
        Set<Taxon> uncheckedAndChildren = new HashSet<>(uncheckedTaxa);
3041
        uncheckedAndChildren.addAll(children);
3042

    
3043
        Set<Taxon> relatedTaxa = makeConceptIncludedTaxa(uncheckedAndChildren, existingTaxa, config);
3044

    
3045

    
3046
        Set<Taxon> result = new HashSet<>(relatedTaxa);
3047
        return result;
3048
    }
3049

    
3050
    /**
3051
     * Computes all conceptually congruent or included taxa and adds them to the existingTaxa data structure.
3052
     * @return the set of these computed taxa
3053
     */
3054
    private Set<Taxon> makeConceptIncludedTaxa(Set<Taxon> unchecked, IncludedTaxaDTO existingTaxa, IncludedTaxonConfiguration config) {
3055
        Set<Taxon> result = new HashSet<>();
3056

    
3057
        for (Taxon taxon : unchecked){
3058
            Set<TaxonRelationship> fromRelations = taxon.getRelationsFromThisTaxon();
3059
            Set<TaxonRelationship> toRelations = taxon.getRelationsToThisTaxon();
3060

    
3061
            for (TaxonRelationship fromRel : fromRelations){
3062
                if (config.includeDoubtful == false && fromRel.isDoubtful()){
3063
                    continue;
3064
                }
3065
                if (fromRel.getType().equals(TaxonRelationshipType.CONGRUENT_TO()) ||
3066
                        !config.onlyCongruent && fromRel.getType().equals(TaxonRelationshipType.INCLUDES()) ||
3067
                        !config.onlyCongruent && fromRel.getType().equals(TaxonRelationshipType.CONGRUENT_OR_INCLUDES())
3068
                        ){
3069
                    result.add(fromRel.getToTaxon());
3070
                }
3071
            }
3072

    
3073
            for (TaxonRelationship toRel : toRelations){
3074
                if (config.includeDoubtful == false && toRel.isDoubtful()){
3075
                    continue;
3076
                }
3077
                if (toRel.getType().equals(TaxonRelationshipType.CONGRUENT_TO())){
3078
                    result.add(toRel.getFromTaxon());
3079
                }
3080
            }
3081
        }
3082

    
3083
        Iterator<Taxon> it = result.iterator();
3084
        while(it.hasNext()){
3085
            UUID uuid = it.next().getUuid();
3086
            if (existingTaxa.contains(uuid)){
3087
                it.remove();
3088
            }else{
3089
                existingTaxa.addIncludedTaxon(uuid, new ArrayList<>(), false);
3090
            }
3091
        }
3092
        return result;
3093
    }
3094

    
3095
    @Override
3096
    public List<TaxonBase> findTaxaByName(MatchingTaxonConfigurator config){
3097
        List<TaxonBase> taxonList = dao.getTaxaByName(true, config.isIncludeSynonyms(), false, false, false, config.getTaxonNameTitle(), null, MatchMode.EXACT, null, null, 0, 0, config.getPropertyPath());
3098
        return taxonList;
3099
    }
3100

    
3101
	@Override
3102
	@Transactional(readOnly = true)
3103
	public <S extends TaxonBase> Pager<IdentifiedEntityDTO<S>> findByIdentifier(
3104
			Class<S> clazz, String identifier, DefinedTerm identifierType, TaxonNode subtreeFilter,
3105
			MatchMode matchmode, boolean includeEntity, Integer pageSize,
3106
			Integer pageNumber,	List<String> propertyPaths) {
3107
		if (subtreeFilter == null){
3108
			return findByIdentifier(clazz, identifier, identifierType, matchmode, includeEntity, pageSize, pageNumber, propertyPaths);
3109
		}
3110

    
3111
		Integer numberOfResults = dao.countByIdentifier(clazz, identifier, identifierType, subtreeFilter, matchmode);
3112
        List<Object[]> daoResults = new ArrayList<>();
3113
        if(numberOfResults > 0) { // no point checking again
3114
        	daoResults = dao.findByIdentifier(clazz, identifier, identifierType, subtreeFilter,
3115
    				matchmode, includeEntity, pageSize, pageNumber, propertyPaths);
3116
        }
3117

    
3118
        List<IdentifiedEntityDTO<S>> result = new ArrayList<>();
3119
        for (Object[] daoObj : daoResults){
3120
        	if (includeEntity){
3121
        		result.add(new IdentifiedEntityDTO<S>((DefinedTerm)daoObj[0], (String)daoObj[1], (S)daoObj[2]));
3122
        	}else{
3123
        		result.add(new IdentifiedEntityDTO<S>((DefinedTerm)daoObj[0], (String)daoObj[1], (UUID)daoObj[2], (String)daoObj[3]));
3124
        	}
3125
        }
3126
		return new DefaultPagerImpl<IdentifiedEntityDTO<S>>(pageNumber, numberOfResults, pageSize, result);
3127
	}
3128

    
3129
	@Override
3130
    @Transactional(readOnly = true)
3131
    public <S extends TaxonBase> Pager<MarkedEntityDTO<S>> findByMarker(
3132
            Class<S> clazz, MarkerType markerType, Boolean markerValue,
3133
            TaxonNode subtreeFilter, boolean includeEntity, TaxonTitleType titleType,
3134
            Integer pageSize, Integer pageNumber, List<String> propertyPaths) {
3135
        if (subtreeFilter == null){
3136
            return super.findByMarker (clazz, markerType, markerValue, includeEntity, pageSize, pageNumber, propertyPaths);
3137
        }
3138

    
3139
        Long numberOfResults = dao.countByMarker(clazz, markerType, markerValue, subtreeFilter);
3140
        List<Object[]> daoResults = new ArrayList<>();
3141
        if(numberOfResults > 0) { // no point checking again
3142
            daoResults = dao.findByMarker(clazz, markerType, markerValue, subtreeFilter,
3143
                    includeEntity, titleType, pageSize, pageNumber, propertyPaths);
3144
        }
3145

    
3146
        List<MarkedEntityDTO<S>> result = new ArrayList<>();
3147
        for (Object[] daoObj : daoResults){
3148
            if (includeEntity){
3149
                result.add(new MarkedEntityDTO<S>((MarkerType)daoObj[0], (Boolean)daoObj[1], (S)daoObj[2]));
3150
            }else{
3151
                result.add(new MarkedEntityDTO<S>((MarkerType)daoObj[0], (Boolean)daoObj[1], (UUID)daoObj[2], (String)daoObj[3]));
3152
            }
3153
        }
3154
        return new DefaultPagerImpl<MarkedEntityDTO<S>>(pageNumber, numberOfResults, pageSize, result);
3155
    }
3156

    
3157
    @Override
3158
	@Transactional(readOnly = false)
3159
	public UpdateResult moveSynonymToAnotherTaxon(Synonym oldSynonym, UUID newTaxonUUID, boolean moveHomotypicGroup,
3160
            SynonymType newSynonymType, Reference newSecundum, String newSecundumDetail,
3161
            boolean keepSecundumIfUndefined) throws HomotypicalGroupChangeException {
3162

    
3163
	    UpdateResult result = new UpdateResult();
3164
		Taxon newTaxon = CdmBase.deproxy(dao.load(newTaxonUUID),Taxon.class);
3165
		result = moveSynonymToAnotherTaxon(oldSynonym, newTaxon, moveHomotypicGroup, newSynonymType,
3166
		        newSecundum, newSecundumDetail, keepSecundumIfUndefined);
3167

    
3168
		return result;
3169
	}
3170

    
3171
	@Override
3172
	public UpdateResult moveFactualDateToAnotherTaxon(UUID fromTaxonUuid, UUID toTaxonUuid){
3173
		UpdateResult result = new UpdateResult();
3174

    
3175
		Taxon fromTaxon = (Taxon)dao.load(fromTaxonUuid);
3176
		Taxon toTaxon = (Taxon) dao.load(toTaxonUuid);
3177
    	for(TaxonDescription description : fromTaxon.getDescriptions()){
3178
              //reload to avoid session conflicts
3179
              description = HibernateProxyHelper.deproxy(description, TaxonDescription.class);
3180

    
3181
              String moveMessage = String.format("Description moved from %s", fromTaxon);
3182
              if(description.isProtectedTitleCache()){
3183
                  String separator = "";
3184
                  if(!StringUtils.isBlank(description.getTitleCache())){
3185
                      separator = " - ";
3186
                  }
3187
                  description.setTitleCache(description.getTitleCache() + separator + moveMessage, true);
3188
              }
3189
              Annotation annotation = Annotation.NewInstance(moveMessage, Language.getDefaultLanguage());
3190
              annotation.setAnnotationType(AnnotationType.TECHNICAL());
3191
              description.addAnnotation(annotation);
3192
              toTaxon.addDescription(description);
3193
              dao.saveOrUpdate(toTaxon);
3194
              dao.saveOrUpdate(fromTaxon);
3195
              result.addUpdatedObject(toTaxon);
3196
              result.addUpdatedObject(fromTaxon);
3197

    
3198
        }
3199

    
3200
    	return result;
3201
	}
3202

    
3203
	@Override
3204
	@Transactional(readOnly = false)
3205
	public UpdateResult swapSynonymAndAcceptedTaxon(UUID synonymUUid,
3206
			UUID acceptedTaxonUuid) {
3207
		TaxonBase base = this.load(synonymUUid);
3208
		Synonym syn = HibernateProxyHelper.deproxy(base, Synonym.class);
3209
		base = this.load(acceptedTaxonUuid);
3210
		Taxon taxon = HibernateProxyHelper.deproxy(base, Taxon.class);
3211

    
3212
		return this.swapSynonymAndAcceptedTaxon(syn, taxon);
3213
	}
3214

    
3215
	@Override
3216
	public UUID saveOrUpdate(TaxonBase taxonbase){
3217
	    if (taxonbase.getName()!= null && taxonbase.getName().getId() > 0){
3218
	        TaxonName name = taxonbase.getName();
3219
	        name = nameService.load(name.getUuid());
3220
	        taxonbase.setName(name);
3221
	    }
3222
	    return super.saveOrUpdate(taxonbase);
3223
	}
3224

    
3225
}
(94-94/101)