Project

General

Profile

Download (167 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.Collections;
15
import java.util.EnumSet;
16
import java.util.HashMap;
17
import java.util.HashSet;
18
import java.util.Iterator;
19
import java.util.List;
20
import java.util.Map;
21
import java.util.Set;
22
import java.util.UUID;
23

    
24
import javax.persistence.EntityNotFoundException;
25

    
26
import org.apache.commons.lang3.StringUtils;
27
import org.apache.log4j.Logger;
28
import org.apache.lucene.queryparser.classic.ParseException;
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.hibernate.criterion.Criterion;
38
import org.springframework.beans.factory.annotation.Autowired;
39
import org.springframework.stereotype.Service;
40
import org.springframework.transaction.annotation.Transactional;
41

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

    
141

    
142
/**
143
 * @author a.kohlbecker
144
 * @since 10.09.2010
145
 */
146
@Service
147
@Transactional(readOnly = true)
148
public class TaxonServiceImpl
149
            extends IdentifiableServiceBase<TaxonBase,ITaxonDao>
150
            implements ITaxonService{
151

    
152
    private static final Logger logger = Logger.getLogger(TaxonServiceImpl.class);
153

    
154
    public static final String POTENTIAL_COMBINATION_NAMESPACE = "Potential combination";
155

    
156
    public static final String INFERRED_EPITHET_NAMESPACE = "Inferred epithet";
157

    
158
    public static final String INFERRED_GENUS_NAMESPACE = "Inferred genus";
159

    
160
    @Autowired
161
    private ITaxonNodeDao taxonNodeDao;
162

    
163
    @Autowired
164
    private ITaxonNameDao nameDao;
165

    
166
    @Autowired
167
    private INameService nameService;
168

    
169
    @Autowired
170
    private IOccurrenceService occurrenceService;
171

    
172
    @Autowired
173
    private ITaxonNodeService nodeService;
174

    
175
    @Autowired
176
    private IDescriptionService descriptionService;
177

    
178
    @Autowired
179
    private IReferenceService referenceService;
180
//
181
//    @Autowired
182
//    private IOrderedTermVocabularyDao orderedVocabularyDao;
183

    
184
    @Autowired
185
    private IOccurrenceDao occurrenceDao;
186

    
187
    @Autowired
188
    private IClassificationDao classificationDao;
189

    
190
    @Autowired
191
    private AbstractBeanInitializer beanInitializer;
192

    
193
    @Autowired
194
    private ILuceneIndexToolProvider luceneIndexToolProvider;
195

    
196
//************************ CONSTRUCTOR ****************************/
197
    public TaxonServiceImpl(){
198
        if (logger.isDebugEnabled()) { logger.debug("Load TaxonService Bean"); }
199
    }
200

    
201
// ****************************** METHODS ********************************/
202

    
203
    @Override
204
    public TaxonBase load(UUID uuid, boolean includeUnpublished, List<String> propertyPaths) {
205
        return dao.load(uuid, includeUnpublished, propertyPaths);
206
    }
207

    
208
    @Override
209
    public List<TaxonBase> searchByName(String name, boolean includeUnpublished, Reference sec) {
210
        return dao.getTaxaByName(name, includeUnpublished, sec);
211
    }
212

    
213
    @Override
214
    @Transactional(readOnly = false)
215
    public UpdateResult swapSynonymAndAcceptedTaxon(Synonym synonym, Taxon acceptedTaxon, boolean setNameInSource, boolean newUuidForAcceptedTaxon, SecReferenceHandlingSwapEnum secHandling, Reference newSecAcc, Reference newSecSyn){
216
        if (newUuidForAcceptedTaxon){
217
            return swapSynonymAndAcceptedTaxonNewUuid(synonym, acceptedTaxon, setNameInSource, secHandling, newSecAcc, newSecSyn);
218
        }else{
219
            return swapSynonymAndAcceptedTaxon(synonym, acceptedTaxon, setNameInSource, secHandling, newSecAcc, newSecSyn);
220
        }
221
    }
222

    
223
    private UpdateResult swapSynonymAndAcceptedTaxon(Synonym synonym, Taxon acceptedTaxon, boolean setNameInSource, SecReferenceHandlingSwapEnum secHandling, Reference newSecAcc, Reference newSecSyn){
224
        UpdateResult result = new UpdateResult();
225
        String oldTaxonTitleCache = acceptedTaxon.getTitleCache();
226

    
227
    	TaxonName synonymName = synonym.getName();
228
    	TaxonName taxonName = HibernateProxyHelper.deproxy(acceptedTaxon.getName());
229
    	Reference secAccepted = acceptedTaxon.getSec();
230
    	String microRefSecAccepted = acceptedTaxon.getSecMicroReference();
231
    	Reference secSynonym = synonym.getSec();
232
        String microRefSecSynonym = synonym.getSecMicroReference();
233

    
234
//        if (secHandling.equals(SecReferenceHandlingSwapEnum.AlwaysDelete) || (secAccepted != null && secSynonym != null && !secAccepted.getUuid().equals(secSynonym.getUuid()) && (secHandling.equals(SecReferenceHandlingSwapEnum.AlwaysSelect) || secHandling.equals(SecReferenceHandlingSwapEnum.KeepOrSelect)))){
235
//            secAccepted = null;
236
//            microRefSecAccepted = null;
237
//            secSynonym = null;
238
//            microRefSecSynonym = null;
239
//        }
240

    
241

    
242
    	Set<ExternalLink> accLinks = new HashSet<>();
243
    	if (acceptedTaxon.getSecSource() != null){
244
        	for (ExternalLink link: acceptedTaxon.getSecSource().getLinks()){
245
                accLinks.add(ExternalLink.NewInstance(ExternalLinkType.Unknown, link.getUri()));
246
            }
247
    	}
248
    	acceptedTaxon.setName(synonymName);
249
    	acceptedTaxon.setSec(newSecAcc);
250
//    	acceptedTaxon.setSecMicroReference(synonym.getSecMicroReference());
251

    
252
//    	if (synonym.getSecSource()!= null && synonym.getSecSource().getLinks() != null){
253
//    	    acceptedTaxon.getSecSource().getLinks().clear();
254
//        	for (ExternalLink link: synonym.getSecSource().getLinks()){
255
//        	    acceptedTaxon.getSecSource().addLink(ExternalLink.NewInstance(ExternalLinkType.Unknown, link.getUri()));
256
//            }
257
//    	}
258

    
259
    	synonym.setName(taxonName);
260
    	synonym.setSec(newSecSyn);
261
//        synonym.setSecMicroReference(microRefSecAccepted);
262
//        if (synonym.getSecSource() != null){
263
//            synonym.getSecSource().getLinks().clear();
264
//            for (ExternalLink link: accLinks){
265
//                synonym.getSecSource().addLink(link);
266
//            }
267
//        }
268

    
269
    	//nameUsedInSource
270
    	handleNameUsedInSourceForSwap(setNameInSource, taxonName, oldTaxonTitleCache, acceptedTaxon.getDescriptions());
271

    
272
    	acceptedTaxon.resetTitleCache();
273
    	synonym.resetTitleCache();
274

    
275
    	MergeResult mergeTaxon = merge(acceptedTaxon, true);
276
    	MergeResult mergeSynonym = merge(synonym, true);
277
    	result.setCdmEntity((CdmBase) mergeTaxon.getMergedEntity());
278
    	result.addUpdatedObject((CdmBase) mergeSynonym.getMergedEntity());
279

    
280
		return result;
281
    }
282

    
283
    private UpdateResult swapSynonymAndAcceptedTaxonNewUuid(Synonym synonym, Taxon acceptedTaxon, boolean setNameInSource, SecReferenceHandlingSwapEnum secHandling, Reference newSecAcc, Reference newSecSyn){
284
        UpdateResult result = new UpdateResult();
285
        acceptedTaxon.removeSynonym(synonym);
286
        TaxonName synonymName = synonym.getName();
287
        TaxonName taxonName = HibernateProxyHelper.deproxy(acceptedTaxon.getName());
288
        String oldTaxonTitleCache = acceptedTaxon.getTitleCache();
289

    
290
        boolean sameHomotypicGroup = synonymName.getHomotypicalGroup().equals(taxonName.getHomotypicalGroup());
291
        synonymName.removeTaxonBase(synonym);
292

    
293
        List<Synonym> synonyms = new ArrayList<>();
294
        for (Synonym syn: acceptedTaxon.getSynonyms()){
295
            syn = HibernateProxyHelper.deproxy(syn, Synonym.class);
296
            synonyms.add(syn);
297
        }
298
        for (Synonym syn: synonyms){
299
            acceptedTaxon.removeSynonym(syn);
300
        }
301
        Taxon newTaxon = acceptedTaxon.clone(true, true, false, true);
302
        newTaxon.setSec(newSecAcc);
303

    
304
        //move descriptions
305
        Set<TaxonDescription> descriptionsToCopy = new HashSet<>(acceptedTaxon.getDescriptions());
306
        for (TaxonDescription description: descriptionsToCopy){
307
            newTaxon.addDescription(description);
308
        }
309
        //nameUsedInSource
310
        handleNameUsedInSourceForSwap(setNameInSource, taxonName, oldTaxonTitleCache, newTaxon.getDescriptions());
311

    
312
        newTaxon.setName(synonymName);
313

    
314
        newTaxon.setPublish(synonym.isPublish());
315
        for (Synonym syn: synonyms){
316
            if (!syn.getName().equals(newTaxon.getName())){
317
                newTaxon.addSynonym(syn, syn.getType());
318
            }
319
        }
320

    
321
        //move all data to new taxon
322
        //Move Taxon RelationShips to new Taxon
323
        for(TaxonRelationship taxonRelationship : newTaxon.getTaxonRelations()){
324
            newTaxon.removeTaxonRelation(taxonRelationship);
325
        }
326

    
327
        for(TaxonRelationship taxonRelationship : acceptedTaxon.getTaxonRelations()){
328
            Taxon fromTaxon = HibernateProxyHelper.deproxy(taxonRelationship.getFromTaxon());
329
            Taxon toTaxon = HibernateProxyHelper.deproxy(taxonRelationship.getToTaxon());
330
            if (fromTaxon == acceptedTaxon){
331
                newTaxon.addTaxonRelation(taxonRelationship.getToTaxon(), taxonRelationship.getType(),
332
                        taxonRelationship.getCitation(), taxonRelationship.getCitationMicroReference());
333

    
334
            }else if(toTaxon == acceptedTaxon){
335
               fromTaxon.addTaxonRelation(newTaxon, taxonRelationship.getType(),
336
                        taxonRelationship.getCitation(), taxonRelationship.getCitationMicroReference());
337
               saveOrUpdate(fromTaxon);
338

    
339
            }else{
340
                logger.warn("Taxon is not part of its own Taxonrelationship");
341
            }
342

    
343
            // Remove old relationships
344
            fromTaxon.removeTaxonRelation(taxonRelationship);
345
            toTaxon.removeTaxonRelation(taxonRelationship);
346
            taxonRelationship.setToTaxon(null);
347
            taxonRelationship.setFromTaxon(null);
348
        }
349

    
350
        //taxon nodes
351
        List<TaxonNode> nodes = new ArrayList<>(acceptedTaxon.getTaxonNodes());
352
        for (TaxonNode node: nodes){
353
            node = HibernateProxyHelper.deproxy(node);
354
            TaxonNode parent = node.getParent();
355
            acceptedTaxon.removeTaxonNode(node);
356
            node.setTaxon(newTaxon);
357
            if (parent != null){
358
                parent.addChildNode(node, null, null);
359
            }
360
        }
361

    
362
        //synonym
363
        Synonym newSynonym = synonym.clone();
364
        newSynonym.setName(taxonName);
365
        newSynonym.setPublish(acceptedTaxon.isPublish());
366
        newSynonym.setSec(newSecSyn);
367
        if (sameHomotypicGroup){
368
            newTaxon.addSynonym(newSynonym, SynonymType.HOMOTYPIC_SYNONYM_OF());
369
        }else{
370
            newTaxon.addSynonym(newSynonym, SynonymType.HETEROTYPIC_SYNONYM_OF());
371
        }
372

    
373
        //deletes
374
        TaxonDeletionConfigurator conf = new TaxonDeletionConfigurator();
375
        conf.setDeleteNameIfPossible(false);
376
        SynonymDeletionConfigurator confSyn = new SynonymDeletionConfigurator();
377
        confSyn.setDeleteNameIfPossible(false);
378
        result.setCdmEntity(newTaxon);
379

    
380
        DeleteResult deleteResult = deleteTaxon(acceptedTaxon.getUuid(), conf, null);
381
        if (synonym.isPersited()){
382
            synonym.setSecSource(null);
383
            deleteResult.includeResult(deleteSynonym(synonym.getUuid(), confSyn));
384
        }
385
        result.includeResult(deleteResult);
386

    
387
        return result;
388
    }
389

    
390
    private void handleNameUsedInSourceForSwap(boolean setNameInSource, TaxonName taxonName, String oldTaxonTitleCache,
391
            Set<TaxonDescription> descriptions) {
392
        for(TaxonDescription description : descriptions){
393
            String message = "Description copied from former accepted taxon: %s (Old title: %s)";
394
            message = String.format(message, oldTaxonTitleCache, description.getTitleCache());
395
            description.setTitleCache(message, true);
396
            if(setNameInSource){
397
                for (DescriptionElementBase element: description.getElements()){
398
                    for (DescriptionElementSource source: element.getSources()){
399
                        if (source.getNameUsedInSource() == null){
400
                            source.setNameUsedInSource(taxonName);
401
                        }
402
                    }
403
                }
404
            }
405
        }
406
    }
407

    
408
    @Override
409
    @Transactional(readOnly = false)
410
    public UpdateResult changeSynonymToAcceptedTaxon(Synonym synonym, Taxon acceptedTaxon, Reference newSecRef, String microRef, SecReferenceHandlingEnum secHandling, boolean deleteSynonym) {
411
        UpdateResult result = new UpdateResult();
412
        TaxonName acceptedName = acceptedTaxon.getName();
413
        TaxonName synonymName = synonym.getName();
414
        HomotypicalGroup synonymHomotypicGroup = synonymName.getHomotypicalGroup();
415

    
416
        //check synonym is not homotypic
417
        if (acceptedName.getHomotypicalGroup().equals(synonymHomotypicGroup)){
418
            String message = "The accepted taxon and the synonym are part of the same homotypical group and therefore can not be both accepted.";
419
            result.addException(new HomotypicalGroupChangeException(message));
420
            result.setAbort();
421
            return result;
422
        }
423

    
424
        Taxon newAcceptedTaxon = Taxon.NewInstance(synonymName, newSecRef, microRef);
425
        newAcceptedTaxon.setPublish(synonym.isPublish());
426
        dao.save(newAcceptedTaxon);
427
        result.setCdmEntity(newAcceptedTaxon);
428
        SynonymType relTypeForGroup = SynonymType.HOMOTYPIC_SYNONYM_OF();
429
        List<Synonym> heteroSynonyms = acceptedTaxon.getSynonymsInGroup(synonymHomotypicGroup);
430

    
431
        for (Synonym heteroSynonym : heteroSynonyms){
432
            if (secHandling == null){
433
                heteroSynonym.setSec(newSecRef);
434
            }
435
            if (synonym.equals(heteroSynonym)){
436
                acceptedTaxon.removeSynonym(heteroSynonym, false);
437
            }else{
438
                //move synonyms in same homotypic group to new accepted taxon
439
                newAcceptedTaxon.addSynonym(heteroSynonym, relTypeForGroup);
440
            }
441
        }
442
        dao.saveOrUpdate(acceptedTaxon);
443
        result.addUpdatedObject(acceptedTaxon);
444
        if (deleteSynonym){
445

    
446
            try {
447
                this.dao.flush();
448
                SynonymDeletionConfigurator config = new SynonymDeletionConfigurator();
449
                config.setDeleteNameIfPossible(false);
450
                this.deleteSynonym(synonym, config);
451

    
452
            } catch (Exception e) {
453
                result.addException(e);
454
            }
455
        }
456

    
457
        return result;
458
    }
459

    
460
    @Override
461
    @Transactional(readOnly = false)
462
    public UpdateResult changeSynonymToAcceptedTaxon(UUID synonymUuid,
463
            UUID acceptedTaxonUuid,
464
            UUID newParentNodeUuid,
465
            UUID newSec,
466
            String microReference,
467
            SecReferenceHandlingEnum secHandling,
468
            boolean deleteSynonym)  {
469
        UpdateResult result = new UpdateResult();
470
        Synonym synonym = CdmBase.deproxy(dao.load(synonymUuid), Synonym.class);
471
        Taxon acceptedTaxon = CdmBase.deproxy(dao.load(acceptedTaxonUuid), Taxon.class);
472
        TaxonNode newParentNode = taxonNodeDao.load(newParentNodeUuid);
473
        Reference newSecRef = null;
474
        switch (secHandling){
475
            case AlwaysDelete:
476
                newSecRef = null;
477
                break;
478
            case UseNewParentSec:
479
                newSecRef = newParentNode.getTaxon() != null? newParentNode.getTaxon().getSec(): null;
480
                break;
481
            case KeepOrWarn:
482
                Reference parentSec = newParentNode.getTaxon() != null? newParentNode.getTaxon().getSec(): null;
483
                Reference synSec = synonym.getSec();
484
                if (synSec != null ){
485
                    newSecRef = CdmBase.deproxy(synSec);
486
                }else{
487
                    newSecRef = CdmBase.deproxy(referenceService.load(newSec));
488
                }
489
                break;
490
            case KeepOrSelect:
491
                newSecRef = CdmBase.deproxy(referenceService.load(newSec));
492
                break;
493
            default:
494
                break;
495
        }
496

    
497
        result =  changeSynonymToAcceptedTaxon(synonym, acceptedTaxon, newSecRef, microReference, secHandling, deleteSynonym);
498
        Taxon newTaxon = (Taxon)result.getCdmEntity();
499

    
500
        TaxonNode newNode = newParentNode.addChildTaxon(newTaxon, null, null);
501
        taxonNodeDao.save(newNode);
502
        result.addUpdatedObject(newTaxon);
503
        result.addUpdatedObject(acceptedTaxon);
504
        result.setCdmEntity(newNode);
505
        return result;
506
    }
507

    
508
    @Override
509
    @Transactional(readOnly = false)
510
    public UpdateResult changeSynonymToRelatedTaxon(UUID synonymUuid,
511
            UUID toTaxonUuid,
512
            TaxonRelationshipType taxonRelationshipType,
513
            Reference citation,
514
            String microcitation){
515

    
516
        UpdateResult result = new UpdateResult();
517
        Taxon toTaxon = (Taxon) dao.load(toTaxonUuid);
518
        Synonym synonym = (Synonym) dao.load(synonymUuid);
519
        result = changeSynonymToRelatedTaxon(synonym, toTaxon, taxonRelationshipType, citation, microcitation);
520
        Taxon relatedTaxon = (Taxon)result.getCdmEntity();
521
//        result.setCdmEntity(relatedTaxon);
522
        result.addUpdatedObject(relatedTaxon);
523
        result.addUpdatedObject(toTaxon);
524
        return result;
525
    }
526

    
527
    @Override
528
    @Transactional(readOnly = false)
529
    public UpdateResult changeSynonymToRelatedTaxon(Synonym synonym, Taxon toTaxon, TaxonRelationshipType taxonRelationshipType, Reference citation, String microcitation){
530
        // Get name from synonym
531
        if (synonym == null){
532
            return null;
533
        }
534

    
535
        UpdateResult result = new UpdateResult();
536

    
537
        TaxonName synonymName = synonym.getName();
538

    
539
      /*  // remove synonym from taxon
540
        toTaxon.removeSynonym(synonym);
541
*/
542
        // Create a taxon with synonym name
543
        Taxon fromTaxon = Taxon.NewInstance(synonymName, null);
544
        fromTaxon.setPublish(synonym.isPublish());
545
        save(fromTaxon);
546
        fromTaxon.setAppendedPhrase(synonym.getAppendedPhrase());
547

    
548
        // Add taxon relation
549
        fromTaxon.addTaxonRelation(toTaxon, taxonRelationshipType, citation, microcitation);
550
        result.setCdmEntity(fromTaxon);
551
        // since we are swapping names, we have to detach the name from the synonym completely.
552
        // Otherwise the synonym will still be in the list of typified names.
553
       // synonym.getName().removeTaxonBase(synonym);
554
        result.includeResult(this.deleteSynonym(synonym, null));
555

    
556
        return result;
557
    }
558

    
559
    @Transactional(readOnly = false)
560
    @Override
561
    public void changeHomotypicalGroupOfSynonym(Synonym synonym, HomotypicalGroup newHomotypicalGroup,
562
            Taxon targetTaxon, boolean setBasionymRelationIfApplicable){
563
        // Get synonym name
564
        TaxonName synonymName = synonym.getName();
565
        HomotypicalGroup oldHomotypicalGroup = synonymName.getHomotypicalGroup();
566

    
567
        // Switch groups
568
        oldHomotypicalGroup.removeTypifiedName(synonymName, false);
569
        newHomotypicalGroup.addTypifiedName(synonymName);
570

    
571
        //remove existing basionym relationships
572
        synonymName.removeBasionyms();
573

    
574
        //add basionym relationship
575
        if (setBasionymRelationIfApplicable){
576
            Set<TaxonName> basionyms = newHomotypicalGroup.getBasionyms();
577
            for (TaxonName basionym : basionyms){
578
                synonymName.addBasionym(basionym);
579
            }
580
        }
581

    
582
        //set synonym relationship correctly
583
        Taxon acceptedTaxon = synonym.getAcceptedTaxon();
584

    
585
        boolean hasNewTargetTaxon = targetTaxon != null && !targetTaxon.equals(acceptedTaxon);
586
        if (acceptedTaxon != null){
587

    
588
            HomotypicalGroup acceptedGroup = acceptedTaxon.getHomotypicGroup();
589
            boolean isHomotypicToTaxon = acceptedGroup.equals(newHomotypicalGroup);
590
            SynonymType newRelationType = isHomotypicToTaxon? SynonymType.HOMOTYPIC_SYNONYM_OF() : SynonymType.HETEROTYPIC_SYNONYM_OF();
591
            synonym.setType(newRelationType);
592

    
593
            if (hasNewTargetTaxon){
594
                acceptedTaxon.removeSynonym(synonym, false);
595
            }
596
        }
597
        if (hasNewTargetTaxon ){
598
            @SuppressWarnings("null")
599
            HomotypicalGroup acceptedGroup = targetTaxon.getHomotypicGroup();
600
            boolean isHomotypicToTaxon = acceptedGroup.equals(newHomotypicalGroup);
601
            SynonymType relType = isHomotypicToTaxon? SynonymType.HOMOTYPIC_SYNONYM_OF() : SynonymType.HETEROTYPIC_SYNONYM_OF();
602
            targetTaxon.addSynonym(synonym, relType);
603
        }
604
    }
605

    
606
    @Override
607
    @Transactional(readOnly = false)
608
    public UpdateResult updateCaches(Class<? extends TaxonBase> clazz, Integer stepSize, IIdentifiableEntityCacheStrategy<TaxonBase> cacheStrategy, IProgressMonitor monitor) {
609
        if (clazz == null){
610
            clazz = TaxonBase.class;
611
        }
612
        return super.updateCachesImpl(clazz, stepSize, cacheStrategy, monitor);
613
    }
614

    
615
    @Override
616
    @Autowired
617
    protected void setDao(ITaxonDao dao) {
618
        this.dao = dao;
619
    }
620

    
621
    @Override
622
    public <T extends TaxonBase> Pager<T> findTaxaByName(Class<T> clazz, String uninomial,	String infragenericEpithet, String specificEpithet,
623
            String infraspecificEpithet, String authorshipCache, Rank rank, Integer pageSize,Integer pageNumber, List<String> propertyPaths) {
624
        long numberOfResults = dao.countTaxaByName(clazz, uninomial, infragenericEpithet, specificEpithet, infraspecificEpithet, authorshipCache, rank);
625

    
626
        List<T> results = new ArrayList<>();
627
        if(numberOfResults > 0) { // no point checking again
628
            results = dao.findTaxaByName(clazz, uninomial, infragenericEpithet, specificEpithet, infraspecificEpithet, authorshipCache, rank,
629
                    pageSize, pageNumber, propertyPaths);
630
        }
631

    
632
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
633
    }
634

    
635
    @Override
636
    public <T extends TaxonBase> List<T> listTaxaByName(Class<T> clazz, String uninomial, String infragenericEpithet, String specificEpithet,
637
            String infraspecificEpithet, String authorshipCache, Rank rank, Integer pageSize,Integer pageNumber, List<String> propertyPaths) {
638

    
639
        return findTaxaByName(clazz, uninomial, infragenericEpithet, specificEpithet, infragenericEpithet, authorshipCache, rank,
640
                pageSize, pageNumber, propertyPaths).getRecords();
641
    }
642

    
643
    @Override
644
    public List<TaxonRelationship> listToTaxonRelationships(Taxon taxon, TaxonRelationshipType type,
645
            boolean includeUnpublished, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths){
646
        long numberOfResults = dao.countTaxonRelationships(taxon, type, includeUnpublished, TaxonRelationship.Direction.relatedTo);
647

    
648
        List<TaxonRelationship> results = new ArrayList<>();
649
        if(numberOfResults > 0) { // no point checking again
650
            results = dao.getTaxonRelationships(taxon, type, includeUnpublished, pageSize, pageNumber, orderHints, propertyPaths, TaxonRelationship.Direction.relatedTo);
651
        }
652
        return results;
653
    }
654

    
655
    @Override
656
    public Pager<TaxonRelationship> pageToTaxonRelationships(Taxon taxon, TaxonRelationshipType type,
657
            boolean includeUnpublished, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) {
658
        long numberOfResults = dao.countTaxonRelationships(taxon, type, includeUnpublished, TaxonRelationship.Direction.relatedTo);
659

    
660
        List<TaxonRelationship> results = new ArrayList<>();
661
        if(numberOfResults > 0) { // no point checking again
662
            results = dao.getTaxonRelationships(taxon, type, includeUnpublished, pageSize, pageNumber, orderHints, propertyPaths, TaxonRelationship.Direction.relatedTo);
663
        }
664
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
665
    }
666

    
667
    @Override
668
    public List<TaxonRelationship> listFromTaxonRelationships(Taxon taxon, TaxonRelationshipType type,
669
            boolean includeUnpublished, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths){
670
        long numberOfResults = dao.countTaxonRelationships(taxon, type, includeUnpublished, TaxonRelationship.Direction.relatedFrom);
671

    
672
        List<TaxonRelationship> results = new ArrayList<>();
673
        if(numberOfResults > 0) { // no point checking again
674
            results = dao.getTaxonRelationships(taxon, type, includeUnpublished, pageSize, pageNumber, orderHints, propertyPaths, TaxonRelationship.Direction.relatedFrom);
675
        }
676
        return results;
677
    }
678

    
679
    @Override
680
    public Pager<TaxonRelationship> pageFromTaxonRelationships(Taxon taxon, TaxonRelationshipType type,
681
            boolean includeUnpublished, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) {
682
        long numberOfResults = dao.countTaxonRelationships(taxon, type, includeUnpublished, TaxonRelationship.Direction.relatedFrom);
683

    
684
        List<TaxonRelationship> results = new ArrayList<>();
685
        if(numberOfResults > 0) { // no point checking again
686
            results = dao.getTaxonRelationships(taxon, type, includeUnpublished, pageSize, pageNumber, orderHints, propertyPaths, TaxonRelationship.Direction.relatedFrom);
687
        }
688
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
689
    }
690

    
691
    @Override
692
    public List<TaxonRelationship> listTaxonRelationships(Set<TaxonRelationshipType> types,
693
            Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) {
694

    
695
        Long numberOfResults = dao.countTaxonRelationships(types);
696
        List<TaxonRelationship> results = new ArrayList<>();
697
        if(numberOfResults > 0) {
698
            results = dao.getTaxonRelationships(types, pageSize, pageNumber, orderHints, propertyPaths);
699
        }
700
        return results;
701
    }
702

    
703
    @Override
704
    public <S extends TaxonBase> Pager<S> page(Class<S> clazz, List<Restriction<?>> restrictions, Integer pageSize,
705
            Integer pageIndex, List<OrderHint> orderHints, List<String> propertyPaths) {
706
        return page(clazz, restrictions, pageSize, pageIndex, orderHints, propertyPaths, INCLUDE_UNPUBLISHED);
707
    }
708

    
709
    @Override
710
    public <S extends TaxonBase> Pager<S> page(Class<S> clazz, List<Restriction<?>> restrictions, Integer pageSize,
711
            Integer pageIndex, List<OrderHint> orderHints, List<String> propertyPaths, boolean includeUnpublished) {
712

    
713
        List<S> records;
714
        long resultSize = dao.count(clazz, restrictions);
715
        if(AbstractPagerImpl.hasResultsInRange(resultSize, pageIndex, pageSize)){
716
            records = dao.list(clazz, restrictions, pageSize, pageIndex, orderHints, propertyPaths, includeUnpublished);
717
        } else {
718
            records = new ArrayList<>();
719
        }
720
        Pager<S> pager = new DefaultPagerImpl<>(pageIndex, resultSize, pageSize, records);
721
        return pager;
722
    }
723

    
724
    @Override
725
    public Taxon findAcceptedTaxonFor(UUID synonymUuid, UUID classificationUuid,
726
            boolean includeUnpublished, List<String> propertyPaths) throws UnpublishedException{
727

    
728
        Synonym synonym = null;
729

    
730
        try {
731
            synonym = (Synonym) dao.load(synonymUuid);
732
            checkPublished(synonym, includeUnpublished, "Synoym is unpublished.");
733
        } catch (ClassCastException e){
734
            throw new EntityNotFoundException("The TaxonBase entity referenced by " + synonymUuid + " is not a Synonmy");
735
        } catch (NullPointerException e){
736
            throw new EntityNotFoundException("No TaxonBase entity found for " + synonymUuid);
737
        }
738

    
739
        Classification classificationFilter = null;
740
        if(classificationUuid != null){
741
            try {
742
                classificationFilter = classificationDao.load(classificationUuid);
743
            } catch (NullPointerException e){
744
                //TODO not sure, why an NPE should be thrown in the above load method
745
                throw new EntityNotFoundException("No Classification entity found for " + classificationUuid);
746
            }
747
            if(classificationFilter == null){
748
                throw new EntityNotFoundException("No Classification entity found for " + classificationUuid);
749
            }
750
        }
751

    
752
        long count = dao.countAcceptedTaxonFor(synonym, classificationFilter) ;
753
        if(count > 0){
754
            Taxon result = dao.acceptedTaxonFor(synonym, classificationFilter, propertyPaths);
755
            checkPublished(result, includeUnpublished, "Accepted taxon unpublished");
756
            return result;
757
        }else{
758
            return null;
759
        }
760
    }
761

    
762
    @Override
763
    public Set<Taxon> listRelatedTaxa(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships, Integer maxDepth,
764
            boolean includeUnpublished, Integer limit, Integer start, List<String> propertyPaths) {
765

    
766
        Set<Taxon> relatedTaxa = collectRelatedTaxa(taxon, includeRelationships, new HashSet<>(), includeUnpublished, maxDepth);
767
        relatedTaxa.remove(taxon);
768
        beanInitializer.initializeAll(relatedTaxa, propertyPaths);
769
        return relatedTaxa;
770
    }
771

    
772
    /**
773
     * Recursively collect related taxa for the given <code>taxon</code> . The returned list will also include the
774
     *  <code>taxon</code> supplied as parameter.
775
     *
776
     * @param taxon
777
     * @param includeRelationships
778
     * @param taxa
779
     * @param maxDepth can be <code>null</code> for infinite depth
780
     * @return
781
     */
782
    private Set<Taxon> collectRelatedTaxa(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships, Set<Taxon> taxa,
783
            boolean includeUnpublished, Integer maxDepth) {
784

    
785
        if(taxa.isEmpty()) {
786
            taxa.add(taxon);
787
        }
788

    
789
        if(includeRelationships.isEmpty()){
790
            return taxa;
791
        }
792

    
793
        if(maxDepth != null) {
794
            maxDepth--;
795
        }
796
        if(logger.isDebugEnabled()){
797
            logger.debug("collecting related taxa for " + taxon + " with maxDepth=" + maxDepth);
798
        }
799
        List<TaxonRelationship> taxonRelationships = dao.getTaxonRelationships(taxon,
800
                (Set<TaxonRelationshipType>)null, includeUnpublished, null, null, null, null, null);
801
        for (TaxonRelationship taxRel : taxonRelationships) {
802

    
803
            // skip invalid data
804
            if (taxRel.getToTaxon() == null || taxRel.getFromTaxon() == null || taxRel.getType() == null) {
805
                continue;
806
            }
807
            // filter by includeRelationships
808
            for (TaxonRelationshipEdge relationshipEdgeFilter : includeRelationships) {
809
                if ( relationshipEdgeFilter.getRelationshipTypes().equals(taxRel.getType()) ) {
810
                    if (relationshipEdgeFilter.getDirections().contains(Direction.relatedTo) && !taxa.contains(taxRel.getToTaxon())) {
811
                        if(logger.isDebugEnabled()){
812
                            logger.debug(maxDepth + ": " + taxon.getTitleCache() + " --[" + taxRel.getType().getLabel() + "]--> " + taxRel.getToTaxon().getTitleCache());
813
                        }
814
                        taxa.add(taxRel.getToTaxon());
815
                        if(maxDepth == null || maxDepth > 0) {
816
                            taxa.addAll(collectRelatedTaxa(taxRel.getToTaxon(), includeRelationships, taxa, includeUnpublished, maxDepth));
817
                        }
818
                    }
819
                    if(relationshipEdgeFilter.getDirections().contains(Direction.relatedFrom) && !taxa.contains(taxRel.getFromTaxon())) {
820
                        taxa.add(taxRel.getFromTaxon());
821
                        if(logger.isDebugEnabled()){
822
                            logger.debug(maxDepth + ": " +taxRel.getFromTaxon().getTitleCache() + " --[" + taxRel.getType().getLabel() + "]--> " + taxon.getTitleCache() );
823
                        }
824
                        if(maxDepth == null || maxDepth > 0) {
825
                            taxa.addAll(collectRelatedTaxa(taxRel.getFromTaxon(), includeRelationships, taxa, includeUnpublished, maxDepth));
826
                        }
827
                    }
828
                }
829
            }
830
        }
831
        return taxa;
832
    }
833

    
834
    @Override
835
    public Pager<Synonym> getSynonyms(Taxon taxon,	SynonymType type, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) {
836
        Long numberOfResults = dao.countSynonyms(taxon, type);
837

    
838
        List<Synonym> results = new ArrayList<>();
839
        if(numberOfResults > 0) { // no point checking again
840
            results = dao.getSynonyms(taxon, type, pageSize, pageNumber, orderHints, propertyPaths);
841
        }
842

    
843
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
844
    }
845

    
846
    @Override
847
    public List<List<Synonym>> getSynonymsByHomotypicGroup(Taxon taxon, List<String> propertyPaths){
848
        List<List<Synonym>> result = new ArrayList<>();
849
        taxon = (Taxon)dao.load(taxon.getUuid(), propertyPaths);
850
        HomotypicGroupTaxonComparator comparator = new HomotypicGroupTaxonComparator(taxon);
851

    
852
        //homotypic
853
        result.add(taxon.getHomotypicSynonymsByHomotypicGroup(comparator));
854

    
855
        //heterotypic
856
        List<HomotypicalGroup> homotypicalGroups = taxon.getHeterotypicSynonymyGroups();  //currently the list is sorted by the Taxon.defaultTaxonComparator
857
        for(HomotypicalGroup homotypicalGroup : homotypicalGroups){
858
            result.add(taxon.getSynonymsInGroup(homotypicalGroup, comparator));
859
        }
860

    
861
        return result;
862
    }
863

    
864
    @Override
865
    public List<Synonym> getHomotypicSynonymsByHomotypicGroup(Taxon taxon, List<String> propertyPaths){
866
        Taxon t = (Taxon)dao.load(taxon.getUuid(), propertyPaths);
867
        HomotypicGroupTaxonComparator comparator = new HomotypicGroupTaxonComparator(taxon);
868

    
869
        return t.getHomotypicSynonymsByHomotypicGroup(comparator);
870
    }
871

    
872
    @Override
873
    public List<List<Synonym>> getHeterotypicSynonymyGroups(Taxon taxon, List<String> propertyPaths){
874
        Taxon t = (Taxon)dao.load(taxon.getUuid(), propertyPaths);
875
        List<HomotypicalGroup> homotypicalGroups = t.getHeterotypicSynonymyGroups();
876
        List<List<Synonym>> heterotypicSynonymyGroups = new ArrayList<>(homotypicalGroups.size());
877
        for(HomotypicalGroup homotypicalGroup : homotypicalGroups){
878
            heterotypicSynonymyGroups.add(t.getSynonymsInGroup(homotypicalGroup));
879
        }
880
        return heterotypicSynonymyGroups;
881
    }
882

    
883
    @Override
884
    public List<UuidAndTitleCache<? extends IdentifiableEntity>> findTaxaAndNamesForEditor(IFindTaxaAndNamesConfigurator config){
885

    
886
        if (config.isDoSynonyms() || config.isDoTaxa() || config.isDoNamesWithoutTaxa() || config.isDoTaxaByCommonNames()){
887
        	return dao.getTaxaByNameForEditor(config.isDoTaxa(), config.isDoSynonyms(), config.isDoNamesWithoutTaxa(),
888
        	        config.isDoMisappliedNames(), config.isDoTaxaByCommonNames(), config.isIncludeUnpublished(), config.isDoIncludeAuthors(),
889
        	        config.getTitleSearchStringSqlized(), config.getClassification(), config.getSubtree(),
890
        	        config.getMatchMode(), config.getNamedAreas(), config.getOrder());
891
        }else{
892
            return new ArrayList<>();
893
        }
894
    }
895

    
896
    @Override
897
    public Pager<IdentifiableEntity> findTaxaAndNames(IFindTaxaAndNamesConfigurator configurator) {
898

    
899
        @SuppressWarnings("rawtypes")
900
        List<IdentifiableEntity> results = new ArrayList<>();
901
        long numberOfResults = 0; // overall number of results (as opposed to number of results per page)
902
        List<TaxonBase> taxa = null;
903

    
904
        // Taxa and synonyms
905
        long numberTaxaResults = 0L;
906

    
907
        List<String> propertyPath = new ArrayList<>();
908
        if(configurator.getTaxonPropertyPath() != null){
909
            propertyPath.addAll(configurator.getTaxonPropertyPath());
910
        }
911

    
912
        if (configurator.isDoMisappliedNames() || configurator.isDoSynonyms() || configurator.isDoTaxa() || configurator.isDoTaxaByCommonNames()){
913
            if(configurator.getPageSize() != null){ // no point counting if we need all anyway
914
                numberTaxaResults =
915
                    dao.countTaxaByName(configurator.isDoTaxa(),configurator.isDoSynonyms(), configurator.isDoMisappliedNames(),
916
                        configurator.isDoTaxaByCommonNames(), configurator.isDoIncludeAuthors(), configurator.getTitleSearchStringSqlized(),
917
                        configurator.getClassification(), configurator.getSubtree(), configurator.getMatchMode(),
918
                        configurator.getNamedAreas(), configurator.isIncludeUnpublished());
919
            }
920

    
921
            if(configurator.getPageSize() == null || numberTaxaResults > configurator.getPageSize() * configurator.getPageNumber()){ // no point checking again if less results
922
                taxa = dao.getTaxaByName(configurator.isDoTaxa(), configurator.isDoSynonyms(),
923
                    configurator.isDoMisappliedNames(), configurator.isDoTaxaByCommonNames(), configurator.isDoIncludeAuthors(),
924
                    configurator.getTitleSearchStringSqlized(), configurator.getClassification(), configurator.getSubtree(),
925
                    configurator.getMatchMode(), configurator.getNamedAreas(), configurator.isIncludeUnpublished(),
926
                    configurator.getOrder(), configurator.getPageSize(), configurator.getPageNumber(), propertyPath);
927
            }
928
        }
929

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

    
932
        if(taxa != null){
933
            results.addAll(taxa);
934
        }
935

    
936
        numberOfResults += numberTaxaResults;
937

    
938
        // Names without taxa
939
        if (configurator.isDoNamesWithoutTaxa()) {
940
            int numberNameResults = 0;
941

    
942
            List<TaxonName> names =
943
                nameDao.findByName(configurator.isDoIncludeAuthors(), configurator.getTitleSearchStringSqlized(), configurator.getMatchMode(),
944
                        configurator.getPageSize(), configurator.getPageNumber(), null, configurator.getTaxonNamePropertyPath());
945
            if (logger.isDebugEnabled()) { logger.debug(names.size() + " matching name(s) found"); }
946
            if (names.size() > 0) {
947
                for (TaxonName taxonName : names) {
948
                    if (taxonName.getTaxonBases().size() == 0) {
949
                        results.add(taxonName);
950
                        numberNameResults++;
951
                    }
952
                }
953
                if (logger.isDebugEnabled()) { logger.debug(numberNameResults + " matching name(s) without taxa found"); }
954
                numberOfResults += numberNameResults;
955
            }
956
        }
957

    
958
       return new DefaultPagerImpl<> (configurator.getPageNumber(), numberOfResults, configurator.getPageSize(), results);
959
    }
960

    
961
    public List<UuidAndTitleCache<TaxonBase>> getTaxonUuidAndTitleCache(Integer limit, String pattern){
962
        return dao.getUuidAndTitleCache(limit, pattern);
963
    }
964

    
965
    @Override
966
    public List<Media> listTaxonDescriptionMedia(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships, boolean limitToGalleries, List<String> propertyPath){
967
        return listMedia(taxon, includeRelationships, limitToGalleries, true, false, false, propertyPath);
968
    }
969

    
970
    @Override
971
    public List<Media> listMedia(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships,
972
            Boolean limitToGalleries, Boolean includeTaxonDescriptions, Boolean includeOccurrences,
973
            Boolean includeTaxonNameDescriptions, List<String> propertyPath) {
974

    
975
        //TODO let inherit
976
        boolean includeUnpublished = INCLUDE_UNPUBLISHED;
977

    
978
    //    logger.setLevel(Level.TRACE);
979
//        Logger.getLogger("org.hibernate.SQL").setLevel(Level.TRACE);
980

    
981
        if (logger.isTraceEnabled()){logger.trace("listMedia() - START");}
982

    
983
        Set<Taxon> taxa = new HashSet<>();
984
        List<Media> taxonMedia = new ArrayList<>();
985
        List<Media> nonImageGalleryImages = new ArrayList<>();
986

    
987
        if (limitToGalleries == null) {
988
            limitToGalleries = false;
989
        }
990

    
991
        // --- resolve related taxa
992
        if (includeRelationships != null && ! includeRelationships.isEmpty()) {
993
            if (logger.isTraceEnabled()){logger.trace("listMedia() - resolve related taxa");}
994
            taxa = listRelatedTaxa(taxon, includeRelationships, null, includeUnpublished, null, null, null);
995
        }
996

    
997
        taxa.add((Taxon) dao.load(taxon.getUuid()));
998

    
999
        if(includeTaxonDescriptions != null && includeTaxonDescriptions){
1000
            if (logger.isTraceEnabled()){logger.trace("listMedia() - includeTaxonDescriptions");}
1001
            List<TaxonDescription> taxonDescriptions = new ArrayList<>();
1002
            // --- TaxonDescriptions
1003
            for (Taxon t : taxa) {
1004
                taxonDescriptions.addAll(descriptionService.listTaxonDescriptions(t, null, null, null, null, propertyPath));
1005
            }
1006
            for (TaxonDescription taxonDescription : taxonDescriptions) {
1007
                if (!limitToGalleries || taxonDescription.isImageGallery()) {
1008
                    for (DescriptionElementBase element : taxonDescription.getElements()) {
1009
                        for (Media media : element.getMedia()) {
1010
                            if(taxonDescription.isImageGallery()){
1011
                                taxonMedia.add(media);
1012
                            }
1013
                            else{
1014
                                nonImageGalleryImages.add(media);
1015
                            }
1016
                        }
1017
                    }
1018
                }
1019
            }
1020
            //put images from image gallery first (#3242)
1021
            taxonMedia.addAll(nonImageGalleryImages);
1022
        }
1023

    
1024

    
1025
        if(includeOccurrences != null && includeOccurrences) {
1026
            if (logger.isTraceEnabled()){logger.trace("listMedia() - includeOccurrences");}
1027
            @SuppressWarnings("rawtypes")
1028
            Set<SpecimenOrObservationBase> specimensOrObservations = new HashSet<>();
1029
            // --- Specimens
1030
            for (Taxon t : taxa) {
1031
                specimensOrObservations.addAll(occurrenceDao.listByAssociatedTaxon(null, t, null, null, null, null));
1032
            }
1033
            for (SpecimenOrObservationBase<?> occurrence : specimensOrObservations) {
1034

    
1035
//            	direct media removed from specimen #3597
1036
//              taxonMedia.addAll(occurrence.getMedia());
1037

    
1038
                // SpecimenDescriptions
1039
                Set<SpecimenDescription> specimenDescriptions = occurrence.getSpecimenDescriptions();
1040
                for (DescriptionBase<?> specimenDescription : specimenDescriptions) {
1041
                    if (!limitToGalleries || specimenDescription.isImageGallery()) {
1042
                        Set<DescriptionElementBase> elements = specimenDescription.getElements();
1043
                        for (DescriptionElementBase element : elements) {
1044
                            for (Media media : element.getMedia()) {
1045
                                taxonMedia.add(media);
1046
                            }
1047
                        }
1048
                    }
1049
                }
1050

    
1051
                if (occurrence.isInstanceOf(DerivedUnit.class)) {
1052
                    DerivedUnit derivedUnit = CdmBase.deproxy(occurrence, DerivedUnit.class);
1053
                    // Collection
1054
                    //TODO why may collections have media attached? #
1055
                    if (derivedUnit.getCollection() != null){
1056
                        taxonMedia.addAll(derivedUnit.getCollection().getMedia());
1057
                    }
1058
                }
1059
                //media in hierarchy
1060
                taxonMedia.addAll(occurrenceService.getMediaInHierarchy(occurrence, null, null, propertyPath).getRecords());
1061
            }
1062
        }
1063

    
1064
        if(includeTaxonNameDescriptions != null && includeTaxonNameDescriptions) {
1065
            if (logger.isTraceEnabled()){logger.trace("listMedia() - includeTaxonNameDescriptions");}
1066
            // --- TaxonNameDescription
1067
            Set<TaxonNameDescription> nameDescriptions = new HashSet<>();
1068
            for (Taxon t : taxa) {
1069
                nameDescriptions.addAll(t.getName().getDescriptions());
1070
            }
1071
            for(TaxonNameDescription nameDescription: nameDescriptions){
1072
                if (!limitToGalleries || nameDescription.isImageGallery()) {
1073
                    Set<DescriptionElementBase> elements = nameDescription.getElements();
1074
                    for (DescriptionElementBase element : elements) {
1075
                        for (Media media : element.getMedia()) {
1076
                            taxonMedia.add(media);
1077
                        }
1078
                    }
1079
                }
1080
            }
1081
        }
1082

    
1083
        taxonMedia = deduplicateMedia(taxonMedia);
1084

    
1085
        if (logger.isTraceEnabled()){logger.trace("listMedia() - initialize");}
1086
        beanInitializer.initializeAll(taxonMedia, propertyPath);
1087

    
1088
        if (logger.isTraceEnabled()){logger.trace("listMedia() - END");}
1089

    
1090
        return taxonMedia;
1091
    }
1092

    
1093
    private List<Media> deduplicateMedia(List<Media> taxonMedia) {
1094
        return CdmUtils.removeIdentical(taxonMedia);
1095
    }
1096

    
1097
    @Override
1098
    public List<TaxonBase> findTaxaByID(Set<Integer> listOfIDs) {
1099
        return this.dao.loadList(listOfIDs, null, null);
1100
    }
1101

    
1102
    @Override
1103
    public TaxonBase findTaxonByUuid(UUID uuid, List<String> propertyPaths){
1104
        return this.dao.findByUuid(uuid, null ,propertyPaths);
1105
    }
1106

    
1107
    @Override
1108
    public long countSynonyms(boolean onlyAttachedToTaxon){
1109
        return this.dao.countSynonyms(onlyAttachedToTaxon);
1110
    }
1111

    
1112
    @Override
1113
    @Transactional(readOnly=false)
1114
    public DeleteResult deleteTaxon(UUID taxonUUID, TaxonDeletionConfigurator config, UUID classificationUuid)  {
1115

    
1116
    	if (config == null){
1117
            config = new TaxonDeletionConfigurator();
1118
        }
1119
    	Taxon taxon = (Taxon)dao.load(taxonUUID);
1120
    	DeleteResult result = new DeleteResult();
1121
    	if (taxon == null){
1122
    	    result.setAbort();
1123
    	    result.addException(new Exception ("The taxon was already deleted."));
1124
    	    return result;
1125
    	}
1126
    	taxon = HibernateProxyHelper.deproxy(taxon);
1127
    	Classification classification = HibernateProxyHelper.deproxy(classificationDao.load(classificationUuid), Classification.class);
1128
    	config.setClassificationUuid(classificationUuid);
1129
        result = isDeletable(taxonUUID, config);
1130

    
1131
        if (result.isOk()){
1132
            // --- DeleteSynonymRelations
1133
            if (config.isDeleteSynonymRelations()){
1134
                boolean removeSynonymNameFromHomotypicalGroup = false;
1135
                // use tmp Set to avoid concurrent modification
1136
                Set<Synonym> synsToDelete = new HashSet<>();
1137
                synsToDelete.addAll(taxon.getSynonyms());
1138
                for (Synonym synonym : synsToDelete){
1139
                    taxon.removeSynonym(synonym, removeSynonymNameFromHomotypicalGroup);
1140

    
1141
                    // --- DeleteSynonymsIfPossible
1142
                    if (config.isDeleteSynonymsIfPossible()){
1143
                        //TODO which value
1144
                        boolean newHomotypicGroupIfNeeded = true;
1145
                        SynonymDeletionConfigurator synConfig = new SynonymDeletionConfigurator();
1146
                        result.includeResult(deleteSynonym(synonym, synConfig));
1147
                    }
1148
                }
1149
            }
1150

    
1151
            // --- DeleteTaxonRelationships
1152
            if (! config.isDeleteTaxonRelationships()){
1153
                if (taxon.getTaxonRelations().size() > 0){
1154
                    result.setAbort();
1155
                    result.addException(new Exception("Taxon can't be deleted as it is related to another taxon. " +
1156
                            "Remove taxon from all relations to other taxa prior to deletion."));
1157
                }
1158
            } else{
1159
                TaxonDeletionConfigurator configRelTaxon = new TaxonDeletionConfigurator();
1160
                configRelTaxon.setDeleteTaxonNodes(false);
1161
                configRelTaxon.setDeleteConceptRelationships(true);
1162

    
1163
                for (TaxonRelationship taxRel: taxon.getTaxonRelations()){
1164
                    if (config.isDeleteMisappliedNames()
1165
                            && taxRel.getType().isMisappliedName()
1166
                            && taxon.equals(taxRel.getToTaxon())){
1167
                        this.deleteTaxon(taxRel.getFromTaxon().getUuid(), config, classificationUuid);
1168
                    } else if (config.isDeleteConceptRelationships() && taxRel.getType().isConceptRelationship()){
1169

    
1170
                        if (taxon.equals(taxRel.getToTaxon()) && isDeletable(taxRel.getFromTaxon().getUuid(), configRelTaxon).isOk()){
1171
                            this.deleteTaxon(taxRel.getFromTaxon().getUuid(), configRelTaxon, classificationUuid);
1172
                        }else if (isDeletable(taxRel.getToTaxon().getUuid(), configRelTaxon).isOk()){
1173
                            this.deleteTaxon(taxRel.getToTaxon().getUuid(), configRelTaxon, classificationUuid);
1174
                        }
1175
                    }
1176
                    taxon.removeTaxonRelation(taxRel);
1177
                }
1178
            }
1179

    
1180
            //    	TaxonDescription
1181
            if (config.isDeleteDescriptions()){
1182
                Set<TaxonDescription> descriptions = taxon.getDescriptions();
1183
                List<TaxonDescription> removeDescriptions = new ArrayList<>();
1184
                for (TaxonDescription desc: descriptions){
1185
                    //TODO use description delete configurator ?
1186
                    //FIXME check if description is ALWAYS deletable
1187
                    if (desc.getDescribedSpecimenOrObservation() != null){
1188
                        result.setAbort();
1189
                        result.addException(new Exception("Taxon can't be deleted as it is used in a TaxonDescription" +
1190
                                " which also describes specimens or observations"));
1191
                        break;
1192
                    }
1193
                    removeDescriptions.add(desc);
1194
                }
1195
                if (result.isOk()){
1196
                    for (TaxonDescription desc: removeDescriptions){
1197
                        taxon.removeDescription(desc);
1198
                        descriptionService.delete(desc);
1199
                    }
1200
                } else {
1201
                    return result;
1202
                }
1203
            }
1204

    
1205
             if (! config.isDeleteTaxonNodes() || (!config.isDeleteInAllClassifications() && classification == null && taxon.getTaxonNodes().size() > 1)){
1206
                 result.addException(new Exception( "Taxon can't be deleted as it is used in more than one classification."));
1207
             }else{
1208
                 if (taxon.getTaxonNodes().size() != 0){
1209
                    Set<TaxonNode> nodes = taxon.getTaxonNodes();
1210
                    Iterator<TaxonNode> iterator = nodes.iterator();
1211
                    TaxonNode node = null;
1212
                    boolean deleteChildren;
1213
                    if (config.getTaxonNodeConfig().getChildHandling().equals(ChildHandling.DELETE)){
1214
                        deleteChildren = true;
1215
                    }else {
1216
                        deleteChildren = false;
1217
                    }
1218
                    boolean success = true;
1219
                    if (!config.isDeleteInAllClassifications() && !(classification == null)){
1220
                        while (iterator.hasNext()){
1221
                            node = iterator.next();
1222
                            if (node.getClassification().equals(classification)){
1223
                                break;
1224
                            }
1225
                            node = null;
1226
                        }
1227
                        if (node != null){
1228
                            HibernateProxyHelper.deproxy(node, TaxonNode.class);
1229
                            success =taxon.removeTaxonNode(node, deleteChildren);
1230
                            nodeService.delete(node);
1231
                            result.addDeletedObject(node);
1232
                        } else {
1233
                        	result.setError();
1234
                        	result.addException(new Exception("The taxon can not be deleted because it is not used in defined classification."));
1235
                        }
1236
                    } else if (config.isDeleteInAllClassifications()){
1237
                        List<TaxonNode> nodesList = new ArrayList<>();
1238
                        nodesList.addAll(taxon.getTaxonNodes());
1239
                        for (ITaxonTreeNode treeNode: nodesList){
1240
                            TaxonNode taxonNode = (TaxonNode) treeNode;
1241
                            if(!deleteChildren){
1242
                                Object[] childNodes = taxonNode.getChildNodes().toArray();
1243
                                for (Object childNode: childNodes){
1244
                                    TaxonNode childNodeCast = (TaxonNode) childNode;
1245
                                    taxonNode.getParent().addChildNode(childNodeCast, childNodeCast.getReference(), childNodeCast.getMicroReference());
1246
                                }
1247
                            }
1248
                        }
1249
                        config.getTaxonNodeConfig().setDeleteElement(false);
1250
                        DeleteResult resultNodes = nodeService.deleteTaxonNodes(nodesList, config);
1251
                        if (!resultNodes.isOk()){
1252
                        	result.addExceptions(resultNodes.getExceptions());
1253
                        	result.setStatus(resultNodes.getStatus());
1254
                        } else {
1255
                            result.addUpdatedObjects(resultNodes.getUpdatedObjects());
1256
                        }
1257
                    }
1258
                    if (!success){
1259
                        result.setError();
1260
                        result.addException(new Exception("The taxon can not be deleted because the taxon node can not be removed."));
1261
                    }
1262
                }
1263
             }
1264
             TaxonName name = taxon.getName();
1265
             taxon.setName(null);
1266
             this.saveOrUpdate(taxon);
1267

    
1268
             if ((taxon.getTaxonNodes() == null || taxon.getTaxonNodes().size()== 0)  && result.isOk()){
1269
                 try{
1270
                     dao.delete(taxon);
1271
                     result.addDeletedObject(taxon);
1272
                 }catch(Exception e){
1273
                     result.addException(e);
1274
                     result.setError();
1275
                 }
1276
             } else {
1277
                 result.setError();
1278
                 result.addException(new Exception("The Taxon can't be deleted because it is used in a classification."));
1279

    
1280
             }
1281
             //TaxonName
1282
             if (config.isDeleteNameIfPossible() && result.isOk()){
1283
                 DeleteResult nameResult = new DeleteResult();
1284
                 //remove name if possible (and required)
1285
                 if (name != null ){
1286
                     nameResult = nameService.delete(name.getUuid(), config.getNameDeletionConfig());
1287
                 }
1288
                 if (nameResult.isError() || nameResult.isAbort()){
1289
                     result.addRelatedObject(name);
1290
                     result.addExceptions(nameResult.getExceptions());
1291
                 }else{
1292
                     result.includeResult(nameResult);
1293
                 }
1294
             }
1295
       }
1296

    
1297
       return result;
1298
    }
1299

    
1300
    @Override
1301
    @Transactional(readOnly = false)
1302
    public DeleteResult delete(UUID synUUID){
1303
    	Synonym syn = (Synonym)dao.load(synUUID);
1304
        return this.deleteSynonym(syn, null);
1305
    }
1306

    
1307
    @Override
1308
    @Transactional(readOnly = false)
1309
    public DeleteResult deleteSynonym(UUID synonymUuid, SynonymDeletionConfigurator config) {
1310
        return deleteSynonym((Synonym)dao.load(synonymUuid), config);
1311
    }
1312

    
1313
    @Override
1314
    @Transactional(readOnly = false)
1315
    public DeleteResult deleteSynonym(Synonym synonym, SynonymDeletionConfigurator config) {
1316
        DeleteResult result = new DeleteResult();
1317
    	if (synonym == null){
1318
    		result.setAbort();
1319
    		result.addException(new Exception("The synonym was already deleted."));
1320
    		return result;
1321
        }
1322

    
1323
        if (config == null){
1324
            config = new SynonymDeletionConfigurator();
1325
        }
1326

    
1327
        result = isDeletable(synonym.getUuid(), config);
1328

    
1329
        if (result.isOk()){
1330

    
1331
            synonym = HibernateProxyHelper.deproxy(this.load(synonym.getUuid()), Synonym.class);
1332

    
1333
            //remove synonym
1334
            Taxon accTaxon = synonym.getAcceptedTaxon();
1335

    
1336
            if (accTaxon != null){
1337
                accTaxon = HibernateProxyHelper.deproxy(accTaxon, Taxon.class);
1338
                accTaxon.removeSynonym(synonym, false);
1339
                this.saveOrUpdate(accTaxon);
1340
                result.addUpdatedObject(accTaxon);
1341
            }
1342
            this.saveOrUpdate(synonym);
1343
            //#6281
1344
            dao.flush();
1345

    
1346
            TaxonName name = synonym.getName();
1347
            synonym.setName(null);
1348

    
1349
            dao.delete(synonym);
1350
            result.addDeletedObject(synonym);
1351

    
1352
            //remove name if possible (and required)
1353
            if (name != null && config.isDeleteNameIfPossible()){
1354

    
1355
                DeleteResult nameDeleteResult = nameService.delete(name, config.getNameDeletionConfig());
1356
                if (nameDeleteResult.isAbort() || nameDeleteResult.isError()){
1357
                	result.addExceptions(nameDeleteResult.getExceptions());
1358
                	result.addRelatedObject(name);
1359
                	result.addUpdatedObject(name);
1360
                }else{
1361
                    result.addDeletedObject(name);
1362
                }
1363
            }
1364
        }
1365
        return result;
1366
    }
1367

    
1368
    @Override
1369
    public Map<String, Map<UUID,Set<TaxonName>>> findIdenticalTaxonNames(List<UUID> sourceRefUuids, List<String> propertyPaths) {
1370
        return this.dao.findIdenticalNames(sourceRefUuids, propertyPaths);
1371
    }
1372

    
1373
    @Override
1374
    public Taxon findBestMatchingTaxon(String taxonName) {
1375
        MatchingTaxonConfigurator config = MatchingTaxonConfigurator.NewInstance();
1376
        config.setTaxonNameTitle(taxonName);
1377
        return findBestMatchingTaxon(config);
1378
    }
1379

    
1380
    @Override
1381
    public Taxon findBestMatchingTaxon(MatchingTaxonConfigurator config) {
1382

    
1383
        Taxon bestCandidate = null;
1384
        try{
1385
            // 1. search for accepted taxa
1386
            List<TaxonBase> taxonList = dao.findByNameTitleCache(true, false, config.isIncludeUnpublished(),
1387
                    config.getTaxonNameTitle(), null, null, MatchMode.EXACT, null, null, 0, null, null);
1388
            boolean bestCandidateMatchesSecUuid = false;
1389
            boolean bestCandidateIsInClassification = false;
1390
            int countEqualCandidates = 0;
1391
            for(TaxonBase<?> taxonBaseCandidate : taxonList){
1392
                if(taxonBaseCandidate instanceof Taxon){
1393
                    Taxon newCanditate = CdmBase.deproxy(taxonBaseCandidate, Taxon.class);
1394
                    boolean newCandidateMatchesSecUuid = isMatchesSecUuid(newCanditate, config);
1395
                    if (! newCandidateMatchesSecUuid && config.isOnlyMatchingSecUuid() ){
1396
                        continue;
1397
                    }else if(newCandidateMatchesSecUuid && ! bestCandidateMatchesSecUuid){
1398
                        bestCandidate = newCanditate;
1399
                        countEqualCandidates = 1;
1400
                        bestCandidateMatchesSecUuid = true;
1401
                        continue;
1402
                    }
1403

    
1404
                    boolean newCandidateInClassification = isInClassification(newCanditate, config);
1405
                    if (! newCandidateInClassification && config.isOnlyMatchingClassificationUuid()){
1406
                        continue;
1407
                    }else if (newCandidateInClassification && ! bestCandidateIsInClassification){
1408
                        bestCandidate = newCanditate;
1409
                        countEqualCandidates = 1;
1410
                        bestCandidateIsInClassification = true;
1411
                        continue;
1412
                    }
1413
                    if (bestCandidate == null){
1414
                        bestCandidate = newCanditate;
1415
                        countEqualCandidates = 1;
1416
                        continue;
1417
                    }
1418
                }else{  //not Taxon.class
1419
                    continue;
1420
                }
1421
                countEqualCandidates++;
1422

    
1423
            }
1424
            if (bestCandidate != null){
1425
                if(countEqualCandidates > 1){
1426
                    logger.info(countEqualCandidates + " equally matching TaxonBases found, using first accepted Taxon: " + bestCandidate.getTitleCache());
1427
                    return bestCandidate;
1428
                } else {
1429
                    logger.info("using accepted Taxon: " + bestCandidate.getTitleCache());
1430
                    return bestCandidate;
1431
                }
1432
            }
1433

    
1434
            // 2. search for synonyms
1435
            if (config.isIncludeSynonyms()){
1436
                List<TaxonBase> synonymList = dao.findByNameTitleCache(false, true, config.isIncludeUnpublished(),
1437
                        config.getTaxonNameTitle(), null, null, MatchMode.EXACT, null, null, 0, null, null);
1438
                for(TaxonBase taxonBase : synonymList){
1439
                    if(taxonBase instanceof Synonym){
1440
                        Synonym synonym = CdmBase.deproxy(taxonBase, Synonym.class);
1441
                        bestCandidate = synonym.getAcceptedTaxon();
1442
                        if(bestCandidate != null){
1443
                            logger.info("using accepted Taxon " +  bestCandidate.getTitleCache() + " for synonym " + taxonBase.getTitleCache());
1444
                            return bestCandidate;
1445
                        }
1446
                        //TODO extend method: search using treeUUID, using SecUUID, first find accepted then include synonyms until a matching taxon is found
1447
                    }
1448
                }
1449
            }
1450

    
1451
        } catch (Exception e){
1452
            logger.error(e);
1453
            e.printStackTrace();
1454
        }
1455

    
1456
        return bestCandidate;
1457
    }
1458

    
1459
    private boolean isInClassification(Taxon taxon, MatchingTaxonConfigurator config) {
1460
        UUID configClassificationUuid = config.getClassificationUuid();
1461
        if (configClassificationUuid == null){
1462
            return false;
1463
        }
1464
        for (TaxonNode node : taxon.getTaxonNodes()){
1465
            UUID classUuid = node.getClassification().getUuid();
1466
            if (configClassificationUuid.equals(classUuid)){
1467
                return true;
1468
            }
1469
        }
1470
        return false;
1471
    }
1472

    
1473
    private boolean isMatchesSecUuid(Taxon taxon, MatchingTaxonConfigurator config) {
1474
        UUID configSecUuid = config.getSecUuid();
1475
        if (configSecUuid == null){
1476
            return false;
1477
        }
1478
        UUID taxonSecUuid = (taxon.getSec() == null)? null : taxon.getSec().getUuid();
1479
        return configSecUuid.equals(taxonSecUuid);
1480
    }
1481

    
1482
    @Override
1483
    public Synonym findBestMatchingSynonym(String taxonName, boolean includeUnpublished) {
1484
        List<TaxonBase> synonymList = dao.findByNameTitleCache(false, true, includeUnpublished, taxonName, null, null, MatchMode.EXACT, null, null, 0, null, null);
1485
        if(! synonymList.isEmpty()){
1486
            Synonym result = CdmBase.deproxy(synonymList.iterator().next(), Synonym.class);
1487
            if(synonymList.size() == 1){
1488
                logger.info(synonymList.size() + " Synonym found " + result.getTitleCache() );
1489
                return result;
1490
            } else {
1491
                logger.info("Several matching synonyms found. Using first: " +  result.getTitleCache());
1492
                return result;
1493
            }
1494
        }
1495
        return null;
1496
    }
1497

    
1498
    @Override
1499
    @Transactional(readOnly = false)
1500
    public UpdateResult moveSynonymToAnotherTaxon(Synonym oldSynonym, UUID newTaxonUUID, boolean moveHomotypicGroup,
1501
            SynonymType newSynonymType, UUID newSecundumUuid, String newSecundumDetail,
1502
            boolean keepSecundumIfUndefined) throws HomotypicalGroupChangeException {
1503

    
1504
        UpdateResult result = new UpdateResult();
1505
        Taxon newTaxon = CdmBase.deproxy(dao.load(newTaxonUUID), Taxon.class);
1506
        result = moveSynonymToAnotherTaxon(oldSynonym, newTaxon, moveHomotypicGroup, newSynonymType,
1507
                newSecundumUuid, newSecundumDetail, keepSecundumIfUndefined);
1508

    
1509
        return result;
1510
    }
1511

    
1512
    @Override
1513
    @Transactional(readOnly = false)
1514
    public UpdateResult moveSynonymToAnotherTaxon(Synonym oldSynonym,
1515
            Taxon newTaxon,
1516
            boolean moveHomotypicGroup,
1517
            SynonymType newSynonymType) throws HomotypicalGroupChangeException {
1518
        return moveSynonymToAnotherTaxon(oldSynonym, newTaxon, moveHomotypicGroup,
1519
                newSynonymType,
1520
                oldSynonym.getSec()!= null? oldSynonym.getSec().getUuid(): null,
1521
                oldSynonym.getSecMicroReference(),
1522
                true);
1523
    }
1524

    
1525
    @Override
1526
    @Transactional(readOnly = false)
1527
    public UpdateResult moveSynonymToAnotherTaxon(Synonym oldSynonym,
1528
            Taxon newTaxon,
1529
            boolean moveHomotypicGroup,
1530
            SynonymType newSynonymType,
1531
            UUID newSecundumUuid,
1532
            String newSecundumDetail,
1533
            boolean keepSecundumIfUndefined) throws HomotypicalGroupChangeException {
1534

    
1535
        Synonym synonym = CdmBase.deproxy(dao.load(oldSynonym.getUuid()), Synonym.class);
1536
        Taxon oldTaxon = CdmBase.deproxy(dao.load(synonym.getAcceptedTaxon().getUuid()), Taxon.class);
1537
        //TODO what if there is no name ?? Concepts may be cached (e.g. via TCS import)
1538
        TaxonName synonymName = synonym.getName();
1539
        TaxonName fromTaxonName = oldTaxon.getName();
1540
        //set default relationship type
1541
        if (newSynonymType == null){
1542
            newSynonymType = SynonymType.HETEROTYPIC_SYNONYM_OF();
1543
        }
1544
        boolean newRelTypeIsHomotypic = newSynonymType.equals(SynonymType.HOMOTYPIC_SYNONYM_OF());
1545

    
1546
        HomotypicalGroup homotypicGroup = synonymName.getHomotypicalGroup();
1547
        int hgSize = homotypicGroup.getTypifiedNames().size();
1548
        boolean isSingleInGroup = !(hgSize > 1);
1549

    
1550
        if (! isSingleInGroup){
1551
            boolean isHomotypicToAccepted = synonymName.isHomotypic(fromTaxonName);
1552
            boolean hasHomotypicSynonymRelatives = isHomotypicToAccepted ? hgSize > 2 : hgSize > 1;
1553
            if (isHomotypicToAccepted){
1554
                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.";
1555
                String homotypicRelatives = hasHomotypicSynonymRelatives ? " and other synonym(s)":"";
1556
                message = String.format(message, homotypicRelatives);
1557
                throw new HomotypicalGroupChangeException(message);
1558
            }
1559
            if (! moveHomotypicGroup){
1560
                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.";
1561
                throw new HomotypicalGroupChangeException(message);
1562
            }
1563
        }else{
1564
            moveHomotypicGroup = true;  //single synonym always allows to moveCompleteGroup
1565
        }
1566
//        Assert.assertTrue("Synonym can only be moved with complete homotypic group", moveHomotypicGroup);
1567

    
1568
        UpdateResult result = new UpdateResult();
1569
        //move all synonyms to new taxon
1570
        List<Synonym> homotypicSynonyms = oldTaxon.getSynonymsInGroup(homotypicGroup);
1571
        Reference newSecundum = referenceService.load(newSecundumUuid);
1572
        for (Synonym synRelation: homotypicSynonyms){
1573

    
1574
            newTaxon = HibernateProxyHelper.deproxy(newTaxon, Taxon.class);
1575
            oldTaxon = HibernateProxyHelper.deproxy(oldTaxon, Taxon.class);
1576
            oldTaxon.removeSynonym(synRelation, false);
1577
            newTaxon.addSynonym(synRelation, newSynonymType);
1578

    
1579
            if (newSecundum != null || !keepSecundumIfUndefined){
1580
                synRelation.setSec(newSecundum);
1581
            }
1582
            if (newSecundumDetail != null || !keepSecundumIfUndefined){
1583
                synRelation.setSecMicroReference(newSecundumDetail);
1584
            }
1585

    
1586
            //set result  //why is this needed? Seems wrong to me (AM 10.10.2016)
1587
            if (!synRelation.equals(oldSynonym)){
1588
                result.setError();
1589
            }
1590
        }
1591

    
1592
        result.addUpdatedObject(oldTaxon);
1593
        result.addUpdatedObject(newTaxon);
1594
        result.addUpdatedObject(homotypicGroup);
1595
        result.addUpdatedObject(synonym);
1596
        saveOrUpdate(oldTaxon);
1597
        saveOrUpdate(newTaxon);
1598

    
1599
        return result;
1600
    }
1601

    
1602
    @Override
1603
    public <T extends TaxonBase>List<UuidAndTitleCache<T>> getUuidAndTitleCache(Class<T> clazz, Integer limit, String pattern) {
1604

    
1605
        return dao.getUuidAndTitleCache(clazz, limit, pattern);
1606
    }
1607

    
1608
    @Override
1609
    public Pager<SearchResult<TaxonBase>> findByFullText(
1610
            Class<? extends TaxonBase> clazz, String queryString,
1611
            Classification classification, TaxonNode subtree, boolean includeUnpublished, List<Language> languages,
1612
            boolean highlightFragments, Integer pageSize, Integer pageNumber,
1613
            List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException {
1614

    
1615
        LuceneSearch luceneSearch = prepareFindByFullTextSearch(clazz, queryString, classification, subtree,
1616
                null, includeUnpublished, languages, highlightFragments, null);
1617

    
1618
        // --- execute search
1619
        TopGroups<BytesRef> topDocsResultSet;
1620
        try {
1621
            topDocsResultSet = luceneSearch.executeSearch(pageSize, pageNumber);
1622
        } catch (ParseException e) {
1623
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
1624
            luceneParseException.setStackTrace(e.getStackTrace());
1625
            throw luceneParseException;
1626
        }
1627

    
1628
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
1629
        idFieldMap.put(CdmBaseType.TAXON_BASE, "id");
1630

    
1631
        // ---  initialize taxa, thighlight matches ....
1632
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
1633
        @SuppressWarnings("rawtypes")
1634
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
1635
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
1636

    
1637
        long totalHits = topDocsResultSet != null ? Long.valueOf(topDocsResultSet.totalGroupCount) : 0;
1638
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
1639
    }
1640

    
1641
    @Transactional(readOnly = true)
1642
    @Override
1643
    public <S extends TaxonBase> Pager<S> findByTitleWithRestrictions(Class<S> clazz, String queryString, MatchMode matchmode, List<Restriction<?>> restrictions, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) {
1644
         long numberOfResults = dao.countByTitleWithRestrictions(clazz, queryString, matchmode, restrictions);
1645

    
1646
         long numberOfResults_doubtful = dao.countByTitleWithRestrictions(clazz, "?".concat(queryString), matchmode, restrictions);
1647
         List<S> results = new ArrayList<>();
1648
         if(numberOfResults > 0 || numberOfResults_doubtful > 0) { // no point checking again //TODO use AbstractPagerImpl.hasResultsInRange(numberOfResults, pageNumber, pageSize)
1649

    
1650
             results = dao.findByTitleWithRestrictions(clazz, queryString, matchmode, restrictions, pageSize, pageNumber, orderHints, propertyPaths);
1651
             results.addAll(dao.findByTitleWithRestrictions(clazz, "?".concat(queryString), matchmode, restrictions, pageSize, pageNumber, orderHints, propertyPaths));
1652
         }
1653
         Collections.sort(results, new TaxonComparator());
1654
         return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
1655
    }
1656

    
1657
    @Transactional(readOnly = true)
1658
    @Override
1659
    public <S extends TaxonBase> Pager<S> findByTitle(Class<S> clazz, String queryString,MatchMode matchmode, List<Criterion> criteria, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) {
1660
        long numberOfResults = dao.countByTitle(clazz, queryString, matchmode, criteria);
1661
        //check whether there are doubtful taxa matching
1662
        long numberOfResults_doubtful = dao.countByTitle(clazz, "?".concat(queryString), matchmode, criteria);
1663
        List<S> results = new ArrayList<>();
1664
        if(numberOfResults > 0 || numberOfResults_doubtful > 0) { // no point checking again //TODO use AbstractPagerImpl.hasResultsInRange(numberOfResults, pageNumber, pageSize)
1665
               if (numberOfResults > 0){
1666
                   results = dao.findByTitle(clazz, queryString, matchmode, criteria, pageSize, pageNumber, orderHints, propertyPaths);
1667
               }else{
1668
                   results = new ArrayList<>();
1669
               }
1670
               if (numberOfResults_doubtful > 0){
1671
                   results.addAll(dao.findByTitle(clazz, "?".concat(queryString), matchmode,  criteria, pageSize, pageNumber, orderHints, propertyPaths));
1672
               }
1673
        }
1674
        Collections.sort(results, new TaxonComparator());
1675
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
1676
    }
1677

    
1678
    @Override
1679
    public Pager<SearchResult<TaxonBase>> findByDistribution(List<NamedArea> areaFilter, List<PresenceAbsenceTerm> statusFilter,
1680
            Classification classification, TaxonNode subtree,
1681
            Integer pageSize, Integer pageNumber,
1682
            List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException {
1683

    
1684
        LuceneSearch luceneSearch = prepareByDistributionSearch(areaFilter, statusFilter, classification, subtree);
1685

    
1686
        // --- execute search
1687
        TopGroups<BytesRef> topDocsResultSet;
1688
        try {
1689
            topDocsResultSet = luceneSearch.executeSearch(pageSize, pageNumber);
1690
        } catch (ParseException e) {
1691
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
1692
            luceneParseException.setStackTrace(e.getStackTrace());
1693
            throw luceneParseException;
1694
        }
1695

    
1696
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
1697
        idFieldMap.put(CdmBaseType.TAXON_BASE, "id");
1698

    
1699
        // ---  initialize taxa, thighlight matches ....
1700
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
1701
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
1702
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
1703

    
1704
        long totalHits = topDocsResultSet != null ? Long.valueOf(topDocsResultSet.totalGroupCount) : 0;
1705
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
1706
    }
1707

    
1708
    /**
1709
     * @param clazz
1710
     * @param queryString
1711
     * @param classification
1712
     * @param includeUnpublished
1713
     * @param languages
1714
     * @param highlightFragments
1715
     * @param sortFields TODO
1716
     * @param directorySelectClass
1717
     * @return
1718
     */
1719
    protected LuceneSearch prepareFindByFullTextSearch(Class<? extends CdmBase> clazz, String queryString,
1720
            Classification classification, TaxonNode subtree, String className, boolean includeUnpublished, List<Language> languages,
1721
            boolean highlightFragments, SortField[] sortFields) {
1722

    
1723
        Builder finalQueryBuilder = new Builder();
1724
        Builder textQueryBuilder = new Builder();
1725

    
1726
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, TaxonBase.class);
1727
        QueryFactory taxonBaseQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(TaxonBase.class);
1728

    
1729
        if(sortFields == null){
1730
            sortFields = new SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING, false)};
1731
        }
1732
        luceneSearch.setSortFields(sortFields);
1733

    
1734
        // ---- search criteria
1735
        luceneSearch.setCdmTypRestriction(clazz);
1736

    
1737
        if(!StringUtils.isEmpty(queryString) && !queryString.equals("*") && !queryString.equals("?") ) {
1738
            textQueryBuilder.add(taxonBaseQueryFactory.newTermQuery("titleCache", queryString), Occur.SHOULD);
1739
            textQueryBuilder.add(taxonBaseQueryFactory.newDefinedTermQuery("name.rank", queryString, languages), Occur.SHOULD);
1740
        }
1741
        if(className != null){
1742
            textQueryBuilder.add(taxonBaseQueryFactory.newTermQuery("classInfo.name", className, false), Occur.MUST);
1743
        }
1744

    
1745
        BooleanQuery textQuery = textQueryBuilder.build();
1746
        if(textQuery.clauses().size() > 0) {
1747
            finalQueryBuilder.add(textQuery, Occur.MUST);
1748
        }
1749

    
1750
        if(classification != null){
1751
            finalQueryBuilder.add(taxonBaseQueryFactory.newEntityIdQuery(AcceptedTaxonBridge.DOC_KEY_CLASSIFICATION_ID, classification), Occur.MUST);
1752
        }
1753
        if(subtree != null){
1754
            finalQueryBuilder.add(taxonBaseQueryFactory.newTermQuery(AcceptedTaxonBridge.DOC_KEY_TREEINDEX, subtree.treeIndexWc(), true), Occur.MUST);
1755
        }
1756
        if(!includeUnpublished)  {
1757
            String accPublishParam = TaxonBase.ACC_TAXON_BRIDGE_PREFIX + AcceptedTaxonBridge.DOC_KEY_PUBLISH_SUFFIX;
1758
            finalQueryBuilder.add(taxonBaseQueryFactory.newBooleanQuery(accPublishParam, true), Occur.MUST);
1759
            finalQueryBuilder.add(taxonBaseQueryFactory.newBooleanQuery("publish", true), Occur.MUST);
1760
        }
1761

    
1762
        luceneSearch.setQuery(finalQueryBuilder.build());
1763

    
1764
        if(highlightFragments){
1765
            luceneSearch.setHighlightFields(taxonBaseQueryFactory.getTextFieldNamesAsArray());
1766
        }
1767
        return luceneSearch;
1768
    }
1769

    
1770
    /**
1771
     * Uses org.apache.lucene.search.join.JoinUtil for query time joining, alternatively
1772
     * the BlockJoinQuery could be used. The latter might be more memory save but has the
1773
     * drawback of requiring to do the join an indexing time.
1774
     * see  http://dev.e-taxonomy.eu/trac/wiki/LuceneNotes#JoinsinLucene for more information on this.
1775
     *
1776
     * Joins TaxonRelationShip with Taxon depending on the direction of the given edge:
1777
     * <ul>
1778
     * <li>direct, everted: {@link Direction.relatedTo}: TaxonRelationShip.relatedTo.id --&gt; Taxon.id </li>
1779
     * <li>inverse: {@link Direction.relatedFrom}:  TaxonRelationShip.relatedFrom.id --&gt; Taxon.id </li>
1780
     * <ul>
1781
     * @param queryString
1782
     * @param classification
1783
     * @param languages
1784
     * @param highlightFragments
1785
     * @param sortFields TODO
1786
     *
1787
     * @return
1788
     * @throws IOException
1789
     */
1790
    protected LuceneSearch prepareFindByTaxonRelationFullTextSearch(TaxonRelationshipEdge edge, String queryString,
1791
            Classification classification, TaxonNode subtree, boolean includeUnpublished, List<Language> languages,
1792
            boolean highlightFragments, SortField[] sortFields) throws IOException {
1793

    
1794
        String fromField;
1795
        String queryTermField;
1796
        String toField = "id"; // TaxonBase.uuid
1797
        String publishField;
1798
        String publishFieldInvers;
1799

    
1800
        if(edge.isBidirectional()){
1801
            throw new RuntimeException("Bidirectional joining not supported!");
1802
        }
1803
        if(edge.isEvers()){
1804
            fromField = "relatedFrom.id";
1805
            queryTermField = "relatedFrom.titleCache";
1806
            publishField = "relatedFrom.publish";
1807
            publishFieldInvers = "relatedTo.publish";
1808
        } else if(edge.isInvers()) {
1809
            fromField = "relatedTo.id";
1810
            queryTermField = "relatedTo.titleCache";
1811
            publishField = "relatedTo.publish";
1812
            publishFieldInvers = "relatedFrom.publish";
1813
        } else {
1814
            throw new RuntimeException("Invalid direction: " + edge.getDirections());
1815
        }
1816

    
1817
        Builder finalQueryBuilder = new Builder();
1818

    
1819
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, TaxonBase.class);
1820
        QueryFactory taxonBaseQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(TaxonBase.class);
1821

    
1822
        Builder joinFromQueryBuilder = new Builder();
1823
        if(!StringUtils.isEmpty(queryString)){
1824
            joinFromQueryBuilder.add(taxonBaseQueryFactory.newTermQuery(queryTermField, queryString), Occur.MUST);
1825
        }
1826
        joinFromQueryBuilder.add(taxonBaseQueryFactory.newEntityIdsQuery("type.id", edge.getRelationshipTypes()), Occur.MUST);
1827
        if(!includeUnpublished){
1828
            joinFromQueryBuilder.add(taxonBaseQueryFactory.newBooleanQuery(publishField, true), Occur.MUST);
1829
            joinFromQueryBuilder.add(taxonBaseQueryFactory.newBooleanQuery(publishFieldInvers, true), Occur.MUST);
1830
        }
1831

    
1832
        Query joinQuery = taxonBaseQueryFactory.newJoinQuery(TaxonRelationship.class, fromField, false, joinFromQueryBuilder.build(), toField, null, ScoreMode.Max);
1833

    
1834
        if(sortFields == null){
1835
            sortFields = new  SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING,  false)};
1836
        }
1837
        luceneSearch.setSortFields(sortFields);
1838

    
1839
        finalQueryBuilder.add(joinQuery, Occur.MUST);
1840

    
1841
        if(classification != null){
1842
            finalQueryBuilder.add(taxonBaseQueryFactory.newEntityIdQuery(AcceptedTaxonBridge.DOC_KEY_CLASSIFICATION_ID, classification), Occur.MUST);
1843
        }
1844
        if(subtree != null){
1845
            finalQueryBuilder.add(taxonBaseQueryFactory.newTermQuery(AcceptedTaxonBridge.DOC_KEY_TREEINDEX, subtree.treeIndexWc(), true), Occur.MUST);
1846
        }
1847

    
1848
        luceneSearch.setQuery(finalQueryBuilder.build());
1849

    
1850
        if(highlightFragments){
1851
            luceneSearch.setHighlightFields(taxonBaseQueryFactory.getTextFieldNamesAsArray());
1852
        }
1853
        return luceneSearch;
1854
    }
1855

    
1856
    @Override
1857
    public Pager<SearchResult<TaxonBase>> findTaxaAndNamesByFullText(
1858
            EnumSet<TaxaAndNamesSearchMode> searchModes, String queryString,
1859
            Classification classification, TaxonNode subtree,
1860
            Set<NamedArea> namedAreas, Set<PresenceAbsenceTerm> distributionStatus, List<Language> languages,
1861
            boolean highlightFragments, Integer pageSize,
1862
            Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths)
1863
            throws IOException, LuceneParseException, LuceneMultiSearchException {
1864

    
1865
        // FIXME: allow taxonomic ordering
1866
        //  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";
1867
        // this require building a special sort column by a special classBridge
1868
        if(highlightFragments){
1869
            logger.warn("findTaxaAndNamesByFullText() : fragment highlighting is " +
1870
                    "currently not fully supported by this method and thus " +
1871
                    "may not work with common names and misapplied names.");
1872
        }
1873

    
1874
        // convert sets to lists
1875
        List<NamedArea> namedAreaList = null;
1876
        List<PresenceAbsenceTerm> distributionStatusList = null;
1877
        if(namedAreas != null){
1878
            namedAreaList = new ArrayList<>(namedAreas.size());
1879
            namedAreaList.addAll(namedAreas);
1880
        }
1881
        if(distributionStatus != null){
1882
            distributionStatusList = new ArrayList<>(distributionStatus.size());
1883
            distributionStatusList.addAll(distributionStatus);
1884
        }
1885

    
1886
        // set default if parameter is null
1887
        if(searchModes == null){
1888
            searchModes = EnumSet.of(TaxaAndNamesSearchMode.doTaxa);
1889
        }
1890

    
1891
        // set sort order and thus override any sort orders which may have been
1892
        // defined by prepare*Search methods
1893
        if(orderHints == null){
1894
            orderHints = OrderHint.NOMENCLATURAL_SORT_ORDER.asList();
1895
        }
1896
        SortField[] sortFields = new SortField[orderHints.size()];
1897
        int i = 0;
1898
        for(OrderHint oh : orderHints){
1899
            sortFields[i++] = oh.toSortField();
1900
        }
1901
//        SortField[] sortFields = new SortField[]{SortField.FIELD_SCORE, new SortField("id", SortField.STRING, false)};
1902
//        SortField[] sortFields = new SortField[]{new SortField(NomenclaturalSortOrderBrigde.NAME_SORT_FIELD_NAME, SortField.STRING, false)};
1903

    
1904
        boolean addDistributionFilter = namedAreas != null && namedAreas.size() > 0;
1905

    
1906
        List<LuceneSearch> luceneSearches = new ArrayList<>();
1907
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
1908

    
1909
        /*
1910
          ======== filtering by distribution , HOWTO ========
1911

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

    
1917

    
1918
          3. how does it work in spatial?
1919
          see
1920
           - http://www.nsshutdown.com/projects/lucene/whitepaper/locallucene_v2.html
1921
           - http://www.infoq.com/articles/LuceneSpatialSupport
1922
           - http://www.mhaller.de/archives/156-Spatial-search-with-Lucene.html
1923
          ------------------------------------------------------------------------
1924

    
1925
          filter strategies:
1926
          A) use a separate distribution filter per index sub-query/search:
1927
           - byTaxonSyonym (query TaxaonBase):
1928
               use a join area filter (Distribution -> TaxonBase)
1929
           - byCommonName (query DescriptionElementBase): use an area filter on
1930
               DescriptionElementBase !!! PROBLEM !!!
1931
               This cannot work since the distributions are different entities than the
1932
               common names and thus these are different lucene documents.
1933
           - byMisaplliedNames (join query TaxonRelationship -> TaxonBase):
1934
               use a join area filter (Distribution -> TaxonBase)
1935

    
1936
          B) use a common distribution filter for all index sub-query/searches:
1937
           - use a common join area filter (Distribution -> TaxonBase)
1938
           - also implement the byCommonName as join query (CommonName -> TaxonBase)
1939
           PROBLEM in this case: we are losing the fragment highlighting for the
1940
           common names, since the returned documents are always TaxonBases
1941
        */
1942

    
1943
        /* The QueryFactory for creating filter queries on Distributions should
1944
         * The query factory used for the common names query cannot be reused
1945
         * for this case, since we want to only record the text fields which are
1946
         * actually used in the primary query
1947
         */
1948
        QueryFactory distributionFilterQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(Distribution.class);
1949

    
1950
        Builder multiIndexByAreaFilterBuilder = new Builder();
1951
        boolean includeUnpublished = searchModes.contains(TaxaAndNamesSearchMode.includeUnpublished);
1952

    
1953
        // search for taxa or synonyms
1954
        if(searchModes.contains(TaxaAndNamesSearchMode.doTaxa) || searchModes.contains(TaxaAndNamesSearchMode.doSynonyms) ) {
1955
            @SuppressWarnings("rawtypes")
1956
            Class<? extends TaxonBase> taxonBaseSubclass = TaxonBase.class;
1957
            String className = null;
1958
            if(searchModes.contains(TaxaAndNamesSearchMode.doTaxa) && !searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)){
1959
                taxonBaseSubclass = Taxon.class;
1960
            } else if (!searchModes.contains(TaxaAndNamesSearchMode.doTaxa) && searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)) {
1961
                className = "eu.etaxonomy.cdm.model.taxon.Synonym";
1962
            }
1963
            luceneSearches.add(prepareFindByFullTextSearch(taxonBaseSubclass,
1964
                    queryString, classification, subtree, className,
1965
                    includeUnpublished, languages, highlightFragments, sortFields));
1966
            idFieldMap.put(CdmBaseType.TAXON_BASE, "id");
1967
            /* A) does not work!!!!
1968
            if(addDistributionFilter){
1969
                // in this case we need a filter which uses a join query
1970
                // to get the TaxonBase documents for the DescriptionElementBase documents
1971
                // which are matching the areas in question
1972
                Query taxonAreaJoinQuery = createByDistributionJoinQuery(
1973
                        namedAreaList,
1974
                        distributionStatusList,
1975
                        distributionFilterQueryFactory
1976
                        );
1977
                multiIndexByAreaFilter.add(new QueryWrapperFilter(taxonAreaJoinQuery), Occur.SHOULD);
1978
            }
1979
            */
1980
            if(addDistributionFilter && searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)){
1981
                // add additional area filter for synonyms
1982
                String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
1983
                String toField = "accTaxon" + AcceptedTaxonBridge.DOC_KEY_ID_SUFFIX; // id in TaxonBase index
1984

    
1985
                //TODO replace by createByDistributionJoinQuery
1986
                BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, distributionFilterQueryFactory);
1987
                Query taxonAreaJoinQuery = distributionFilterQueryFactory.newJoinQuery(Distribution.class, fromField, true, byDistributionQuery, toField, Taxon.class, ScoreMode.None);
1988
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
1989

    
1990
            }
1991
        }
1992

    
1993
        // search by CommonTaxonName
1994
        if(searchModes.contains(TaxaAndNamesSearchMode.doTaxaByCommonNames)) {
1995
            // B)
1996
            QueryFactory descriptionElementQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(DescriptionElementBase.class);
1997
            Query byCommonNameJoinQuery = descriptionElementQueryFactory.newJoinQuery(
1998
                    CommonTaxonName.class,
1999
                    "inDescription.taxon.id",
2000
                    true,
2001
                    QueryFactory.addTypeRestriction(
2002
                                createByDescriptionElementFullTextQuery(queryString, classification, subtree, null, languages, descriptionElementQueryFactory)
2003
                                , CommonTaxonName.class
2004
                                ).build(), "id", null, ScoreMode.Max);
2005
            if (logger.isDebugEnabled()){logger.debug("byCommonNameJoinQuery: " + byCommonNameJoinQuery.toString());}
2006
            LuceneSearch byCommonNameSearch = new LuceneSearch(luceneIndexToolProvider,
2007
                    GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, Taxon.class);
2008
            byCommonNameSearch.setCdmTypRestriction(Taxon.class);
2009
            Builder builder = new BooleanQuery.Builder();
2010
            builder.add(byCommonNameJoinQuery, Occur.MUST);
2011
            if(!includeUnpublished)  {
2012
                QueryFactory taxonBaseQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(TaxonBase.class);
2013
                builder.add(taxonBaseQueryFactory.newBooleanQuery("publish", true), Occur.MUST);
2014
            }
2015
            byCommonNameSearch.setQuery(builder.build());
2016
            byCommonNameSearch.setSortFields(sortFields);
2017

    
2018
            idFieldMap.put(CdmBaseType.TAXON_BASE, "id");
2019

    
2020
            luceneSearches.add(byCommonNameSearch);
2021

    
2022
            /* A) does not work!!!!
2023
            luceneSearches.add(
2024
                    prepareByDescriptionElementFullTextSearch(CommonTaxonName.class,
2025
                            queryString, classification, null, languages, highlightFragments)
2026
                        );
2027
            idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
2028
            if(addDistributionFilter){
2029
                // in this case we are able to use DescriptionElementBase documents
2030
                // which are matching the areas in question directly
2031
                BooleanQuery byDistributionQuery = createByDistributionQuery(
2032
                        namedAreaList,
2033
                        distributionStatusList,
2034
                        distributionFilterQueryFactory
2035
                        );
2036
                multiIndexByAreaFilter.add(new QueryWrapperFilter(byDistributionQuery), Occur.SHOULD);
2037
            } */
2038
        }
2039

    
2040

    
2041
        // search by misapplied names
2042
        //TODO merge with pro parte synonym search once #7487 is fixed
2043
        if(searchModes.contains(TaxaAndNamesSearchMode.doMisappliedNames) /*|| searchModes.contains(TaxaAndNamesSearchMode.doSynonyms) */) {
2044
            // NOTE:
2045
            // prepareFindByTaxonRelationFullTextSearch() is making use of JoinUtil.createJoinQuery()
2046
            // which allows doing query time joins
2047
            // finds the misapplied name (Taxon B) which is an misapplication for
2048
            // a related Taxon A.
2049
            //
2050
            Set<TaxonRelationshipType> relTypes = new HashSet<>();
2051
            if (searchModes.contains(TaxaAndNamesSearchMode.doMisappliedNames)){
2052
                relTypes.addAll(TaxonRelationshipType.allMisappliedNameTypes());
2053
            }
2054
//            if (searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)){
2055
//                relTypes.addAll(TaxonRelationshipType.allSynonymTypes());
2056
//            }
2057

    
2058
            luceneSearches.add(prepareFindByTaxonRelationFullTextSearch(
2059
                    new TaxonRelationshipEdge(relTypes, Direction.relatedTo),
2060
                    queryString, classification, subtree, includeUnpublished, languages, highlightFragments, sortFields));
2061
            idFieldMap.put(CdmBaseType.TAXON_BASE, "id");
2062

    
2063
            if(addDistributionFilter){
2064
                String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
2065

    
2066
                /*
2067
                 * Here I was facing a weird and nasty bug which took me bugging be really for hours until I found this solution.
2068
                 * Maybe this is a bug in java itself.
2069
                 *
2070
                 * When the string toField is constructed by using the expression TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString()
2071
                 * directly:
2072
                 *
2073
                 *    String toField = "relation." + TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString() +".to.id";
2074
                 *
2075
                 * The byDistributionQuery fails, however when the uuid is first stored in another string variable the query
2076
                 * will execute as expected:
2077
                 *
2078
                 *    String misappliedNameForUuid = TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString();
2079
                 *    String toField = "relation." + misappliedNameForUuid +".to.id";
2080
                 *
2081
                 * Comparing both strings by the String.equals method returns true, so both String are identical.
2082
                 *
2083
                 * The bug occurs when running eu.etaxonomy.cdm.api.service.TaxonServiceSearchTest in eclipse and in maven and seems to to be
2084
                 * 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)
2085
                 * The bug is persistent after a reboot of the development computer.
2086
                 */
2087
//                String misappliedNameForUuid = TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString();
2088
//                String toField = "relation." + misappliedNameForUuid +".to.id";
2089
                String toField = "relation.1ed87175-59dd-437e-959e-0d71583d8417.to.id";
2090
//                System.out.println("relation.1ed87175-59dd-437e-959e-0d71583d8417.to.id".equals("relation." + misappliedNameForUuid +".to.id") ? " > identical" : " > different");
2091
//                System.out.println("relation.1ed87175-59dd-437e-959e-0d71583d8417.to.id".equals("relation." + TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString() +".to.id") ? " > identical" : " > different");
2092

    
2093
                //TODO replace by createByDistributionJoinQuery
2094
                BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, distributionFilterQueryFactory);
2095
                Query taxonAreaJoinQuery = distributionFilterQueryFactory.newJoinQuery(Distribution.class,
2096
                        fromField, true, byDistributionQuery, toField, null, ScoreMode.None);
2097

    
2098
//                debug code for bug described above
2099
                //does not compile anymore since changing from lucene 3.6.2 to lucene 4.10+
2100
//                DocIdSet filterMatchSet = filter.getDocIdSet(luceneIndexToolProvider.getIndexReaderFor(Taxon.class));
2101
//                System.err.println(DocIdBitSetPrinter.docsAsString(filterMatchSet, 100));
2102

    
2103
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
2104
            }
2105
        }
2106

    
2107
        // search by pro parte synonyms
2108
        if(searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)) {
2109
            //TODO merge with misapplied name search once #7487 is fixed
2110
            Set<TaxonRelationshipType> relTypes = new HashSet<>();
2111
            relTypes.addAll(TaxonRelationshipType.allSynonymTypes());
2112

    
2113
            luceneSearches.add(prepareFindByTaxonRelationFullTextSearch(
2114
                    new TaxonRelationshipEdge(relTypes, Direction.relatedTo),
2115
                    queryString, classification, subtree, includeUnpublished, languages, highlightFragments, sortFields));
2116
            idFieldMap.put(CdmBaseType.TAXON_BASE, "id");
2117

    
2118
            if(addDistributionFilter){
2119
                String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
2120
                String toField = "relation.8a896603-0fa3-44c6-9cd7-df2d8792e577.to.id";
2121
                BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, distributionFilterQueryFactory);
2122
                Query taxonAreaJoinQuery = distributionFilterQueryFactory.newJoinQuery(Distribution.class,
2123
                        fromField, true, byDistributionQuery, toField, null, ScoreMode.None);
2124
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
2125
            }
2126
        }//end pro parte synonyms
2127

    
2128
        LuceneMultiSearch multiSearch = new LuceneMultiSearch(luceneIndexToolProvider,
2129
                luceneSearches.toArray(new LuceneSearch[luceneSearches.size()]));
2130

    
2131
        if(addDistributionFilter){
2132

    
2133
            // B)
2134
            // in this case we need a filter which uses a join query
2135
            // to get the TaxonBase documents for the DescriptionElementBase documents
2136
            // which are matching the areas in question
2137
            //
2138
            // for doTaxa, doByCommonName
2139
            Query taxonAreaJoinQuery = createByDistributionJoinQuery(
2140
                    namedAreaList,
2141
                    distributionStatusList,
2142
                    distributionFilterQueryFactory,
2143
                    Taxon.class, true
2144
                    );
2145
            multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
2146
        }
2147

    
2148
        if (addDistributionFilter){
2149
            multiSearch.setFilter(multiIndexByAreaFilterBuilder.build());
2150
        }
2151

    
2152

    
2153
        // --- execute search
2154
        TopGroups<BytesRef> topDocsResultSet;
2155
        try {
2156
            topDocsResultSet = multiSearch.executeSearch(pageSize, pageNumber);
2157
        } catch (ParseException e) {
2158
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
2159
            luceneParseException.setStackTrace(e.getStackTrace());
2160
            throw luceneParseException;
2161
        }
2162

    
2163
        // --- initialize taxa, highlight matches ....
2164
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(multiSearch, multiSearch.getQuery());
2165

    
2166

    
2167
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
2168
                topDocsResultSet, multiSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
2169

    
2170
        long totalHits = (topDocsResultSet != null) ? Long.valueOf(topDocsResultSet.totalGroupCount) : 0;
2171
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
2172
    }
2173

    
2174
    /**
2175
     * @param namedAreaList at least one area must be in the list
2176
     * @param distributionStatusList optional
2177
     * @param toType toType
2178
     *      Optional parameter. Only used for debugging to print the toType documents
2179
     * @param asFilter TODO
2180
     * @return
2181
     * @throws IOException
2182
     */
2183
    protected Query createByDistributionJoinQuery(
2184
            List<NamedArea> namedAreaList,
2185
            List<PresenceAbsenceTerm> distributionStatusList,
2186
            QueryFactory queryFactory, Class<? extends CdmBase> toType, boolean asFilter
2187
            ) throws IOException {
2188

    
2189
        String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
2190
        String toField = "id"; // id in toType usually this is the TaxonBase index
2191

    
2192
        BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, queryFactory);
2193

    
2194
        ScoreMode scoreMode = asFilter ?  ScoreMode.None : ScoreMode.Max;
2195

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

    
2198
        return taxonAreaJoinQuery;
2199
    }
2200

    
2201
    /**
2202
     * @param namedAreaList
2203
     * @param distributionStatusList
2204
     * @param queryFactory
2205
     * @return
2206
     */
2207
    private BooleanQuery createByDistributionQuery(List<NamedArea> namedAreaList,
2208
            List<PresenceAbsenceTerm> distributionStatusList, QueryFactory queryFactory) {
2209
        Builder areaQueryBuilder = new Builder();
2210
        // area field from Distribution
2211
        areaQueryBuilder.add(queryFactory.newEntityIdsQuery("area.id", namedAreaList), Occur.MUST);
2212

    
2213
        // status field from Distribution
2214
        if(distributionStatusList != null && distributionStatusList.size() > 0){
2215
            areaQueryBuilder.add(queryFactory.newEntityIdsQuery("status.id", distributionStatusList), Occur.MUST);
2216
        }
2217

    
2218
        BooleanQuery areaQuery = areaQueryBuilder.build();
2219
        logger.debug("createByDistributionQuery() query: " + areaQuery.toString());
2220
        return areaQuery;
2221
    }
2222

    
2223
    /**
2224
     * This method has been primarily created for testing the area join query but might
2225
     * also be useful in other situations
2226
     *
2227
     * @param namedAreaList
2228
     * @param distributionStatusList
2229
     * @param classification
2230
     * @param highlightFragments
2231
     * @return
2232
     * @throws IOException
2233
     */
2234
    protected LuceneSearch prepareByDistributionSearch(
2235
            List<NamedArea> namedAreaList, List<PresenceAbsenceTerm> distributionStatusList,
2236
            Classification classification, TaxonNode subtree) throws IOException {
2237

    
2238
        Builder finalQueryBuilder = new Builder();
2239

    
2240
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, Taxon.class);
2241

    
2242
        // FIXME is this query factory using the wrong type?
2243
        QueryFactory taxonQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(Taxon.class);
2244

    
2245
        SortField[] sortFields = new  SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING, false)};
2246
        luceneSearch.setSortFields(sortFields);
2247

    
2248

    
2249
        Query byAreaQuery = createByDistributionJoinQuery(namedAreaList, distributionStatusList, taxonQueryFactory, null, false);
2250

    
2251
        finalQueryBuilder.add(byAreaQuery, Occur.MUST);
2252

    
2253
        if(classification != null){
2254
            finalQueryBuilder.add(taxonQueryFactory.newEntityIdQuery(AcceptedTaxonBridge.DOC_KEY_CLASSIFICATION_ID, classification), Occur.MUST);
2255
        }
2256
        if(subtree != null){
2257
            finalQueryBuilder.add(taxonQueryFactory.newTermQuery(AcceptedTaxonBridge.DOC_KEY_TREEINDEX, subtree.treeIndexWc(), true), Occur.MUST);
2258
        }
2259
        BooleanQuery finalQuery = finalQueryBuilder.build();
2260
        logger.info("prepareByAreaSearch() query: " + finalQuery.toString());
2261
        luceneSearch.setQuery(finalQuery);
2262

    
2263
        return luceneSearch;
2264
    }
2265

    
2266
    @Override
2267
    public Pager<SearchResult<TaxonBase>> findByDescriptionElementFullText(
2268
            Class<? extends DescriptionElementBase> clazz, String queryString,
2269
            Classification classification, TaxonNode subtree, List<Feature> features, List<Language> languages,
2270
            boolean highlightFragments, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException {
2271

    
2272
        LuceneSearch luceneSearch = prepareByDescriptionElementFullTextSearch(clazz, queryString, classification, subtree, features, languages, highlightFragments);
2273

    
2274
        // --- execute search
2275
        TopGroups<BytesRef> topDocsResultSet;
2276
        try {
2277
            topDocsResultSet = luceneSearch.executeSearch(pageSize, pageNumber);
2278
        } catch (ParseException e) {
2279
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
2280
            luceneParseException.setStackTrace(e.getStackTrace());
2281
            throw luceneParseException;
2282
        }
2283

    
2284
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
2285
        idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
2286

    
2287
        // --- initialize taxa, highlight matches ....
2288
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
2289
        @SuppressWarnings("rawtypes")
2290
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
2291
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
2292

    
2293
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
2294
        return new DefaultPagerImpl<>(pageNumber, Long.valueOf(totalHits), pageSize, searchResults);
2295
    }
2296

    
2297
    @Override
2298
    public Pager<SearchResult<TaxonBase>> findByEverythingFullText(String queryString,
2299
            Classification classification, TaxonNode subtree, boolean includeUnpublished, List<Language> languages, boolean highlightFragments,
2300
            Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException, LuceneMultiSearchException {
2301

    
2302
        LuceneSearch luceneSearchByDescriptionElement = prepareByDescriptionElementFullTextSearch(null, queryString,
2303
                classification, subtree,
2304
                null, languages, highlightFragments);
2305
        LuceneSearch luceneSearchByTaxonBase = prepareFindByFullTextSearch(null, queryString, classification, subtree, null,
2306
                includeUnpublished, languages, highlightFragments, null);
2307

    
2308
        LuceneMultiSearch multiSearch = new LuceneMultiSearch(luceneIndexToolProvider, luceneSearchByDescriptionElement, luceneSearchByTaxonBase);
2309

    
2310
        // --- execute search
2311
        TopGroups<BytesRef> topDocsResultSet;
2312
        try {
2313
            topDocsResultSet = multiSearch.executeSearch(pageSize, pageNumber);
2314
        } catch (ParseException e) {
2315
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
2316
            luceneParseException.setStackTrace(e.getStackTrace());
2317
            throw luceneParseException;
2318
        }
2319

    
2320
        // --- initialize taxa, highlight matches ....
2321
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(multiSearch, multiSearch.getQuery());
2322

    
2323
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
2324
        idFieldMap.put(CdmBaseType.TAXON_BASE, "id");
2325
        idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
2326

    
2327
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
2328
                topDocsResultSet, multiSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
2329

    
2330
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
2331
        return new DefaultPagerImpl<>(pageNumber, Long.valueOf(totalHits), pageSize, searchResults);
2332
    }
2333

    
2334
    /**
2335
     * @param clazz
2336
     * @param queryString
2337
     * @param classification
2338
     * @param features
2339
     * @param languages
2340
     * @param highlightFragments
2341
     * @param directorySelectClass
2342
     * @return
2343
     */
2344
    protected LuceneSearch prepareByDescriptionElementFullTextSearch(Class<? extends CdmBase> clazz,
2345
            String queryString, Classification classification, TaxonNode subtree, List<Feature> features,
2346
            List<Language> languages, boolean highlightFragments) {
2347

    
2348
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, DescriptionElementBase.class);
2349
        QueryFactory descriptionElementQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(DescriptionElementBase.class);
2350

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

    
2353
        BooleanQuery finalQuery = createByDescriptionElementFullTextQuery(queryString, classification, subtree, features,
2354
                languages, descriptionElementQueryFactory);
2355

    
2356
        luceneSearch.setSortFields(sortFields);
2357
        luceneSearch.setCdmTypRestriction(clazz);
2358
        luceneSearch.setQuery(finalQuery);
2359
        if(highlightFragments){
2360
            luceneSearch.setHighlightFields(descriptionElementQueryFactory.getTextFieldNamesAsArray());
2361
        }
2362

    
2363
        return luceneSearch;
2364
    }
2365

    
2366
    /**
2367
     * @param queryString
2368
     * @param classification
2369
     * @param features
2370
     * @param languages
2371
     * @param descriptionElementQueryFactory
2372
     * @return
2373
     */
2374
    private BooleanQuery createByDescriptionElementFullTextQuery(String queryString,
2375
            Classification classification, TaxonNode subtree, List<Feature> features,
2376
            List<Language> languages, QueryFactory descriptionElementQueryFactory) {
2377

    
2378
        Builder finalQueryBuilder = new Builder();
2379
        Builder textQueryBuilder = new Builder();
2380

    
2381
        if(!StringUtils.isEmpty(queryString)){
2382

    
2383
            textQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("titleCache", queryString), Occur.SHOULD);
2384

    
2385
            // common name
2386
            Builder nameQueryBuilder = new Builder();
2387
            if(languages == null || languages.size() == 0){
2388
                nameQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("name", queryString), Occur.MUST);
2389
            } else {
2390
                Builder languageSubQueryBuilder = new Builder();
2391
                for(Language lang : languages){
2392
                    languageSubQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("language.uuid",  lang.getUuid().toString(), false), Occur.SHOULD);
2393
                }
2394
                nameQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("name", queryString), Occur.MUST);
2395
                nameQueryBuilder.add(languageSubQueryBuilder.build(), Occur.MUST);
2396
            }
2397
            textQueryBuilder.add(nameQueryBuilder.build(), Occur.SHOULD);
2398

    
2399

    
2400
            // text field from TextData
2401
            textQueryBuilder.add(descriptionElementQueryFactory.newMultilanguageTextQuery("text", queryString, languages), Occur.SHOULD);
2402

    
2403
            // --- TermBase fields - by representation ----
2404
            // state field from CategoricalData
2405
            textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("stateData.state", queryString, languages), Occur.SHOULD);
2406

    
2407
            // state field from CategoricalData
2408
            textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("stateData.modifyingText", queryString, languages), Occur.SHOULD);
2409

    
2410
            // area field from Distribution
2411
            textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("area", queryString, languages), Occur.SHOULD);
2412

    
2413
            // status field from Distribution
2414
            textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("status", queryString, languages), Occur.SHOULD);
2415

    
2416
            finalQueryBuilder.add(textQueryBuilder.build(), Occur.MUST);
2417

    
2418
        }
2419
        // --- classification ----
2420

    
2421

    
2422
        if(classification != null){
2423
            finalQueryBuilder.add(descriptionElementQueryFactory.newEntityIdQuery("inDescription.taxon.taxonNodes.classification.id", classification), Occur.MUST);
2424
        }
2425
        if(subtree != null){
2426
            finalQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("inDescription.taxon.taxonNodes.treeIndex", subtree.treeIndexWc(), true), Occur.MUST);
2427
        }
2428

    
2429
        // --- IdentifieableEntity fields - by uuid
2430
        if(features != null && features.size() > 0 ){
2431
            finalQueryBuilder.add(descriptionElementQueryFactory.newEntityUuidsQuery("feature.uuid", features), Occur.MUST);
2432
        }
2433

    
2434
        // the description must be associated with a taxon
2435
        finalQueryBuilder.add(descriptionElementQueryFactory.newIsNotNullQuery("inDescription.taxon.id"), Occur.MUST);
2436

    
2437
        BooleanQuery finalQuery = finalQueryBuilder.build();
2438
        logger.info("prepareByDescriptionElementFullTextSearch() query: " + finalQuery.toString());
2439
        return finalQuery;
2440
    }
2441

    
2442
    @Override
2443
    public List<Synonym> createInferredSynonyms(Taxon taxon, Classification classification, SynonymType type, boolean doWithMisappliedNames){
2444

    
2445
        List <Synonym> inferredSynonyms = new ArrayList<>();
2446
        List<Synonym> inferredSynonymsToBeRemoved = new ArrayList<>();
2447

    
2448
        Map <UUID, IZoologicalName> zooHashMap = new HashMap<>();
2449
        boolean includeUnpublished = INCLUDE_UNPUBLISHED;
2450

    
2451
        UUID nameUuid= taxon.getName().getUuid();
2452
        IZoologicalName taxonName = getZoologicalName(nameUuid, zooHashMap);
2453
        String epithetOfTaxon = null;
2454
        String infragenericEpithetOfTaxon = null;
2455
        String infraspecificEpithetOfTaxon = null;
2456
        if (taxonName.isSpecies()){
2457
             epithetOfTaxon= taxonName.getSpecificEpithet();
2458
        } else if (taxonName.isInfraGeneric()){
2459
            infragenericEpithetOfTaxon = taxonName.getInfraGenericEpithet();
2460
        } else if (taxonName.isInfraSpecific()){
2461
            infraspecificEpithetOfTaxon = taxonName.getInfraSpecificEpithet();
2462
        }
2463
        String genusOfTaxon = taxonName.getGenusOrUninomial();
2464
        Set<TaxonNode> nodes = taxon.getTaxonNodes();
2465
        List<String> taxonNames = new ArrayList<>();
2466

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

    
2471
            if (node.getClassification().equals(classification)){
2472
                if (!node.isTopmostNode()){
2473
                    TaxonNode parent = node.getParent();
2474
                    parent = CdmBase.deproxy(parent);
2475
                    TaxonName parentName =  parent.getTaxon().getName();
2476
                    IZoologicalName zooParentName = CdmBase.deproxy(parentName);
2477
                    Taxon parentTaxon = CdmBase.deproxy(parent.getTaxon());
2478

    
2479
                    //create inferred synonyms for species, subspecies
2480
                    if ((parentName.isGenus() || parentName.isSpecies() || parentName.getRank().equals(Rank.SUBGENUS())) ){
2481

    
2482
                        Synonym inferredEpithet = null;
2483
                        Synonym inferredGenus = null;
2484
                        Synonym potentialCombination = null;
2485

    
2486
                        List<String> propertyPaths = new ArrayList<>();
2487
                        propertyPaths.add("synonym");
2488
                        propertyPaths.add("synonym.name");
2489
                        List<OrderHint> orderHintsSynonyms = new ArrayList<>();
2490
                        orderHintsSynonyms.add(new OrderHint("titleCache", SortOrder.ASCENDING));
2491

    
2492
                        List<Synonym> synonyMsOfParent = dao.getSynonyms(parentTaxon, SynonymType.HETEROTYPIC_SYNONYM_OF(), null, null,orderHintsSynonyms,propertyPaths);
2493
                        List<Synonym> synonymsOfTaxon= dao.getSynonyms(taxon, SynonymType.HETEROTYPIC_SYNONYM_OF(),
2494
                                null, null,orderHintsSynonyms,propertyPaths);
2495

    
2496
                        List<TaxonRelationship> taxonRelListParent = new ArrayList<>();
2497
                        List<TaxonRelationship> taxonRelListTaxon = new ArrayList<>();
2498
                        if (doWithMisappliedNames){
2499
                            List<OrderHint> orderHintsMisapplied = new ArrayList<>();
2500
                            orderHintsMisapplied.add(new OrderHint("relatedFrom.titleCache", SortOrder.ASCENDING));
2501
                            taxonRelListParent = dao.getTaxonRelationships(parentTaxon, TaxonRelationshipType.MISAPPLIED_NAME_FOR(),
2502
                                    includeUnpublished, null, null, orderHintsMisapplied, propertyPaths, Direction.relatedTo);
2503
                            taxonRelListTaxon = dao.getTaxonRelationships(taxon, TaxonRelationshipType.MISAPPLIED_NAME_FOR(),
2504
                                    includeUnpublished, null, null, orderHintsMisapplied, propertyPaths, Direction.relatedTo);
2505
                        }
2506

    
2507
                        if (type.equals(SynonymType.INFERRED_EPITHET_OF())){
2508
                            for (Synonym synonymRelationOfParent:synonyMsOfParent){
2509

    
2510
                                inferredEpithet = createInferredEpithets(taxon,
2511
                                        zooHashMap, taxonName, epithetOfTaxon,
2512
                                        infragenericEpithetOfTaxon,
2513
                                        infraspecificEpithetOfTaxon,
2514
                                        taxonNames, parentName,
2515
                                        synonymRelationOfParent);
2516

    
2517
                                inferredSynonyms.add(inferredEpithet);
2518
                                zooHashMap.put(inferredEpithet.getName().getUuid(), inferredEpithet.getName());
2519
                                taxonNames.add(inferredEpithet.getName().getNameCache());
2520
                            }
2521

    
2522
                            if (doWithMisappliedNames){
2523

    
2524
                                for (TaxonRelationship taxonRelationship: taxonRelListParent){
2525
                                     Taxon misappliedName = taxonRelationship.getFromTaxon();
2526

    
2527
                                     inferredEpithet = createInferredEpithets(taxon,
2528
                                             zooHashMap, taxonName, epithetOfTaxon,
2529
                                             infragenericEpithetOfTaxon,
2530
                                             infraspecificEpithetOfTaxon,
2531
                                             taxonNames, parentName,
2532
                                             misappliedName);
2533

    
2534
                                    inferredSynonyms.add(inferredEpithet);
2535
                                    zooHashMap.put(inferredEpithet.getName().getUuid(), inferredEpithet.getName());
2536
                                    taxonNames.add(inferredEpithet.getName().getNameCache());
2537
                                }
2538
                            }
2539

    
2540
                            if (!taxonNames.isEmpty()){
2541
                                List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2542
                                if (!synNotInCDM.isEmpty()){
2543
                                    inferredSynonymsToBeRemoved.clear();
2544

    
2545
                                    for (Synonym syn :inferredSynonyms){
2546
                                        IZoologicalName name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2547
                                        if (!synNotInCDM.contains(name.getNameCache())){
2548
                                            inferredSynonymsToBeRemoved.add(syn);
2549
                                        }
2550
                                    }
2551

    
2552
                                    // Remove identified Synonyms from inferredSynonyms
2553
                                    for (Synonym synonym : inferredSynonymsToBeRemoved) {
2554
                                        inferredSynonyms.remove(synonym);
2555
                                    }
2556
                                }
2557
                            }
2558

    
2559
                        }else if (type.equals(SynonymType.INFERRED_GENUS_OF())){
2560

    
2561
                            for (Synonym synonymRelationOfTaxon:synonymsOfTaxon){
2562

    
2563
                                inferredGenus = createInferredGenus(taxon,
2564
                                        zooHashMap, taxonName, epithetOfTaxon,
2565
                                        genusOfTaxon, taxonNames, zooParentName, synonymRelationOfTaxon);
2566

    
2567
                                inferredSynonyms.add(inferredGenus);
2568
                                zooHashMap.put(inferredGenus.getName().getUuid(), inferredGenus.getName());
2569
                                taxonNames.add(inferredGenus.getName().getNameCache());
2570
                            }
2571

    
2572
                            if (doWithMisappliedNames){
2573

    
2574
                                for (TaxonRelationship taxonRelationship: taxonRelListTaxon){
2575
                                    Taxon misappliedName = taxonRelationship.getFromTaxon();
2576
                                    inferredGenus = createInferredGenus(taxon, zooHashMap, taxonName, infraspecificEpithetOfTaxon, genusOfTaxon, taxonNames, zooParentName,  misappliedName);
2577

    
2578
                                    inferredSynonyms.add(inferredGenus);
2579
                                    zooHashMap.put(inferredGenus.getName().getUuid(), inferredGenus.getName());
2580
                                     taxonNames.add(inferredGenus.getName().getNameCache());
2581
                                }
2582
                            }
2583

    
2584

    
2585
                            if (!taxonNames.isEmpty()){
2586
                                List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2587
                                IZoologicalName name;
2588
                                if (!synNotInCDM.isEmpty()){
2589
                                    inferredSynonymsToBeRemoved.clear();
2590

    
2591
                                    for (Synonym syn :inferredSynonyms){
2592
                                        name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2593
                                        if (!synNotInCDM.contains(name.getNameCache())){
2594
                                            inferredSynonymsToBeRemoved.add(syn);
2595
                                        }
2596
                                    }
2597

    
2598
                                    // Remove identified Synonyms from inferredSynonyms
2599
                                    for (Synonym synonym : inferredSynonymsToBeRemoved) {
2600
                                        inferredSynonyms.remove(synonym);
2601
                                    }
2602
                                }
2603
                            }
2604

    
2605
                        }else if (type.equals(SynonymType.POTENTIAL_COMBINATION_OF())){
2606

    
2607
                            Reference sourceReference = null; // TODO: Determination of sourceReference is redundant
2608
                            //for all synonyms of the parent...
2609
                            for (Synonym synonymRelationOfParent:synonyMsOfParent){
2610
                                TaxonName synName;
2611
                                HibernateProxyHelper.deproxy(synonymRelationOfParent);
2612

    
2613
                                synName = synonymRelationOfParent.getName();
2614

    
2615
                                // Set the sourceReference
2616
                                sourceReference = synonymRelationOfParent.getSec();
2617

    
2618
                                // Determine the idInSource
2619
                                String idInSourceParent = getIdInSource(synonymRelationOfParent);
2620

    
2621
                                IZoologicalName parentSynZooName = getZoologicalName(synName.getUuid(), zooHashMap);
2622
                                String synParentGenus = parentSynZooName.getGenusOrUninomial();
2623
                                String synParentInfragenericName = null;
2624
                                String synParentSpecificEpithet = null;
2625

    
2626
                                if (parentSynZooName.isInfraGeneric()){
2627
                                    synParentInfragenericName = parentSynZooName.getInfraGenericEpithet();
2628
                                }
2629
                                if (parentSynZooName.isSpecies()){
2630
                                    synParentSpecificEpithet = parentSynZooName.getSpecificEpithet();
2631
                                }
2632

    
2633
                               /* if (synGenusName != null && !synonymsGenus.containsKey(synGenusName)){
2634
                                    synonymsGenus.put(synGenusName, idInSource);
2635
                                }*/
2636

    
2637
                                //for all synonyms of the taxon
2638

    
2639
                                for (Synonym synonymRelationOfTaxon:synonymsOfTaxon){
2640

    
2641
                                    IZoologicalName zooSynName = getZoologicalName(synonymRelationOfTaxon.getName().getUuid(), zooHashMap);
2642
                                    potentialCombination = createPotentialCombination(idInSourceParent, parentSynZooName, zooSynName,
2643
                                            synParentGenus,
2644
                                            synParentInfragenericName,
2645
                                            synParentSpecificEpithet, synonymRelationOfTaxon, zooHashMap);
2646

    
2647
                                    taxon.addSynonym(potentialCombination, SynonymType.POTENTIAL_COMBINATION_OF());
2648
                                    inferredSynonyms.add(potentialCombination);
2649
                                    zooHashMap.put(potentialCombination.getName().getUuid(), potentialCombination.getName());
2650
                                     taxonNames.add(potentialCombination.getName().getNameCache());
2651
                                }
2652
                            }
2653

    
2654
                            if (doWithMisappliedNames){
2655

    
2656
                                for (TaxonRelationship parentRelationship: taxonRelListParent){
2657

    
2658
                                    TaxonName misappliedParentName;
2659

    
2660
                                    Taxon misappliedParent = parentRelationship.getFromTaxon();
2661
                                    misappliedParentName = misappliedParent.getName();
2662

    
2663
                                    HibernateProxyHelper.deproxy(misappliedParent);
2664

    
2665
                                    // Set the sourceReference
2666
                                    sourceReference = misappliedParent.getSec();
2667

    
2668
                                    // Determine the idInSource
2669
                                    String idInSourceParent = getIdInSource(misappliedParent);
2670

    
2671
                                    IZoologicalName parentSynZooName = getZoologicalName(misappliedParentName.getUuid(), zooHashMap);
2672
                                    String synParentGenus = parentSynZooName.getGenusOrUninomial();
2673
                                    String synParentInfragenericName = null;
2674
                                    String synParentSpecificEpithet = null;
2675

    
2676
                                    if (parentSynZooName.isInfraGeneric()){
2677
                                        synParentInfragenericName = parentSynZooName.getInfraGenericEpithet();
2678
                                    }
2679
                                    if (parentSynZooName.isSpecies()){
2680
                                        synParentSpecificEpithet = parentSynZooName.getSpecificEpithet();
2681
                                    }
2682

    
2683
                                    for (TaxonRelationship taxonRelationship: taxonRelListTaxon){
2684
                                        Taxon misappliedName = taxonRelationship.getFromTaxon();
2685
                                        IZoologicalName zooMisappliedName = getZoologicalName(misappliedName.getName().getUuid(), zooHashMap);
2686
                                        potentialCombination = createPotentialCombination(
2687
                                                idInSourceParent, parentSynZooName, zooMisappliedName,
2688
                                                synParentGenus,
2689
                                                synParentInfragenericName,
2690
                                                synParentSpecificEpithet, misappliedName, zooHashMap);
2691

    
2692
                                        taxon.addSynonym(potentialCombination, SynonymType.POTENTIAL_COMBINATION_OF());
2693
                                        inferredSynonyms.add(potentialCombination);
2694
                                        zooHashMap.put(potentialCombination.getName().getUuid(), potentialCombination.getName());
2695
                                         taxonNames.add(potentialCombination.getName().getNameCache());
2696
                                    }
2697
                                }
2698
                            }
2699

    
2700
                            if (!taxonNames.isEmpty()){
2701
                                List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2702
                                IZoologicalName name;
2703
                                if (!synNotInCDM.isEmpty()){
2704
                                    inferredSynonymsToBeRemoved.clear();
2705
                                    for (Synonym syn :inferredSynonyms){
2706
                                        try{
2707
                                            name = syn.getName();
2708
                                        }catch (ClassCastException e){
2709
                                            name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2710
                                        }
2711
                                        if (!synNotInCDM.contains(name.getNameCache())){
2712
                                            inferredSynonymsToBeRemoved.add(syn);
2713
                                        }
2714
                                     }
2715
                                    // Remove identified Synonyms from inferredSynonyms
2716
                                    for (Synonym synonym : inferredSynonymsToBeRemoved) {
2717
                                        inferredSynonyms.remove(synonym);
2718
                                    }
2719
                                }
2720
                            }
2721
                        }
2722
                    }else {
2723
                        logger.info("The synonym type is not defined.");
2724
                        return inferredSynonyms;
2725
                    }
2726
                }
2727
            }
2728
        }
2729

    
2730
        return inferredSynonyms;
2731
    }
2732

    
2733
    private Synonym createPotentialCombination(String idInSourceParent,
2734
            IZoologicalName parentSynZooName, 	IZoologicalName zooSynName, String synParentGenus,
2735
            String synParentInfragenericName, String synParentSpecificEpithet,
2736
            TaxonBase<?> syn, Map<UUID, IZoologicalName> zooHashMap) {
2737
        Synonym potentialCombination;
2738
        Reference sourceReference;
2739
        IZoologicalName inferredSynName;
2740
        HibernateProxyHelper.deproxy(syn);
2741

    
2742
        // Set sourceReference
2743
        sourceReference = syn.getSec();
2744
        if (sourceReference == null){
2745
            logger.warn("The synonym has no sec reference because it is a misapplied name! Take the sec reference of taxon");
2746
            //TODO:Remove
2747
            if (!parentSynZooName.getTaxa().isEmpty()){
2748
                TaxonBase<?> taxon = parentSynZooName.getTaxa().iterator().next();
2749

    
2750
                sourceReference = taxon.getSec();
2751
            }
2752
        }
2753
        String synTaxonSpecificEpithet = zooSynName.getSpecificEpithet();
2754

    
2755
        String synTaxonInfraSpecificName= null;
2756

    
2757
        if (parentSynZooName.isSpecies()){
2758
            synTaxonInfraSpecificName = zooSynName.getInfraSpecificEpithet();
2759
        }
2760

    
2761
        /*if (epithetName != null && !synonymsEpithet.contains(epithetName)){
2762
            synonymsEpithet.add(epithetName);
2763
        }*/
2764

    
2765
        //create potential combinations...
2766
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(syn.getName().getRank());
2767

    
2768
        inferredSynName.setGenusOrUninomial(synParentGenus);
2769
        if (zooSynName.isSpecies()){
2770
              inferredSynName.setSpecificEpithet(synTaxonSpecificEpithet);
2771
              if (parentSynZooName.isInfraGeneric()){
2772
                  inferredSynName.setInfraGenericEpithet(synParentInfragenericName);
2773
              }
2774
        }
2775
        if (zooSynName.isInfraSpecific()){
2776
            inferredSynName.setSpecificEpithet(synParentSpecificEpithet);
2777
            inferredSynName.setInfraSpecificEpithet(synTaxonInfraSpecificName);
2778
        }
2779
        if (parentSynZooName.isInfraGeneric()){
2780
            inferredSynName.setInfraGenericEpithet(synParentInfragenericName);
2781
        }
2782

    
2783
        potentialCombination = Synonym.NewInstance(inferredSynName, null);
2784

    
2785
        // Set the sourceReference
2786
        potentialCombination.setSec(sourceReference);
2787

    
2788

    
2789
        // Determine the idInSource
2790
        String idInSourceSyn= getIdInSource(syn);
2791

    
2792
        if (idInSourceParent != null && idInSourceSyn != null) {
2793
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation, idInSourceSyn + "; " + idInSourceParent, POTENTIAL_COMBINATION_NAMESPACE, sourceReference, null);
2794
            inferredSynName.addSource(originalSource);
2795
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation, idInSourceSyn + "; " + idInSourceParent, POTENTIAL_COMBINATION_NAMESPACE, sourceReference, null);
2796
            potentialCombination.addSource(originalSource);
2797
        }
2798

    
2799
        return potentialCombination;
2800
    }
2801

    
2802
    private Synonym createInferredGenus(Taxon taxon,
2803
            Map<UUID, IZoologicalName> zooHashMap, IZoologicalName taxonName,
2804
            String epithetOfTaxon, String genusOfTaxon,
2805
            List<String> taxonNames, IZoologicalName zooParentName,
2806
            TaxonBase syn) {
2807

    
2808
        Synonym inferredGenus;
2809
        TaxonName synName;
2810
        IZoologicalName inferredSynName;
2811
        synName =syn.getName();
2812
        HibernateProxyHelper.deproxy(syn);
2813

    
2814
        // Determine the idInSource
2815
        String idInSourceSyn = getIdInSource(syn);
2816
        String idInSourceTaxon = getIdInSource(taxon);
2817
        // Determine the sourceReference
2818
        Reference sourceReference = syn.getSec();
2819

    
2820
        //logger.warn(sourceReference.getTitleCache());
2821

    
2822
        synName = syn.getName();
2823
        IZoologicalName synZooName = getZoologicalName(synName.getUuid(), zooHashMap);
2824
        String synSpeciesEpithetName = synZooName.getSpecificEpithet();
2825
         /* if (synonymsEpithet != null && !synonymsEpithet.contains(synSpeciesEpithetName)){
2826
            synonymsEpithet.add(synSpeciesEpithetName);
2827
        }*/
2828

    
2829
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(taxon.getName().getRank());
2830
        //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...
2831

    
2832
        inferredSynName.setGenusOrUninomial(genusOfTaxon);
2833
        if (zooParentName.isInfraGeneric()){
2834
            inferredSynName.setInfraGenericEpithet(zooParentName.getInfraGenericEpithet());
2835
        }
2836

    
2837
        if (taxonName.isSpecies()){
2838
            inferredSynName.setSpecificEpithet(synSpeciesEpithetName);
2839
        }
2840
        if (taxonName.isInfraSpecific()){
2841
            inferredSynName.setSpecificEpithet(epithetOfTaxon);
2842
            inferredSynName.setInfraSpecificEpithet(synZooName.getInfraGenericEpithet());
2843
        }
2844

    
2845
        inferredGenus = Synonym.NewInstance(inferredSynName, null);
2846

    
2847
        // Set the sourceReference
2848
        inferredGenus.setSec(sourceReference);
2849

    
2850
        // Add the original source
2851
        if (idInSourceSyn != null && idInSourceTaxon != null) {
2852
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2853
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2854
            inferredGenus.addSource(originalSource);
2855

    
2856
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2857
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2858
            inferredSynName.addSource(originalSource);
2859
            originalSource = null;
2860

    
2861
        }else{
2862
            logger.error("There is an idInSource missing: " + idInSourceSyn + " of Synonym or " + idInSourceTaxon + " of Taxon");
2863
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2864
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2865
            inferredGenus.addSource(originalSource);
2866

    
2867
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2868
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2869
            inferredSynName.addSource(originalSource);
2870
            originalSource = null;
2871
        }
2872

    
2873
        taxon.addSynonym(inferredGenus, SynonymType.INFERRED_GENUS_OF());
2874

    
2875
        return inferredGenus;
2876
    }
2877

    
2878
    private Synonym createInferredEpithets(Taxon taxon,
2879
            Map<UUID, IZoologicalName> zooHashMap, IZoologicalName taxonName,
2880
            String epithetOfTaxon, String infragenericEpithetOfTaxon,
2881
            String infraspecificEpithetOfTaxon, List<String> taxonNames,
2882
            TaxonName parentName, TaxonBase<?> syn) {
2883

    
2884
        Synonym inferredEpithet;
2885
        TaxonName synName;
2886
        IZoologicalName inferredSynName;
2887
        HibernateProxyHelper.deproxy(syn);
2888

    
2889
        // Determine the idInSource
2890
        String idInSourceSyn = getIdInSource(syn);
2891
        String idInSourceTaxon =  getIdInSource(taxon);
2892
        // Determine the sourceReference
2893
        Reference sourceReference = syn.getSec();
2894

    
2895
        if (sourceReference == null){
2896
             logger.warn("The synonym has no sec reference because it is a misapplied name! Take the sec reference of taxon" + taxon.getSec());
2897
             sourceReference = taxon.getSec();
2898
        }
2899

    
2900
        synName = syn.getName();
2901
        IZoologicalName zooSynName = getZoologicalName(synName.getUuid(), zooHashMap);
2902
        String synGenusName = zooSynName.getGenusOrUninomial();
2903
        String synInfraGenericEpithet = null;
2904
        String synSpecificEpithet = null;
2905

    
2906
        if (zooSynName.getInfraGenericEpithet() != null){
2907
            synInfraGenericEpithet = zooSynName.getInfraGenericEpithet();
2908
        }
2909

    
2910
        if (zooSynName.isInfraSpecific()){
2911
            synSpecificEpithet = zooSynName.getSpecificEpithet();
2912
        }
2913

    
2914
           /* if (synGenusName != null && !synonymsGenus.containsKey(synGenusName)){
2915
            synonymsGenus.put(synGenusName, idInSource);
2916
        }*/
2917

    
2918
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(taxon.getName().getRank());
2919

    
2920
        // DEBUG TODO: for subgenus or subspecies the infrageneric or infraspecific epithet should be used!!!
2921
        if (epithetOfTaxon == null && infragenericEpithetOfTaxon == null && infraspecificEpithetOfTaxon == null) {
2922
            logger.error("This specificEpithet is NULL" + taxon.getTitleCache());
2923
        }
2924
        inferredSynName.setGenusOrUninomial(synGenusName);
2925

    
2926
        if (parentName.isInfraGeneric()){
2927
            inferredSynName.setInfraGenericEpithet(synInfraGenericEpithet);
2928
        }
2929
        if (taxonName.isSpecies()){
2930
            inferredSynName.setSpecificEpithet(epithetOfTaxon);
2931
        }else if (taxonName.isInfraSpecific()){
2932
            inferredSynName.setSpecificEpithet(synSpecificEpithet);
2933
            inferredSynName.setInfraSpecificEpithet(infraspecificEpithetOfTaxon);
2934
        }
2935

    
2936
        inferredEpithet = Synonym.NewInstance(inferredSynName, null);
2937

    
2938
        // Set the sourceReference
2939
        inferredEpithet.setSec(sourceReference);
2940

    
2941
        /* Add the original source
2942
        if (idInSource != null) {
2943
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(idInSource, "InferredEpithetOf", syn.getSec(), null);
2944

    
2945
            // Add the citation
2946
            Reference citation = getCitation(syn);
2947
            if (citation != null) {
2948
                originalSource.setCitation(citation);
2949
                inferredEpithet.addSource(originalSource);
2950
            }
2951
        }*/
2952
        String taxonId = idInSourceTaxon+ "; " + idInSourceSyn;
2953

    
2954

    
2955
        IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2956
                taxonId, INFERRED_EPITHET_NAMESPACE, sourceReference, null);
2957

    
2958
        inferredEpithet.addSource(originalSource);
2959

    
2960
        originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2961
                taxonId, INFERRED_EPITHET_NAMESPACE, sourceReference, null);
2962

    
2963
        inferredSynName.addSource(originalSource);
2964

    
2965
        taxon.addSynonym(inferredEpithet, SynonymType.INFERRED_EPITHET_OF());
2966

    
2967
        return inferredEpithet;
2968
    }
2969

    
2970
    /**
2971
     * Returns an existing IZoologicalName or extends an internal hashmap if it does not exist.
2972
     * Very likely only useful for createInferredSynonyms().
2973
     * @param uuid
2974
     * @param zooHashMap
2975
     * @return
2976
     */
2977
    private IZoologicalName getZoologicalName(UUID uuid, Map <UUID, IZoologicalName> zooHashMap) {
2978
        IZoologicalName taxonName =nameDao.findZoologicalNameByUUID(uuid);
2979
        if (taxonName == null) {
2980
            taxonName = zooHashMap.get(uuid);
2981
        }
2982
        return taxonName;
2983
    }
2984

    
2985
    /**
2986
     * Returns the idInSource for a given Synonym.
2987
     * @param syn
2988
     */
2989
    private String getIdInSource(TaxonBase<?> taxonBase) {
2990
        String idInSource = null;
2991
        Set<IdentifiableSource> sources = taxonBase.getSources();
2992
        if (sources.size() == 1) {
2993
            IdentifiableSource source = sources.iterator().next();
2994
            if (source != null) {
2995
                idInSource  = source.getIdInSource();
2996
            }
2997
        } else if (sources.size() > 1) {
2998
            int count = 1;
2999
            idInSource = "";
3000
            for (IdentifiableSource source : sources) {
3001
                idInSource += source.getIdInSource();
3002
                if (count < sources.size()) {
3003
                    idInSource += "; ";
3004
                }
3005
                count++;
3006
            }
3007
        } else if (sources.size() == 0){
3008
            logger.warn("No idInSource for TaxonBase " + taxonBase.getUuid() + " - " + taxonBase.getTitleCache());
3009
        }
3010

    
3011
        return idInSource;
3012
    }
3013

    
3014
    /**
3015
     * Returns the citation for a given Synonym.
3016
     * @param syn
3017
     */
3018
    private Reference getCitation(Synonym syn) {
3019
        Reference citation = null;
3020
        Set<IdentifiableSource> sources = syn.getSources();
3021
        if (sources.size() == 1) {
3022
            IdentifiableSource source = sources.iterator().next();
3023
            if (source != null) {
3024
                citation = source.getCitation();
3025
            }
3026
        } else if (sources.size() > 1) {
3027
            logger.warn("This Synonym has more than one source: " + syn.getUuid() + " (" + syn.getTitleCache() +")");
3028
        }
3029

    
3030
        return citation;
3031
    }
3032

    
3033
    @Override
3034
    public List<Synonym>  createAllInferredSynonyms(Taxon taxon, Classification tree, boolean doWithMisappliedNames){
3035
        List <Synonym> inferredSynonyms = new ArrayList<>();
3036

    
3037
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.INFERRED_EPITHET_OF(), doWithMisappliedNames));
3038
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.INFERRED_GENUS_OF(), doWithMisappliedNames));
3039
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.POTENTIAL_COMBINATION_OF(), doWithMisappliedNames));
3040

    
3041
        return inferredSynonyms;
3042
    }
3043

    
3044
    @Override
3045
    public List<Classification> listClassifications(TaxonBase taxonBase, Integer limit, Integer start, List<String> propertyPaths) {
3046

    
3047
        // TODO quickly implemented, create according dao !!!!
3048
        Set<TaxonNode> nodes = new HashSet<>();
3049
        Set<Classification> classifications = new HashSet<>();
3050
        List<Classification> list = new ArrayList<>();
3051

    
3052
        if (taxonBase == null) {
3053
            return list;
3054
        }
3055

    
3056
        taxonBase = load(taxonBase.getUuid());
3057

    
3058
        if (taxonBase instanceof Taxon) {
3059
            nodes.addAll(((Taxon)taxonBase).getTaxonNodes());
3060
        } else {
3061
            Taxon taxon = ((Synonym)taxonBase).getAcceptedTaxon();
3062
            if (taxon != null){
3063
                nodes.addAll(taxon.getTaxonNodes());
3064
            }
3065
        }
3066
        for (TaxonNode node : nodes) {
3067
            classifications.add(node.getClassification());
3068
        }
3069
        list.addAll(classifications);
3070
        return list;
3071
    }
3072

    
3073
    @Override
3074
    @Transactional(readOnly = false)
3075
    public UpdateResult changeRelatedTaxonToSynonym(UUID fromTaxonUuid,
3076
            UUID toTaxonUuid,
3077
            TaxonRelationshipType oldRelationshipType,
3078
            SynonymType synonymType) throws DataChangeNoRollbackException {
3079
        UpdateResult result = new UpdateResult();
3080
        Taxon fromTaxon = (Taxon) dao.load(fromTaxonUuid);
3081
        Taxon toTaxon = (Taxon) dao.load(toTaxonUuid);
3082
        result = changeRelatedTaxonToSynonym(fromTaxon, toTaxon, oldRelationshipType, synonymType);
3083

    
3084
        result.addUpdatedObject(toTaxon);
3085
        result.addUpdatedObject(result.getCdmEntity());
3086

    
3087
        return result;
3088
    }
3089

    
3090
    @Override
3091
    @Transactional(readOnly = false)
3092
    public UpdateResult changeRelatedTaxonToSynonym(Taxon fromTaxon, Taxon toTaxon, TaxonRelationshipType oldRelationshipType,
3093
            SynonymType synonymType) throws DataChangeNoRollbackException {
3094

    
3095
        UpdateResult result = new UpdateResult();
3096
        // Create new synonym using concept name
3097
        TaxonName synonymName = fromTaxon.getName();
3098

    
3099
        // Remove concept relation from taxon
3100
        toTaxon.removeTaxon(fromTaxon, oldRelationshipType);
3101

    
3102
        // Create a new synonym for the taxon
3103
        Synonym synonym;
3104
        if (synonymType != null
3105
                && synonymType.equals(SynonymType.HOMOTYPIC_SYNONYM_OF())){
3106
            synonym = Synonym.NewInstance(synonymName, fromTaxon.getSec());
3107
            toTaxon.addHomotypicSynonym(synonym);
3108
        } else{
3109
            synonym = toTaxon.addHeterotypicSynonymName(synonymName);
3110
        }
3111
        //keep the publish flag
3112
        synonym.setPublish(fromTaxon.isPublish());
3113
        this.saveOrUpdate(toTaxon);
3114
        //TODO: configurator and classification
3115
        TaxonDeletionConfigurator config = new TaxonDeletionConfigurator();
3116
        config.setDeleteNameIfPossible(false);
3117
        result.includeResult(this.deleteTaxon(fromTaxon.getUuid(), config, null));
3118
        result.setCdmEntity(synonym);
3119
        result.addUpdatedObject(toTaxon);
3120
        result.addUpdatedObject(synonym);
3121
        return result;
3122
    }
3123

    
3124
    @Override
3125
    public DeleteResult isDeletable(UUID taxonBaseUuid, DeleteConfiguratorBase config){
3126
        DeleteResult result = new DeleteResult();
3127
        TaxonBase<?> taxonBase = load(taxonBaseUuid);
3128
        Set<CdmBase> references = commonService.getReferencingObjectsForDeletion(taxonBase);
3129
        if (taxonBase instanceof Taxon){
3130
            TaxonDeletionConfigurator taxonConfig = (TaxonDeletionConfigurator) config;
3131
            List<String> propertyPaths = new ArrayList<>();
3132
            propertyPaths.add("taxonNodes");
3133
            Taxon taxon = (Taxon)load(taxonBaseUuid, propertyPaths);
3134

    
3135
            result = isDeletableForTaxon(references, taxonConfig );
3136

    
3137
            if (taxonConfig.isDeleteNameIfPossible()){
3138
                if (taxonBase.getName() != null){
3139
                    DeleteResult nameResult = nameService.isDeletable(taxonBase.getName().getUuid(), taxonConfig.getNameDeletionConfig(), taxon.getUuid());
3140
                    if (!nameResult.isOk()){
3141
                        result.addExceptions(nameResult.getExceptions());
3142
                    }
3143
                }
3144

    
3145
            }
3146
        }else{
3147
            SynonymDeletionConfigurator synonymConfig = (SynonymDeletionConfigurator) config;
3148
            result = isDeletableForSynonym(references, synonymConfig);
3149
            if (synonymConfig.isDeleteNameIfPossible() && taxonBase.getName() != null){
3150
                DeleteResult nameResult = nameService.isDeletable(taxonBase.getName().getUuid(), synonymConfig.getNameDeletionConfig(), taxonBase.getUuid());
3151
                if (!nameResult.isOk()){
3152
                    result.addExceptions(nameResult.getExceptions());
3153

    
3154
                }
3155
            }
3156
        }
3157

    
3158
        return result;
3159
    }
3160

    
3161
    private DeleteResult isDeletableForSynonym(Set<CdmBase> references, SynonymDeletionConfigurator config){
3162

    
3163
        DeleteResult result = new DeleteResult();
3164
        for (CdmBase ref: references){
3165
            if (!(ref instanceof Taxon || ref instanceof TaxonName || ref instanceof SecundumSource)){
3166
                String message = "The Synonym can't be deleted as long as it is referenced by " + ref.getClass().getSimpleName() + " with id "+ ref.getId();
3167
                result.addException(new ReferencedObjectUndeletableException(message));
3168
                result.addRelatedObject(ref);
3169
                result.setAbort();
3170
            }
3171
        }
3172

    
3173
        return result;
3174
    }
3175

    
3176
    private DeleteResult isDeletableForTaxon(Set<CdmBase> references, TaxonDeletionConfigurator config){
3177
        String message = null;
3178
        DeleteResult result = new DeleteResult();
3179
        for (CdmBase ref: references){
3180
            if (!(ref instanceof TaxonName || ref instanceof SecundumSource)){
3181
            	message = null;
3182
                if (!config.isDeleteSynonymRelations() && (ref instanceof Synonym)){
3183
                    message = "The taxon can't be deleted as long as it has synonyms.";
3184
                }
3185
                if (!config.isDeleteDescriptions() && (ref instanceof DescriptionBase)){
3186
                    message = "The taxon can't be deleted as long as it has factual data.";
3187
                }
3188

    
3189
                if (!config.isDeleteTaxonNodes() && (ref instanceof TaxonNode)){
3190
                    message = "The taxon can't be deleted as long as it belongs to a taxon node.";
3191
                }
3192
                if (ref instanceof TaxonNode && config.getClassificationUuid() != null && !config.isDeleteInAllClassifications() && !((TaxonNode)ref).getClassification().getUuid().equals(config.getClassificationUuid())){
3193
                    message = "The taxon can't be deleted as long as it is used in more than one classification";
3194

    
3195
                }
3196
                if (!config.isDeleteTaxonRelationships() && (ref instanceof TaxonRelationship)){
3197
                    if (!config.isDeleteMisappliedNames() &&
3198
                            (((TaxonRelationship)ref).getType().isMisappliedName())){
3199
                        message = "The taxon can't be deleted as long as it has misapplied names or invalid designations.";
3200
                    } else{
3201
                        message = "The taxon can't be deleted as long as it belongs to taxon relationship.";
3202
                    }
3203
                }
3204
                if (ref instanceof PolytomousKeyNode){
3205
                    message = "The taxon can't be deleted as long as it is referenced by a polytomous key node.";
3206
                }
3207

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

    
3212
               /* //PolytomousKeyNode
3213
                if (referencingObject.isInstanceOf(PolytomousKeyNode.class)){
3214
                    String message = "Taxon" + taxon.getTitleCache() + " can't be deleted as it is used in polytomous key node";
3215
                    return message;
3216
                }*/
3217

    
3218
                //TaxonInteraction
3219
                if (ref.isInstanceOf(TaxonInteraction.class)){
3220
                    message = "Taxon can't be deleted as it is used in taxonInteraction#taxon2";
3221
                }
3222

    
3223
              //TaxonInteraction
3224
                if (ref.isInstanceOf(DeterminationEvent.class)){
3225
                    message = "Taxon can't be deleted as it is used in a determination event";
3226
                }
3227
            }
3228
            if (message != null){
3229
	            result.addException(new ReferencedObjectUndeletableException(message));
3230
	            result.addRelatedObject(ref);
3231
	            result.setAbort();
3232
            }
3233
        }
3234

    
3235
        return result;
3236
    }
3237

    
3238
    @Override
3239
    public IncludedTaxaDTO listIncludedTaxa(UUID taxonUuid, IncludedTaxonConfiguration config) {
3240
        IncludedTaxaDTO result = new IncludedTaxaDTO(taxonUuid);
3241

    
3242
        //preliminary implementation
3243

    
3244
        Set<Taxon> taxa = new HashSet<>();
3245
        TaxonBase<?> taxonBase = find(taxonUuid);
3246
        if (taxonBase == null){
3247
            return new IncludedTaxaDTO();
3248
        }else if (taxonBase.isInstanceOf(Taxon.class)){
3249
            Taxon taxon = CdmBase.deproxy(taxonBase, Taxon.class);
3250
            taxa.add(taxon);
3251
        }else if (taxonBase.isInstanceOf(Synonym.class)){
3252
            //TODO partial synonyms ??
3253
            //TODO synonyms in general
3254
            Synonym syn = CdmBase.deproxy(taxonBase, Synonym.class);
3255
            taxa.add(syn.getAcceptedTaxon());
3256
        }else{
3257
            throw new IllegalArgumentException("Unhandled class " + taxonBase.getClass().getSimpleName());
3258
        }
3259

    
3260
        Set<Taxon> related = makeRelatedIncluded(taxa, result, config);
3261
        int i = 0;
3262
        while((! related.isEmpty()) && i++ < 100){  //to avoid
3263
             related = makeRelatedIncluded(related, result, config);
3264
        }
3265

    
3266
        return result;
3267
    }
3268

    
3269
    /**
3270
     * @param uncheckedTaxa
3271
     * @param existingTaxa
3272
     * @param config
3273
     *
3274
     * Computes all children and conceptually congruent and included taxa and adds them to the existingTaxa
3275
     * data structure.
3276
     * @return the set of conceptually related taxa for further use
3277
     */
3278
    private Set<Taxon> makeRelatedIncluded(Set<Taxon> uncheckedTaxa, IncludedTaxaDTO existingTaxa, IncludedTaxonConfiguration config) {
3279

    
3280
        //children
3281
        Set<TaxonNode> taxonNodes = new HashSet<>();
3282
        for (Taxon taxon: uncheckedTaxa){
3283
            taxonNodes.addAll(taxon.getTaxonNodes());
3284
        }
3285

    
3286
        Set<Taxon> children = new HashSet<>();
3287
        if (! config.onlyCongruent){
3288
            for (TaxonNode node: taxonNodes){
3289
                List<TaxonNode> childNodes = nodeService.loadChildNodesOfTaxonNode(node, null, true, config.includeUnpublished, null);
3290
                for (TaxonNode child : childNodes){
3291
                    children.add(child.getTaxon());
3292
                }
3293
            }
3294
            children.remove(null);  // just to be on the save side
3295
        }
3296

    
3297
        Iterator<Taxon> it = children.iterator();
3298
        while(it.hasNext()){
3299
            UUID uuid = it.next().getUuid();
3300
            if (existingTaxa.contains(uuid)){
3301
                it.remove();
3302
            }else{
3303
                existingTaxa.addIncludedTaxon(uuid, new ArrayList<>(), false);
3304
            }
3305
        }
3306

    
3307
        //concept relations
3308
        Set<Taxon> uncheckedAndChildren = new HashSet<>(uncheckedTaxa);
3309
        uncheckedAndChildren.addAll(children);
3310

    
3311
        Set<Taxon> relatedTaxa = makeConceptIncludedTaxa(uncheckedAndChildren, existingTaxa, config);
3312

    
3313

    
3314
        Set<Taxon> result = new HashSet<>(relatedTaxa);
3315
        return result;
3316
    }
3317

    
3318
    /**
3319
     * Computes all conceptually congruent or included taxa and adds them to the existingTaxa data structure.
3320
     * @return the set of these computed taxa
3321
     */
3322
    private Set<Taxon> makeConceptIncludedTaxa(Set<Taxon> unchecked, IncludedTaxaDTO existingTaxa, IncludedTaxonConfiguration config) {
3323
        Set<Taxon> result = new HashSet<>();
3324

    
3325
        for (Taxon taxon : unchecked){
3326
            Set<TaxonRelationship> fromRelations = taxon.getRelationsFromThisTaxon();
3327
            Set<TaxonRelationship> toRelations = taxon.getRelationsToThisTaxon();
3328

    
3329
            for (TaxonRelationship fromRel : fromRelations){
3330
                if (config.includeDoubtful == false && fromRel.isDoubtful()){
3331
                    continue;
3332
                }
3333
                TaxonRelationshipType fromRelType = fromRel.getType();
3334
                if (fromRelType.equals(TaxonRelationshipType.CONGRUENT_TO()) ||
3335
                        !config.onlyCongruent && (
3336
                            fromRelType.equals(TaxonRelationshipType.INCLUDES()) ||
3337
                            fromRelType.equals(TaxonRelationshipType.CONGRUENT_OR_INCLUDES())
3338
                        )
3339
                    ){
3340
                    result.add(fromRel.getToTaxon());
3341
                }
3342
            }
3343

    
3344
            for (TaxonRelationship toRel : toRelations){
3345
                if (config.includeDoubtful == false && toRel.isDoubtful()){
3346
                    continue;
3347
                }
3348
                TaxonRelationshipType fromRelType = toRel.getType();
3349
                if (fromRelType.equals(TaxonRelationshipType.CONGRUENT_TO()) ||
3350
                        !config.includeDoubtful && fromRelType.equals(TaxonRelationshipType.TAXONOMICALLY_INCLUDED_IN())){
3351
                    result.add(toRel.getFromTaxon());
3352
                }
3353
            }
3354
        }
3355

    
3356
        Iterator<Taxon> it = result.iterator();
3357
        while(it.hasNext()){
3358
            UUID uuid = it.next().getUuid();
3359
            if (existingTaxa.contains(uuid)){
3360
                it.remove();
3361
            }else{
3362
                existingTaxa.addIncludedTaxon(uuid, new ArrayList<>(), false);
3363
            }
3364
        }
3365
        return result;
3366
    }
3367

    
3368
    @Override
3369
    public List<TaxonBase> findTaxaByName(MatchingTaxonConfigurator config){
3370
        @SuppressWarnings("rawtypes")
3371
        List<TaxonBase> taxonList = dao.getTaxaByName(true, config.isIncludeSynonyms(), false, false, false,
3372
                config.getTaxonNameTitle(), null, null, MatchMode.EXACT, null, config.isIncludeSynonyms(), null, 0, 0, config.getPropertyPath());
3373
        return taxonList;
3374
    }
3375

    
3376
	@Override
3377
	@Transactional(readOnly = true)
3378
	public <S extends TaxonBase> Pager<IdentifiedEntityDTO<S>> findByIdentifier(
3379
			Class<S> clazz, String identifier, DefinedTerm identifierType, TaxonNode subtreeFilter,
3380
			MatchMode matchmode, boolean includeEntity, Integer pageSize,
3381
			Integer pageNumber,	List<String> propertyPaths) {
3382
		if (subtreeFilter == null){
3383
			return findByIdentifier(clazz, identifier, identifierType, matchmode, includeEntity, pageSize, pageNumber, propertyPaths);
3384
		}
3385

    
3386
		long numberOfResults = dao.countByIdentifier(clazz, identifier, identifierType, subtreeFilter, matchmode);
3387
        List<Object[]> daoResults = new ArrayList<>();
3388
        if(numberOfResults > 0) { // no point checking again
3389
        	daoResults = dao.findByIdentifier(clazz, identifier, identifierType, subtreeFilter,
3390
    				matchmode, includeEntity, pageSize, pageNumber, propertyPaths);
3391
        }
3392

    
3393
        List<IdentifiedEntityDTO<S>> result = new ArrayList<>();
3394
        for (Object[] daoObj : daoResults){
3395
        	if (includeEntity){
3396
        		result.add(new IdentifiedEntityDTO<>((DefinedTerm)daoObj[0], (String)daoObj[1], (S)daoObj[2]));
3397
        	}else{
3398
        		result.add(new IdentifiedEntityDTO<>((DefinedTerm)daoObj[0], (String)daoObj[1], (UUID)daoObj[2], (String)daoObj[3], null));
3399
        	}
3400
        }
3401
		return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, result);
3402
	}
3403

    
3404
	@Override
3405
    @Transactional(readOnly = true)
3406
    public <S extends TaxonBase> Pager<MarkedEntityDTO<S>> findByMarker(
3407
            Class<S> clazz, MarkerType markerType, Boolean markerValue,
3408
            TaxonNode subtreeFilter, boolean includeEntity, TaxonTitleType titleType,
3409
            Integer pageSize, Integer pageNumber, List<String> propertyPaths) {
3410
        if (subtreeFilter == null){
3411
            return super.findByMarker (clazz, markerType, markerValue, includeEntity, pageSize, pageNumber, propertyPaths);
3412
        }
3413

    
3414
        Long numberOfResults = dao.countByMarker(clazz, markerType, markerValue, subtreeFilter);
3415
        List<Object[]> daoResults = new ArrayList<>();
3416
        if(numberOfResults > 0) { // no point checking again
3417
            daoResults = dao.findByMarker(clazz, markerType, markerValue, subtreeFilter,
3418
                    includeEntity, titleType, pageSize, pageNumber, propertyPaths);
3419
        }
3420

    
3421
        List<MarkedEntityDTO<S>> result = new ArrayList<>();
3422
        for (Object[] daoObj : daoResults){
3423
            if (includeEntity){
3424
                result.add(new MarkedEntityDTO<S>((MarkerType)daoObj[0], (Boolean)daoObj[1], (S)daoObj[2]));
3425
            }else{
3426
                result.add(new MarkedEntityDTO<S>((MarkerType)daoObj[0], (Boolean)daoObj[1], (UUID)daoObj[2], (String)daoObj[3]));
3427
            }
3428
        }
3429
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, result);
3430
    }
3431

    
3432
    @Override
3433
	public UpdateResult moveFactualDateToAnotherTaxon(UUID fromTaxonUuid, UUID toTaxonUuid){
3434
		UpdateResult result = new UpdateResult();
3435

    
3436
		Taxon fromTaxon = (Taxon)dao.load(fromTaxonUuid);
3437
		Taxon toTaxon = (Taxon) dao.load(toTaxonUuid);
3438
    	for(TaxonDescription description : fromTaxon.getDescriptions()){
3439
              //reload to avoid session conflicts
3440
              description = HibernateProxyHelper.deproxy(description, TaxonDescription.class);
3441

    
3442
              String moveMessage = String.format("Description moved from %s", fromTaxon);
3443
              if(description.isProtectedTitleCache()){
3444
                  String separator = "";
3445
                  if(!StringUtils.isBlank(description.getTitleCache())){
3446
                      separator = " - ";
3447
                  }
3448
                  description.setTitleCache(description.getTitleCache() + separator + moveMessage, true);
3449
              }
3450
              Annotation annotation = Annotation.NewInstance(moveMessage, Language.getDefaultLanguage());
3451
              annotation.setAnnotationType(AnnotationType.TECHNICAL());
3452
              description.addAnnotation(annotation);
3453
              toTaxon.addDescription(description);
3454
              dao.saveOrUpdate(toTaxon);
3455
              dao.saveOrUpdate(fromTaxon);
3456
              result.addUpdatedObject(toTaxon);
3457
              result.addUpdatedObject(fromTaxon);
3458

    
3459
        }
3460

    
3461
    	return result;
3462
	}
3463

    
3464
	@Override
3465
	@Transactional(readOnly = false)
3466
	public UpdateResult swapSynonymAndAcceptedTaxon(UUID synonymUUid,
3467
			UUID acceptedTaxonUuid, boolean setNameInSource, boolean newUuidForAcceptedTaxon, SecReferenceHandlingSwapEnum secHandling, UUID newSecAcc, UUID newSecSyn) {
3468
		TaxonBase<?> base = this.load(synonymUUid);
3469
		Synonym syn = HibernateProxyHelper.deproxy(base, Synonym.class);
3470
		base = this.load(acceptedTaxonUuid);
3471
		Taxon taxon = HibernateProxyHelper.deproxy(base, Taxon.class);
3472

    
3473
		Reference refAcc = referenceService.load(newSecAcc);
3474
		Reference refSyn = referenceService.load(newSecSyn);
3475

    
3476
		return this.swapSynonymAndAcceptedTaxon(syn, taxon, setNameInSource, newUuidForAcceptedTaxon, secHandling, refAcc, refSyn);
3477
	}
3478

    
3479
    @Override
3480
    public TaxonRelationshipsDTO listTaxonRelationships(UUID taxonUuid, Set<TaxonRelationshipType> directTypes,
3481
            Set<TaxonRelationshipType> inversTypes,
3482
            Direction direction, boolean groupMisapplications,
3483
            boolean includeUnpublished,
3484
            Integer pageSize, Integer pageNumber) {
3485
        TaxonBase<?> taxonBase = dao.load(taxonUuid);
3486
        if (taxonBase == null || !taxonBase.isInstanceOf(TaxonBase.class)){
3487
            //TODO handle
3488
            throw new RuntimeException("Taxon for uuid " + taxonUuid + " not found");
3489
        }else{
3490
            Taxon taxon = CdmBase.deproxy(taxonBase, Taxon.class);
3491
            boolean doDirect = (direction == null || direction == Direction.relatedTo);
3492
            boolean doInvers = (direction == null || direction == Direction.relatedFrom);
3493

    
3494
            TaxonRelationshipsDTO dto = new TaxonRelationshipsDTO();
3495

    
3496
            //TODO paging is difficult because misapplication string is an attribute
3497
            //of toplevel dto
3498
//        long numberOfResults = dao.countTaxonRelationships(taxon, type, includeUnpublished, TaxonRelationship.Direction.relatedFrom);
3499
//        List<TaxonRelationshipsDTO> results = new ArrayList<>();
3500
//        if(numberOfResults > 0) { // no point checking again
3501
//            results = dao.getTaxonRelationships(taxon, type, includeUnpublished, pageSize, pageNumber, orderHints, propertyPaths, TaxonRelationship.Direction.relatedFrom);
3502
//        }
3503
//
3504
//        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);;
3505

    
3506
            //TODO languages
3507
            List<Language> languages = null;
3508
            if (doDirect){
3509
                direction = Direction.relatedTo;
3510
                //TODO order hints, property path
3511
                List<TaxonRelationship> relations = dao.getTaxonRelationships(taxon, directTypes, includeUnpublished, pageSize, pageNumber, null, null, direction.invers());
3512
                for (TaxonRelationship relation : relations){
3513
                    dto.addRelation(relation, direction, languages);
3514
                }
3515
            }
3516
            if (doInvers){
3517
                direction = Direction.relatedFrom;
3518
                //TODO order hints, property path
3519
                List<TaxonRelationship> relations = dao.getTaxonRelationships(taxon, inversTypes, includeUnpublished, pageSize, pageNumber, null, null, direction.invers());
3520
                for (TaxonRelationship relation : relations){
3521
                    dto.addRelation(relation, direction, languages);
3522
                }
3523
            }
3524
            if (groupMisapplications){
3525
                //TODO
3526
                dto.createMisapplicationString();
3527
            }
3528
            return dto;
3529
        }
3530
    }
3531
}
(87-87/95)