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.monitor.IProgressMonitor;
70
import eu.etaxonomy.cdm.compare.taxon.HomotypicGroupTaxonComparator;
71
import eu.etaxonomy.cdm.compare.taxon.TaxonComparator;
72
import eu.etaxonomy.cdm.exception.UnpublishedException;
73
import eu.etaxonomy.cdm.hibernate.HibernateProxyHelper;
74
import eu.etaxonomy.cdm.hibernate.search.AcceptedTaxonBridge;
75
import eu.etaxonomy.cdm.hibernate.search.GroupByTaxonClassBridge;
76
import eu.etaxonomy.cdm.model.CdmBaseType;
77
import eu.etaxonomy.cdm.model.common.Annotation;
78
import eu.etaxonomy.cdm.model.common.AnnotationType;
79
import eu.etaxonomy.cdm.model.common.CdmBase;
80
import eu.etaxonomy.cdm.model.common.IdentifiableEntity;
81
import eu.etaxonomy.cdm.model.common.IdentifiableSource;
82
import eu.etaxonomy.cdm.model.common.Language;
83
import eu.etaxonomy.cdm.model.common.MarkerType;
84
import eu.etaxonomy.cdm.model.common.RelationshipBase.Direction;
85
import eu.etaxonomy.cdm.model.description.CommonTaxonName;
86
import eu.etaxonomy.cdm.model.description.DescriptionBase;
87
import eu.etaxonomy.cdm.model.description.DescriptionElementBase;
88
import eu.etaxonomy.cdm.model.description.DescriptionElementSource;
89
import eu.etaxonomy.cdm.model.description.Distribution;
90
import eu.etaxonomy.cdm.model.description.Feature;
91
import eu.etaxonomy.cdm.model.description.IIdentificationKey;
92
import eu.etaxonomy.cdm.model.description.PolytomousKeyNode;
93
import eu.etaxonomy.cdm.model.description.PresenceAbsenceTerm;
94
import eu.etaxonomy.cdm.model.description.SpecimenDescription;
95
import eu.etaxonomy.cdm.model.description.TaxonDescription;
96
import eu.etaxonomy.cdm.model.description.TaxonInteraction;
97
import eu.etaxonomy.cdm.model.description.TaxonNameDescription;
98
import eu.etaxonomy.cdm.model.location.NamedArea;
99
import eu.etaxonomy.cdm.model.media.ExternalLink;
100
import eu.etaxonomy.cdm.model.media.ExternalLinkType;
101
import eu.etaxonomy.cdm.model.media.Media;
102
import eu.etaxonomy.cdm.model.metadata.SecReferenceHandlingEnum;
103
import eu.etaxonomy.cdm.model.metadata.SecReferenceHandlingSwapEnum;
104
import eu.etaxonomy.cdm.model.name.HomotypicalGroup;
105
import eu.etaxonomy.cdm.model.name.IZoologicalName;
106
import eu.etaxonomy.cdm.model.name.Rank;
107
import eu.etaxonomy.cdm.model.name.TaxonName;
108
import eu.etaxonomy.cdm.model.name.TaxonNameFactory;
109
import eu.etaxonomy.cdm.model.occurrence.DerivedUnit;
110
import eu.etaxonomy.cdm.model.occurrence.DeterminationEvent;
111
import eu.etaxonomy.cdm.model.occurrence.SpecimenOrObservationBase;
112
import eu.etaxonomy.cdm.model.reference.OriginalSourceType;
113
import eu.etaxonomy.cdm.model.reference.Reference;
114
import eu.etaxonomy.cdm.model.taxon.Classification;
115
import eu.etaxonomy.cdm.model.taxon.ITaxonTreeNode;
116
import eu.etaxonomy.cdm.model.taxon.SecundumSource;
117
import eu.etaxonomy.cdm.model.taxon.Synonym;
118
import eu.etaxonomy.cdm.model.taxon.SynonymType;
119
import eu.etaxonomy.cdm.model.taxon.Taxon;
120
import eu.etaxonomy.cdm.model.taxon.TaxonBase;
121
import eu.etaxonomy.cdm.model.taxon.TaxonNode;
122
import eu.etaxonomy.cdm.model.taxon.TaxonRelationship;
123
import eu.etaxonomy.cdm.model.taxon.TaxonRelationshipType;
124
import eu.etaxonomy.cdm.model.term.DefinedTerm;
125
import eu.etaxonomy.cdm.persistence.dao.common.Restriction;
126
import eu.etaxonomy.cdm.persistence.dao.initializer.AbstractBeanInitializer;
127
import eu.etaxonomy.cdm.persistence.dao.name.ITaxonNameDao;
128
import eu.etaxonomy.cdm.persistence.dao.occurrence.IOccurrenceDao;
129
import eu.etaxonomy.cdm.persistence.dao.taxon.IClassificationDao;
130
import eu.etaxonomy.cdm.persistence.dao.taxon.ITaxonDao;
131
import eu.etaxonomy.cdm.persistence.dao.taxon.ITaxonNodeDao;
132
import eu.etaxonomy.cdm.persistence.dto.MergeResult;
133
import eu.etaxonomy.cdm.persistence.dto.UuidAndTitleCache;
134
import eu.etaxonomy.cdm.persistence.query.MatchMode;
135
import eu.etaxonomy.cdm.persistence.query.OrderHint;
136
import eu.etaxonomy.cdm.persistence.query.OrderHint.SortOrder;
137
import eu.etaxonomy.cdm.persistence.query.TaxonTitleType;
138
import eu.etaxonomy.cdm.strategy.cache.common.IIdentifiableEntityCacheStrategy;
139

    
140

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

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

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

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

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

    
159
    @Autowired
160
    private ITaxonNodeDao taxonNodeDao;
161

    
162
    @Autowired
163
    private ITaxonNameDao nameDao;
164

    
165
    @Autowired
166
    private INameService nameService;
167

    
168
    @Autowired
169
    private IOccurrenceService occurrenceService;
170

    
171
    @Autowired
172
    private ITaxonNodeService nodeService;
173

    
174
    @Autowired
175
    private IDescriptionService descriptionService;
176

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

    
183
    @Autowired
184
    private IOccurrenceDao occurrenceDao;
185

    
186
    @Autowired
187
    private IClassificationDao classificationDao;
188

    
189
    @Autowired
190
    private AbstractBeanInitializer beanInitializer;
191

    
192
    @Autowired
193
    private ILuceneIndexToolProvider luceneIndexToolProvider;
194

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

    
200
// ****************************** METHODS ********************************/
201

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

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

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

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

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

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

    
240

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

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

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

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

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

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

    
279
		return result;
280
    }
281

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

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

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

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

    
311
        newTaxon.setName(synonymName);
312

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

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

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

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

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

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

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

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

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

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

    
386
        return result;
387
    }
388

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

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

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

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

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

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

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

    
456
        return result;
457
    }
458

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

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

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

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

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

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

    
534
        UpdateResult result = new UpdateResult();
535

    
536
        TaxonName synonymName = synonym.getName();
537

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

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

    
555
        return result;
556
    }
557

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
727
        Synonym synonym = null;
728

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
860
        return result;
861
    }
862

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

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

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

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

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

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

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

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

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

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

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

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

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

    
935
        numberOfResults += numberTaxaResults;
936

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

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

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

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

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

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

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

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

    
980
        logger.trace("listMedia() - START");
981

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

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

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

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

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

    
1023

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

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

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

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

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

    
1082
        logger.trace("listMedia() - initialize");
1083
        beanInitializer.initializeAll(taxonMedia, propertyPath);
1084

    
1085
        logger.trace("listMedia() - END");
1086

    
1087
        return taxonMedia;
1088
    }
1089

    
1090
    @Override
1091
    public List<TaxonBase> findTaxaByID(Set<Integer> listOfIDs) {
1092
        return this.dao.loadList(listOfIDs, null, null);
1093
    }
1094

    
1095
    @Override
1096
    public TaxonBase findTaxonByUuid(UUID uuid, List<String> propertyPaths){
1097
        return this.dao.findByUuid(uuid, null ,propertyPaths);
1098
    }
1099

    
1100
    @Override
1101
    public long countSynonyms(boolean onlyAttachedToTaxon){
1102
        return this.dao.countSynonyms(onlyAttachedToTaxon);
1103
    }
1104

    
1105
    @Override
1106
    @Transactional(readOnly=false)
1107
    public DeleteResult deleteTaxon(UUID taxonUUID, TaxonDeletionConfigurator config, UUID classificationUuid)  {
1108

    
1109
    	if (config == null){
1110
            config = new TaxonDeletionConfigurator();
1111
        }
1112
    	Taxon taxon = (Taxon)dao.load(taxonUUID);
1113
    	DeleteResult result = new DeleteResult();
1114
    	if (taxon == null){
1115
    	    result.setAbort();
1116
    	    result.addException(new Exception ("The taxon was already deleted."));
1117
    	    return result;
1118
    	}
1119
    	taxon = HibernateProxyHelper.deproxy(taxon);
1120
    	Classification classification = HibernateProxyHelper.deproxy(classificationDao.load(classificationUuid), Classification.class);
1121
    	config.setClassificationUuid(classificationUuid);
1122
        result = isDeletable(taxonUUID, config);
1123

    
1124
        if (result.isOk()){
1125
            // --- DeleteSynonymRelations
1126
            if (config.isDeleteSynonymRelations()){
1127
                boolean removeSynonymNameFromHomotypicalGroup = false;
1128
                // use tmp Set to avoid concurrent modification
1129
                Set<Synonym> synsToDelete = new HashSet<>();
1130
                synsToDelete.addAll(taxon.getSynonyms());
1131
                for (Synonym synonym : synsToDelete){
1132
                    taxon.removeSynonym(synonym, removeSynonymNameFromHomotypicalGroup);
1133

    
1134
                    // --- DeleteSynonymsIfPossible
1135
                    if (config.isDeleteSynonymsIfPossible()){
1136
                        //TODO which value
1137
                        boolean newHomotypicGroupIfNeeded = true;
1138
                        SynonymDeletionConfigurator synConfig = new SynonymDeletionConfigurator();
1139
                        result.includeResult(deleteSynonym(synonym, synConfig));
1140
                    }
1141
                }
1142
            }
1143

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

    
1156
                for (TaxonRelationship taxRel: taxon.getTaxonRelations()){
1157
                    if (config.isDeleteMisappliedNames()
1158
                            && taxRel.getType().isMisappliedName()
1159
                            && taxon.equals(taxRel.getToTaxon())){
1160
                        this.deleteTaxon(taxRel.getFromTaxon().getUuid(), config, classificationUuid);
1161
                    } else if (config.isDeleteConceptRelationships() && taxRel.getType().isConceptRelationship()){
1162

    
1163
                        if (taxon.equals(taxRel.getToTaxon()) && isDeletable(taxRel.getFromTaxon().getUuid(), configRelTaxon).isOk()){
1164
                            this.deleteTaxon(taxRel.getFromTaxon().getUuid(), configRelTaxon, classificationUuid);
1165
                        }else if (isDeletable(taxRel.getToTaxon().getUuid(), configRelTaxon).isOk()){
1166
                            this.deleteTaxon(taxRel.getToTaxon().getUuid(), configRelTaxon, classificationUuid);
1167
                        }
1168
                    }
1169
                    taxon.removeTaxonRelation(taxRel);
1170
                }
1171
            }
1172

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

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

    
1261
             if ((taxon.getTaxonNodes() == null || taxon.getTaxonNodes().size()== 0)  && result.isOk()){
1262
                 try{
1263
                     dao.delete(taxon);
1264
                     result.addDeletedObject(taxon);
1265
                 }catch(Exception e){
1266
                     result.addException(e);
1267
                     result.setError();
1268
                 }
1269
             } else {
1270
                 result.setError();
1271
                 result.addException(new Exception("The Taxon can't be deleted because it is used in a classification."));
1272

    
1273
             }
1274
             //TaxonName
1275
             if (config.isDeleteNameIfPossible() && result.isOk()){
1276
                 DeleteResult nameResult = new DeleteResult();
1277
                 //remove name if possible (and required)
1278
                 if (name != null ){
1279
                     nameResult = nameService.delete(name.getUuid(), config.getNameDeletionConfig());
1280
                 }
1281
                 if (nameResult.isError() || nameResult.isAbort()){
1282
                     result.addRelatedObject(name);
1283
                     result.addExceptions(nameResult.getExceptions());
1284
                 }else{
1285
                     result.includeResult(nameResult);
1286
                 }
1287
             }
1288
       }
1289

    
1290
       return result;
1291
    }
1292

    
1293
    @Override
1294
    @Transactional(readOnly = false)
1295
    public DeleteResult delete(UUID synUUID){
1296
    	Synonym syn = (Synonym)dao.load(synUUID);
1297
        return this.deleteSynonym(syn, null);
1298
    }
1299

    
1300
    @Override
1301
    @Transactional(readOnly = false)
1302
    public DeleteResult deleteSynonym(UUID synonymUuid, SynonymDeletionConfigurator config) {
1303
        return deleteSynonym((Synonym)dao.load(synonymUuid), config);
1304
    }
1305

    
1306
    @Override
1307
    @Transactional(readOnly = false)
1308
    public DeleteResult deleteSynonym(Synonym synonym, SynonymDeletionConfigurator config) {
1309
        DeleteResult result = new DeleteResult();
1310
    	if (synonym == null){
1311
    		result.setAbort();
1312
    		result.addException(new Exception("The synonym was already deleted."));
1313
    		return result;
1314
        }
1315

    
1316
        if (config == null){
1317
            config = new SynonymDeletionConfigurator();
1318
        }
1319

    
1320
        result = isDeletable(synonym.getUuid(), config);
1321

    
1322
        if (result.isOk()){
1323

    
1324
            synonym = HibernateProxyHelper.deproxy(this.load(synonym.getUuid()), Synonym.class);
1325

    
1326
            //remove synonym
1327
            Taxon accTaxon = synonym.getAcceptedTaxon();
1328

    
1329
            if (accTaxon != null){
1330
                accTaxon = HibernateProxyHelper.deproxy(accTaxon, Taxon.class);
1331
                accTaxon.removeSynonym(synonym, false);
1332
                this.saveOrUpdate(accTaxon);
1333
                result.addUpdatedObject(accTaxon);
1334
            }
1335
            this.saveOrUpdate(synonym);
1336
            //#6281
1337
            dao.flush();
1338

    
1339
            TaxonName name = synonym.getName();
1340
            synonym.setName(null);
1341

    
1342
            dao.delete(synonym);
1343
            result.addDeletedObject(synonym);
1344

    
1345
            //remove name if possible (and required)
1346
            if (name != null && config.isDeleteNameIfPossible()){
1347

    
1348
                DeleteResult nameDeleteResult = nameService.delete(name, config.getNameDeletionConfig());
1349
                if (nameDeleteResult.isAbort() || nameDeleteResult.isError()){
1350
                	result.addExceptions(nameDeleteResult.getExceptions());
1351
                	result.addRelatedObject(name);
1352
                	result.addUpdatedObject(name);
1353
                }else{
1354
                    result.addDeletedObject(name);
1355
                }
1356
            }
1357
        }
1358
        return result;
1359
    }
1360

    
1361
    @Override
1362
    public Map<String, Map<UUID,Set<TaxonName>>> findIdenticalTaxonNames(List<UUID> sourceRefUuids, List<String> propertyPaths) {
1363
        return this.dao.findIdenticalNames(sourceRefUuids, propertyPaths);
1364
    }
1365

    
1366
    @Override
1367
    public Taxon findBestMatchingTaxon(String taxonName) {
1368
        MatchingTaxonConfigurator config = MatchingTaxonConfigurator.NewInstance();
1369
        config.setTaxonNameTitle(taxonName);
1370
        return findBestMatchingTaxon(config);
1371
    }
1372

    
1373
    @Override
1374
    public Taxon findBestMatchingTaxon(MatchingTaxonConfigurator config) {
1375

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

    
1397
                    boolean newCandidateInClassification = isInClassification(newCanditate, config);
1398
                    if (! newCandidateInClassification && config.isOnlyMatchingClassificationUuid()){
1399
                        continue;
1400
                    }else if (newCandidateInClassification && ! bestCandidateIsInClassification){
1401
                        bestCandidate = newCanditate;
1402
                        countEqualCandidates = 1;
1403
                        bestCandidateIsInClassification = true;
1404
                        continue;
1405
                    }
1406
                    if (bestCandidate == null){
1407
                        bestCandidate = newCanditate;
1408
                        countEqualCandidates = 1;
1409
                        continue;
1410
                    }
1411
                }else{  //not Taxon.class
1412
                    continue;
1413
                }
1414
                countEqualCandidates++;
1415

    
1416
            }
1417
            if (bestCandidate != null){
1418
                if(countEqualCandidates > 1){
1419
                    logger.info(countEqualCandidates + " equally matching TaxonBases found, using first accepted Taxon: " + bestCandidate.getTitleCache());
1420
                    return bestCandidate;
1421
                } else {
1422
                    logger.info("using accepted Taxon: " + bestCandidate.getTitleCache());
1423
                    return bestCandidate;
1424
                }
1425
            }
1426

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

    
1444
        } catch (Exception e){
1445
            logger.error(e);
1446
            e.printStackTrace();
1447
        }
1448

    
1449
        return bestCandidate;
1450
    }
1451

    
1452
    private boolean isInClassification(Taxon taxon, MatchingTaxonConfigurator config) {
1453
        UUID configClassificationUuid = config.getClassificationUuid();
1454
        if (configClassificationUuid == null){
1455
            return false;
1456
        }
1457
        for (TaxonNode node : taxon.getTaxonNodes()){
1458
            UUID classUuid = node.getClassification().getUuid();
1459
            if (configClassificationUuid.equals(classUuid)){
1460
                return true;
1461
            }
1462
        }
1463
        return false;
1464
    }
1465

    
1466
    private boolean isMatchesSecUuid(Taxon taxon, MatchingTaxonConfigurator config) {
1467
        UUID configSecUuid = config.getSecUuid();
1468
        if (configSecUuid == null){
1469
            return false;
1470
        }
1471
        UUID taxonSecUuid = (taxon.getSec() == null)? null : taxon.getSec().getUuid();
1472
        return configSecUuid.equals(taxonSecUuid);
1473
    }
1474

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

    
1491
    @Override
1492
    @Transactional(readOnly = false)
1493
    public UpdateResult moveSynonymToAnotherTaxon(Synonym oldSynonym, UUID newTaxonUUID, boolean moveHomotypicGroup,
1494
            SynonymType newSynonymType, UUID newSecundumUuid, String newSecundumDetail,
1495
            boolean keepSecundumIfUndefined) throws HomotypicalGroupChangeException {
1496

    
1497
        UpdateResult result = new UpdateResult();
1498
        Taxon newTaxon = CdmBase.deproxy(dao.load(newTaxonUUID), Taxon.class);
1499
        result = moveSynonymToAnotherTaxon(oldSynonym, newTaxon, moveHomotypicGroup, newSynonymType,
1500
                newSecundumUuid, newSecundumDetail, keepSecundumIfUndefined);
1501

    
1502
        return result;
1503
    }
1504

    
1505
    @Override
1506
    @Transactional(readOnly = false)
1507
    public UpdateResult moveSynonymToAnotherTaxon(Synonym oldSynonym,
1508
            Taxon newTaxon,
1509
            boolean moveHomotypicGroup,
1510
            SynonymType newSynonymType) throws HomotypicalGroupChangeException {
1511
        return moveSynonymToAnotherTaxon(oldSynonym, newTaxon, moveHomotypicGroup,
1512
                newSynonymType,
1513
                oldSynonym.getSec()!= null? oldSynonym.getSec().getUuid(): null,
1514
                oldSynonym.getSecMicroReference(),
1515
                true);
1516
    }
1517

    
1518
    @Override
1519
    @Transactional(readOnly = false)
1520
    public UpdateResult moveSynonymToAnotherTaxon(Synonym oldSynonym,
1521
            Taxon newTaxon,
1522
            boolean moveHomotypicGroup,
1523
            SynonymType newSynonymType,
1524
            UUID newSecundumUuid,
1525
            String newSecundumDetail,
1526
            boolean keepSecundumIfUndefined) throws HomotypicalGroupChangeException {
1527

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

    
1539
        HomotypicalGroup homotypicGroup = synonymName.getHomotypicalGroup();
1540
        int hgSize = homotypicGroup.getTypifiedNames().size();
1541
        boolean isSingleInGroup = !(hgSize > 1);
1542

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

    
1561
        UpdateResult result = new UpdateResult();
1562
        //move all synonyms to new taxon
1563
        List<Synonym> homotypicSynonyms = oldTaxon.getSynonymsInGroup(homotypicGroup);
1564
        Reference newSecundum = referenceService.load(newSecundumUuid);
1565
        for (Synonym synRelation: homotypicSynonyms){
1566

    
1567
            newTaxon = HibernateProxyHelper.deproxy(newTaxon, Taxon.class);
1568
            oldTaxon = HibernateProxyHelper.deproxy(oldTaxon, Taxon.class);
1569
            oldTaxon.removeSynonym(synRelation, false);
1570
            newTaxon.addSynonym(synRelation, newSynonymType);
1571

    
1572
            if (newSecundum != null || !keepSecundumIfUndefined){
1573
                synRelation.setSec(newSecundum);
1574
            }
1575
            if (newSecundumDetail != null || !keepSecundumIfUndefined){
1576
                synRelation.setSecMicroReference(newSecundumDetail);
1577
            }
1578

    
1579
            //set result  //why is this needed? Seems wrong to me (AM 10.10.2016)
1580
            if (!synRelation.equals(oldSynonym)){
1581
                result.setError();
1582
            }
1583
        }
1584

    
1585
        result.addUpdatedObject(oldTaxon);
1586
        result.addUpdatedObject(newTaxon);
1587
        saveOrUpdate(oldTaxon);
1588
        saveOrUpdate(newTaxon);
1589

    
1590
        return result;
1591
    }
1592

    
1593
    @Override
1594
    public <T extends TaxonBase>List<UuidAndTitleCache<T>> getUuidAndTitleCache(Class<T> clazz, Integer limit, String pattern) {
1595

    
1596
        return dao.getUuidAndTitleCache(clazz, limit, pattern);
1597
    }
1598

    
1599
    @Override
1600
    public Pager<SearchResult<TaxonBase>> findByFullText(
1601
            Class<? extends TaxonBase> clazz, String queryString,
1602
            Classification classification, TaxonNode subtree, boolean includeUnpublished, List<Language> languages,
1603
            boolean highlightFragments, Integer pageSize, Integer pageNumber,
1604
            List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException {
1605

    
1606
        LuceneSearch luceneSearch = prepareFindByFullTextSearch(clazz, queryString, classification, subtree,
1607
                null, includeUnpublished, languages, highlightFragments, null);
1608

    
1609
        // --- execute search
1610
        TopGroups<BytesRef> topDocsResultSet;
1611
        try {
1612
            topDocsResultSet = luceneSearch.executeSearch(pageSize, pageNumber);
1613
        } catch (ParseException e) {
1614
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
1615
            luceneParseException.setStackTrace(e.getStackTrace());
1616
            throw luceneParseException;
1617
        }
1618

    
1619
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
1620
        idFieldMap.put(CdmBaseType.TAXON_BASE, "id");
1621

    
1622
        // ---  initialize taxa, thighlight matches ....
1623
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
1624
        @SuppressWarnings("rawtypes")
1625
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
1626
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
1627

    
1628
        long totalHits = topDocsResultSet != null ? Long.valueOf(topDocsResultSet.totalGroupCount) : 0;
1629
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
1630
    }
1631

    
1632
    @Transactional(readOnly = true)
1633
    @Override
1634
    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) {
1635
         long numberOfResults = dao.countByTitleWithRestrictions(clazz, queryString, matchmode, restrictions);
1636

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

    
1641
             results = dao.findByTitleWithRestrictions(clazz, queryString, matchmode, restrictions, pageSize, pageNumber, orderHints, propertyPaths);
1642
             results.addAll(dao.findByTitleWithRestrictions(clazz, "?".concat(queryString), matchmode, restrictions, pageSize, pageNumber, orderHints, propertyPaths));
1643
         }
1644
         Collections.sort(results, new TaxonComparator());
1645
         return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
1646
    }
1647

    
1648
    @Transactional(readOnly = true)
1649
    @Override
1650
    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) {
1651
        long numberOfResults = dao.countByTitle(clazz, queryString, matchmode, criteria);
1652
        //check whether there are doubtful taxa matching
1653
        long numberOfResults_doubtful = dao.countByTitle(clazz, "?".concat(queryString), matchmode, criteria);
1654
        List<S> results = new ArrayList<>();
1655
        if(numberOfResults > 0 || numberOfResults_doubtful > 0) { // no point checking again //TODO use AbstractPagerImpl.hasResultsInRange(numberOfResults, pageNumber, pageSize)
1656
               if (numberOfResults > 0){
1657
                   results = dao.findByTitle(clazz, queryString, matchmode, criteria, pageSize, pageNumber, orderHints, propertyPaths);
1658
               }else{
1659
                   results = new ArrayList<>();
1660
               }
1661
               if (numberOfResults_doubtful > 0){
1662
                   results.addAll(dao.findByTitle(clazz, "?".concat(queryString), matchmode,  criteria, pageSize, pageNumber, orderHints, propertyPaths));
1663
               }
1664
        }
1665
        Collections.sort(results, new TaxonComparator());
1666
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
1667
    }
1668

    
1669
    @Override
1670
    public Pager<SearchResult<TaxonBase>> findByDistribution(List<NamedArea> areaFilter, List<PresenceAbsenceTerm> statusFilter,
1671
            Classification classification, TaxonNode subtree,
1672
            Integer pageSize, Integer pageNumber,
1673
            List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException {
1674

    
1675
        LuceneSearch luceneSearch = prepareByDistributionSearch(areaFilter, statusFilter, classification, subtree);
1676

    
1677
        // --- execute search
1678
        TopGroups<BytesRef> topDocsResultSet;
1679
        try {
1680
            topDocsResultSet = luceneSearch.executeSearch(pageSize, pageNumber);
1681
        } catch (ParseException e) {
1682
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
1683
            luceneParseException.setStackTrace(e.getStackTrace());
1684
            throw luceneParseException;
1685
        }
1686

    
1687
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
1688
        idFieldMap.put(CdmBaseType.TAXON_BASE, "id");
1689

    
1690
        // ---  initialize taxa, thighlight matches ....
1691
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
1692
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
1693
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
1694

    
1695
        long totalHits = topDocsResultSet != null ? Long.valueOf(topDocsResultSet.totalGroupCount) : 0;
1696
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
1697
    }
1698

    
1699
    /**
1700
     * @param clazz
1701
     * @param queryString
1702
     * @param classification
1703
     * @param includeUnpublished
1704
     * @param languages
1705
     * @param highlightFragments
1706
     * @param sortFields TODO
1707
     * @param directorySelectClass
1708
     * @return
1709
     */
1710
    protected LuceneSearch prepareFindByFullTextSearch(Class<? extends CdmBase> clazz, String queryString,
1711
            Classification classification, TaxonNode subtree, String className, boolean includeUnpublished, List<Language> languages,
1712
            boolean highlightFragments, SortField[] sortFields) {
1713

    
1714
        Builder finalQueryBuilder = new Builder();
1715
        Builder textQueryBuilder = new Builder();
1716

    
1717
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, TaxonBase.class);
1718
        QueryFactory taxonBaseQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(TaxonBase.class);
1719

    
1720
        if(sortFields == null){
1721
            sortFields = new SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING, false)};
1722
        }
1723
        luceneSearch.setSortFields(sortFields);
1724

    
1725
        // ---- search criteria
1726
        luceneSearch.setCdmTypRestriction(clazz);
1727

    
1728
        if(!StringUtils.isEmpty(queryString) && !queryString.equals("*") && !queryString.equals("?") ) {
1729
            textQueryBuilder.add(taxonBaseQueryFactory.newTermQuery("titleCache", queryString), Occur.SHOULD);
1730
            textQueryBuilder.add(taxonBaseQueryFactory.newDefinedTermQuery("name.rank", queryString, languages), Occur.SHOULD);
1731
        }
1732
        if(className != null){
1733
            textQueryBuilder.add(taxonBaseQueryFactory.newTermQuery("classInfo.name", className, false), Occur.MUST);
1734
        }
1735

    
1736
        BooleanQuery textQuery = textQueryBuilder.build();
1737
        if(textQuery.clauses().size() > 0) {
1738
            finalQueryBuilder.add(textQuery, Occur.MUST);
1739
        }
1740

    
1741
        if(classification != null){
1742
            finalQueryBuilder.add(taxonBaseQueryFactory.newEntityIdQuery(AcceptedTaxonBridge.DOC_KEY_CLASSIFICATION_ID, classification), Occur.MUST);
1743
        }
1744
        if(subtree != null){
1745
            finalQueryBuilder.add(taxonBaseQueryFactory.newTermQuery(AcceptedTaxonBridge.DOC_KEY_TREEINDEX, subtree.treeIndexWc(), true), Occur.MUST);
1746
        }
1747
        if(!includeUnpublished)  {
1748
            String accPublishParam = TaxonBase.ACC_TAXON_BRIDGE_PREFIX + AcceptedTaxonBridge.DOC_KEY_PUBLISH_SUFFIX;
1749
            finalQueryBuilder.add(taxonBaseQueryFactory.newBooleanQuery(accPublishParam, true), Occur.MUST);
1750
            finalQueryBuilder.add(taxonBaseQueryFactory.newBooleanQuery("publish", true), Occur.MUST);
1751
        }
1752

    
1753
        luceneSearch.setQuery(finalQueryBuilder.build());
1754

    
1755
        if(highlightFragments){
1756
            luceneSearch.setHighlightFields(taxonBaseQueryFactory.getTextFieldNamesAsArray());
1757
        }
1758
        return luceneSearch;
1759
    }
1760

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

    
1785
        String fromField;
1786
        String queryTermField;
1787
        String toField = "id"; // TaxonBase.uuid
1788
        String publishField;
1789
        String publishFieldInvers;
1790

    
1791
        if(edge.isBidirectional()){
1792
            throw new RuntimeException("Bidirectional joining not supported!");
1793
        }
1794
        if(edge.isEvers()){
1795
            fromField = "relatedFrom.id";
1796
            queryTermField = "relatedFrom.titleCache";
1797
            publishField = "relatedFrom.publish";
1798
            publishFieldInvers = "relatedTo.publish";
1799
        } else if(edge.isInvers()) {
1800
            fromField = "relatedTo.id";
1801
            queryTermField = "relatedTo.titleCache";
1802
            publishField = "relatedTo.publish";
1803
            publishFieldInvers = "relatedFrom.publish";
1804
        } else {
1805
            throw new RuntimeException("Invalid direction: " + edge.getDirections());
1806
        }
1807

    
1808
        Builder finalQueryBuilder = new Builder();
1809

    
1810
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, TaxonBase.class);
1811
        QueryFactory taxonBaseQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(TaxonBase.class);
1812

    
1813
        Builder joinFromQueryBuilder = new Builder();
1814
        if(!StringUtils.isEmpty(queryString)){
1815
            joinFromQueryBuilder.add(taxonBaseQueryFactory.newTermQuery(queryTermField, queryString), Occur.MUST);
1816
        }
1817
        joinFromQueryBuilder.add(taxonBaseQueryFactory.newEntityIdsQuery("type.id", edge.getRelationshipTypes()), Occur.MUST);
1818
        if(!includeUnpublished){
1819
            joinFromQueryBuilder.add(taxonBaseQueryFactory.newBooleanQuery(publishField, true), Occur.MUST);
1820
            joinFromQueryBuilder.add(taxonBaseQueryFactory.newBooleanQuery(publishFieldInvers, true), Occur.MUST);
1821
        }
1822

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

    
1825
        if(sortFields == null){
1826
            sortFields = new  SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING,  false)};
1827
        }
1828
        luceneSearch.setSortFields(sortFields);
1829

    
1830
        finalQueryBuilder.add(joinQuery, Occur.MUST);
1831

    
1832
        if(classification != null){
1833
            finalQueryBuilder.add(taxonBaseQueryFactory.newEntityIdQuery(AcceptedTaxonBridge.DOC_KEY_CLASSIFICATION_ID, classification), Occur.MUST);
1834
        }
1835
        if(subtree != null){
1836
            finalQueryBuilder.add(taxonBaseQueryFactory.newTermQuery(AcceptedTaxonBridge.DOC_KEY_TREEINDEX, subtree.treeIndexWc(), true), Occur.MUST);
1837
        }
1838

    
1839
        luceneSearch.setQuery(finalQueryBuilder.build());
1840

    
1841
        if(highlightFragments){
1842
            luceneSearch.setHighlightFields(taxonBaseQueryFactory.getTextFieldNamesAsArray());
1843
        }
1844
        return luceneSearch;
1845
    }
1846

    
1847
    @Override
1848
    public Pager<SearchResult<TaxonBase>> findTaxaAndNamesByFullText(
1849
            EnumSet<TaxaAndNamesSearchMode> searchModes, String queryString,
1850
            Classification classification, TaxonNode subtree,
1851
            Set<NamedArea> namedAreas, Set<PresenceAbsenceTerm> distributionStatus, List<Language> languages,
1852
            boolean highlightFragments, Integer pageSize,
1853
            Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths)
1854
            throws IOException, LuceneParseException, LuceneMultiSearchException {
1855

    
1856
        // FIXME: allow taxonomic ordering
1857
        //  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";
1858
        // this require building a special sort column by a special classBridge
1859
        if(highlightFragments){
1860
            logger.warn("findTaxaAndNamesByFullText() : fragment highlighting is " +
1861
                    "currently not fully supported by this method and thus " +
1862
                    "may not work with common names and misapplied names.");
1863
        }
1864

    
1865
        // convert sets to lists
1866
        List<NamedArea> namedAreaList = null;
1867
        List<PresenceAbsenceTerm> distributionStatusList = null;
1868
        if(namedAreas != null){
1869
            namedAreaList = new ArrayList<>(namedAreas.size());
1870
            namedAreaList.addAll(namedAreas);
1871
        }
1872
        if(distributionStatus != null){
1873
            distributionStatusList = new ArrayList<>(distributionStatus.size());
1874
            distributionStatusList.addAll(distributionStatus);
1875
        }
1876

    
1877
        // set default if parameter is null
1878
        if(searchModes == null){
1879
            searchModes = EnumSet.of(TaxaAndNamesSearchMode.doTaxa);
1880
        }
1881

    
1882
        // set sort order and thus override any sort orders which may have been
1883
        // defined by prepare*Search methods
1884
        if(orderHints == null){
1885
            orderHints = OrderHint.NOMENCLATURAL_SORT_ORDER.asList();
1886
        }
1887
        SortField[] sortFields = new SortField[orderHints.size()];
1888
        int i = 0;
1889
        for(OrderHint oh : orderHints){
1890
            sortFields[i++] = oh.toSortField();
1891
        }
1892
//        SortField[] sortFields = new SortField[]{SortField.FIELD_SCORE, new SortField("id", SortField.STRING, false)};
1893
//        SortField[] sortFields = new SortField[]{new SortField(NomenclaturalSortOrderBrigde.NAME_SORT_FIELD_NAME, SortField.STRING, false)};
1894

    
1895
        boolean addDistributionFilter = namedAreas != null && namedAreas.size() > 0;
1896

    
1897
        List<LuceneSearch> luceneSearches = new ArrayList<>();
1898
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
1899

    
1900
        /*
1901
          ======== filtering by distribution , HOWTO ========
1902

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

    
1908

    
1909
          3. how does it work in spatial?
1910
          see
1911
           - http://www.nsshutdown.com/projects/lucene/whitepaper/locallucene_v2.html
1912
           - http://www.infoq.com/articles/LuceneSpatialSupport
1913
           - http://www.mhaller.de/archives/156-Spatial-search-with-Lucene.html
1914
          ------------------------------------------------------------------------
1915

    
1916
          filter strategies:
1917
          A) use a separate distribution filter per index sub-query/search:
1918
           - byTaxonSyonym (query TaxaonBase):
1919
               use a join area filter (Distribution -> TaxonBase)
1920
           - byCommonName (query DescriptionElementBase): use an area filter on
1921
               DescriptionElementBase !!! PROBLEM !!!
1922
               This cannot work since the distributions are different entities than the
1923
               common names and thus these are different lucene documents.
1924
           - byMisaplliedNames (join query TaxonRelationship -> TaxonBase):
1925
               use a join area filter (Distribution -> TaxonBase)
1926

    
1927
          B) use a common distribution filter for all index sub-query/searches:
1928
           - use a common join area filter (Distribution -> TaxonBase)
1929
           - also implement the byCommonName as join query (CommonName -> TaxonBase)
1930
           PROBLEM in this case: we are losing the fragment highlighting for the
1931
           common names, since the returned documents are always TaxonBases
1932
        */
1933

    
1934
        /* The QueryFactory for creating filter queries on Distributions should
1935
         * The query factory used for the common names query cannot be reused
1936
         * for this case, since we want to only record the text fields which are
1937
         * actually used in the primary query
1938
         */
1939
        QueryFactory distributionFilterQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(Distribution.class);
1940

    
1941
        Builder multiIndexByAreaFilterBuilder = new Builder();
1942
        boolean includeUnpublished = searchModes.contains(TaxaAndNamesSearchMode.includeUnpublished);
1943

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

    
1976
                //TODO replace by createByDistributionJoinQuery
1977
                BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, distributionFilterQueryFactory);
1978
                Query taxonAreaJoinQuery = distributionFilterQueryFactory.newJoinQuery(Distribution.class, fromField, true, byDistributionQuery, toField, Taxon.class, ScoreMode.None);
1979
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
1980

    
1981
            }
1982
        }
1983

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

    
2009
            idFieldMap.put(CdmBaseType.TAXON_BASE, "id");
2010

    
2011
            luceneSearches.add(byCommonNameSearch);
2012

    
2013
            /* A) does not work!!!!
2014
            luceneSearches.add(
2015
                    prepareByDescriptionElementFullTextSearch(CommonTaxonName.class,
2016
                            queryString, classification, null, languages, highlightFragments)
2017
                        );
2018
            idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
2019
            if(addDistributionFilter){
2020
                // in this case we are able to use DescriptionElementBase documents
2021
                // which are matching the areas in question directly
2022
                BooleanQuery byDistributionQuery = createByDistributionQuery(
2023
                        namedAreaList,
2024
                        distributionStatusList,
2025
                        distributionFilterQueryFactory
2026
                        );
2027
                multiIndexByAreaFilter.add(new QueryWrapperFilter(byDistributionQuery), Occur.SHOULD);
2028
            } */
2029
        }
2030

    
2031

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

    
2049
            luceneSearches.add(prepareFindByTaxonRelationFullTextSearch(
2050
                    new TaxonRelationshipEdge(relTypes, Direction.relatedTo),
2051
                    queryString, classification, subtree, includeUnpublished, languages, highlightFragments, sortFields));
2052
            idFieldMap.put(CdmBaseType.TAXON_BASE, "id");
2053

    
2054
            if(addDistributionFilter){
2055
                String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
2056

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

    
2084
                //TODO replace by createByDistributionJoinQuery
2085
                BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, distributionFilterQueryFactory);
2086
                Query taxonAreaJoinQuery = distributionFilterQueryFactory.newJoinQuery(Distribution.class,
2087
                        fromField, true, byDistributionQuery, toField, null, ScoreMode.None);
2088

    
2089
//                debug code for bug described above
2090
                //does not compile anymore since changing from lucene 3.6.2 to lucene 4.10+
2091
//                DocIdSet filterMatchSet = filter.getDocIdSet(luceneIndexToolProvider.getIndexReaderFor(Taxon.class));
2092
//                System.err.println(DocIdBitSetPrinter.docsAsString(filterMatchSet, 100));
2093

    
2094
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
2095
            }
2096
        }
2097

    
2098
        // search by pro parte synonyms
2099
        if(searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)) {
2100
            //TODO merge with misapplied name search once #7487 is fixed
2101
            Set<TaxonRelationshipType> relTypes = new HashSet<>();
2102
            relTypes.addAll(TaxonRelationshipType.allSynonymTypes());
2103

    
2104
            luceneSearches.add(prepareFindByTaxonRelationFullTextSearch(
2105
                    new TaxonRelationshipEdge(relTypes, Direction.relatedTo),
2106
                    queryString, classification, subtree, includeUnpublished, languages, highlightFragments, sortFields));
2107
            idFieldMap.put(CdmBaseType.TAXON_BASE, "id");
2108

    
2109
            if(addDistributionFilter){
2110
                String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
2111
                String toField = "relation.8a896603-0fa3-44c6-9cd7-df2d8792e577.to.id";
2112
                BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, distributionFilterQueryFactory);
2113
                Query taxonAreaJoinQuery = distributionFilterQueryFactory.newJoinQuery(Distribution.class,
2114
                        fromField, true, byDistributionQuery, toField, null, ScoreMode.None);
2115
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
2116
            }
2117
        }//end pro parte synonyms
2118

    
2119
        LuceneMultiSearch multiSearch = new LuceneMultiSearch(luceneIndexToolProvider,
2120
                luceneSearches.toArray(new LuceneSearch[luceneSearches.size()]));
2121

    
2122
        if(addDistributionFilter){
2123

    
2124
            // B)
2125
            // in this case we need a filter which uses a join query
2126
            // to get the TaxonBase documents for the DescriptionElementBase documents
2127
            // which are matching the areas in question
2128
            //
2129
            // for doTaxa, doByCommonName
2130
            Query taxonAreaJoinQuery = createByDistributionJoinQuery(
2131
                    namedAreaList,
2132
                    distributionStatusList,
2133
                    distributionFilterQueryFactory,
2134
                    Taxon.class, true
2135
                    );
2136
            multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
2137
        }
2138

    
2139
        if (addDistributionFilter){
2140
            multiSearch.setFilter(multiIndexByAreaFilterBuilder.build());
2141
        }
2142

    
2143

    
2144
        // --- execute search
2145
        TopGroups<BytesRef> topDocsResultSet;
2146
        try {
2147
            topDocsResultSet = multiSearch.executeSearch(pageSize, pageNumber);
2148
        } catch (ParseException e) {
2149
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
2150
            luceneParseException.setStackTrace(e.getStackTrace());
2151
            throw luceneParseException;
2152
        }
2153

    
2154
        // --- initialize taxa, highlight matches ....
2155
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(multiSearch, multiSearch.getQuery());
2156

    
2157

    
2158
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
2159
                topDocsResultSet, multiSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
2160

    
2161
        long totalHits = (topDocsResultSet != null) ? Long.valueOf(topDocsResultSet.totalGroupCount) : 0;
2162
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
2163
    }
2164

    
2165
    /**
2166
     * @param namedAreaList at least one area must be in the list
2167
     * @param distributionStatusList optional
2168
     * @param toType toType
2169
     *      Optional parameter. Only used for debugging to print the toType documents
2170
     * @param asFilter TODO
2171
     * @return
2172
     * @throws IOException
2173
     */
2174
    protected Query createByDistributionJoinQuery(
2175
            List<NamedArea> namedAreaList,
2176
            List<PresenceAbsenceTerm> distributionStatusList,
2177
            QueryFactory queryFactory, Class<? extends CdmBase> toType, boolean asFilter
2178
            ) throws IOException {
2179

    
2180
        String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
2181
        String toField = "id"; // id in toType usually this is the TaxonBase index
2182

    
2183
        BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, queryFactory);
2184

    
2185
        ScoreMode scoreMode = asFilter ?  ScoreMode.None : ScoreMode.Max;
2186

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

    
2189
        return taxonAreaJoinQuery;
2190
    }
2191

    
2192
    /**
2193
     * @param namedAreaList
2194
     * @param distributionStatusList
2195
     * @param queryFactory
2196
     * @return
2197
     */
2198
    private BooleanQuery createByDistributionQuery(List<NamedArea> namedAreaList,
2199
            List<PresenceAbsenceTerm> distributionStatusList, QueryFactory queryFactory) {
2200
        Builder areaQueryBuilder = new Builder();
2201
        // area field from Distribution
2202
        areaQueryBuilder.add(queryFactory.newEntityIdsQuery("area.id", namedAreaList), Occur.MUST);
2203

    
2204
        // status field from Distribution
2205
        if(distributionStatusList != null && distributionStatusList.size() > 0){
2206
            areaQueryBuilder.add(queryFactory.newEntityIdsQuery("status.id", distributionStatusList), Occur.MUST);
2207
        }
2208

    
2209
        BooleanQuery areaQuery = areaQueryBuilder.build();
2210
        logger.debug("createByDistributionQuery() query: " + areaQuery.toString());
2211
        return areaQuery;
2212
    }
2213

    
2214
    /**
2215
     * This method has been primarily created for testing the area join query but might
2216
     * also be useful in other situations
2217
     *
2218
     * @param namedAreaList
2219
     * @param distributionStatusList
2220
     * @param classification
2221
     * @param highlightFragments
2222
     * @return
2223
     * @throws IOException
2224
     */
2225
    protected LuceneSearch prepareByDistributionSearch(
2226
            List<NamedArea> namedAreaList, List<PresenceAbsenceTerm> distributionStatusList,
2227
            Classification classification, TaxonNode subtree) throws IOException {
2228

    
2229
        Builder finalQueryBuilder = new Builder();
2230

    
2231
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, Taxon.class);
2232

    
2233
        // FIXME is this query factory using the wrong type?
2234
        QueryFactory taxonQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(Taxon.class);
2235

    
2236
        SortField[] sortFields = new  SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING, false)};
2237
        luceneSearch.setSortFields(sortFields);
2238

    
2239

    
2240
        Query byAreaQuery = createByDistributionJoinQuery(namedAreaList, distributionStatusList, taxonQueryFactory, null, false);
2241

    
2242
        finalQueryBuilder.add(byAreaQuery, Occur.MUST);
2243

    
2244
        if(classification != null){
2245
            finalQueryBuilder.add(taxonQueryFactory.newEntityIdQuery(AcceptedTaxonBridge.DOC_KEY_CLASSIFICATION_ID, classification), Occur.MUST);
2246
        }
2247
        if(subtree != null){
2248
            finalQueryBuilder.add(taxonQueryFactory.newTermQuery(AcceptedTaxonBridge.DOC_KEY_TREEINDEX, subtree.treeIndexWc(), true), Occur.MUST);
2249
        }
2250
        BooleanQuery finalQuery = finalQueryBuilder.build();
2251
        logger.info("prepareByAreaSearch() query: " + finalQuery.toString());
2252
        luceneSearch.setQuery(finalQuery);
2253

    
2254
        return luceneSearch;
2255
    }
2256

    
2257
    @Override
2258
    public Pager<SearchResult<TaxonBase>> findByDescriptionElementFullText(
2259
            Class<? extends DescriptionElementBase> clazz, String queryString,
2260
            Classification classification, TaxonNode subtree, List<Feature> features, List<Language> languages,
2261
            boolean highlightFragments, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException {
2262

    
2263
        LuceneSearch luceneSearch = prepareByDescriptionElementFullTextSearch(clazz, queryString, classification, subtree, features, languages, highlightFragments);
2264

    
2265
        // --- execute search
2266
        TopGroups<BytesRef> topDocsResultSet;
2267
        try {
2268
            topDocsResultSet = luceneSearch.executeSearch(pageSize, pageNumber);
2269
        } catch (ParseException e) {
2270
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
2271
            luceneParseException.setStackTrace(e.getStackTrace());
2272
            throw luceneParseException;
2273
        }
2274

    
2275
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
2276
        idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
2277

    
2278
        // --- initialize taxa, highlight matches ....
2279
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
2280
        @SuppressWarnings("rawtypes")
2281
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
2282
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
2283

    
2284
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
2285
        return new DefaultPagerImpl<>(pageNumber, Long.valueOf(totalHits), pageSize, searchResults);
2286
    }
2287

    
2288
    @Override
2289
    public Pager<SearchResult<TaxonBase>> findByEverythingFullText(String queryString,
2290
            Classification classification, TaxonNode subtree, boolean includeUnpublished, List<Language> languages, boolean highlightFragments,
2291
            Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException, LuceneMultiSearchException {
2292

    
2293
        LuceneSearch luceneSearchByDescriptionElement = prepareByDescriptionElementFullTextSearch(null, queryString,
2294
                classification, subtree,
2295
                null, languages, highlightFragments);
2296
        LuceneSearch luceneSearchByTaxonBase = prepareFindByFullTextSearch(null, queryString, classification, subtree, null,
2297
                includeUnpublished, languages, highlightFragments, null);
2298

    
2299
        LuceneMultiSearch multiSearch = new LuceneMultiSearch(luceneIndexToolProvider, luceneSearchByDescriptionElement, luceneSearchByTaxonBase);
2300

    
2301
        // --- execute search
2302
        TopGroups<BytesRef> topDocsResultSet;
2303
        try {
2304
            topDocsResultSet = multiSearch.executeSearch(pageSize, pageNumber);
2305
        } catch (ParseException e) {
2306
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
2307
            luceneParseException.setStackTrace(e.getStackTrace());
2308
            throw luceneParseException;
2309
        }
2310

    
2311
        // --- initialize taxa, highlight matches ....
2312
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(multiSearch, multiSearch.getQuery());
2313

    
2314
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
2315
        idFieldMap.put(CdmBaseType.TAXON_BASE, "id");
2316
        idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
2317

    
2318
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
2319
                topDocsResultSet, multiSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
2320

    
2321
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
2322
        return new DefaultPagerImpl<>(pageNumber, Long.valueOf(totalHits), pageSize, searchResults);
2323
    }
2324

    
2325
    /**
2326
     * @param clazz
2327
     * @param queryString
2328
     * @param classification
2329
     * @param features
2330
     * @param languages
2331
     * @param highlightFragments
2332
     * @param directorySelectClass
2333
     * @return
2334
     */
2335
    protected LuceneSearch prepareByDescriptionElementFullTextSearch(Class<? extends CdmBase> clazz,
2336
            String queryString, Classification classification, TaxonNode subtree, List<Feature> features,
2337
            List<Language> languages, boolean highlightFragments) {
2338

    
2339
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, DescriptionElementBase.class);
2340
        QueryFactory descriptionElementQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(DescriptionElementBase.class);
2341

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

    
2344
        BooleanQuery finalQuery = createByDescriptionElementFullTextQuery(queryString, classification, subtree, features,
2345
                languages, descriptionElementQueryFactory);
2346

    
2347
        luceneSearch.setSortFields(sortFields);
2348
        luceneSearch.setCdmTypRestriction(clazz);
2349
        luceneSearch.setQuery(finalQuery);
2350
        if(highlightFragments){
2351
            luceneSearch.setHighlightFields(descriptionElementQueryFactory.getTextFieldNamesAsArray());
2352
        }
2353

    
2354
        return luceneSearch;
2355
    }
2356

    
2357
    /**
2358
     * @param queryString
2359
     * @param classification
2360
     * @param features
2361
     * @param languages
2362
     * @param descriptionElementQueryFactory
2363
     * @return
2364
     */
2365
    private BooleanQuery createByDescriptionElementFullTextQuery(String queryString,
2366
            Classification classification, TaxonNode subtree, List<Feature> features,
2367
            List<Language> languages, QueryFactory descriptionElementQueryFactory) {
2368

    
2369
        Builder finalQueryBuilder = new Builder();
2370
        Builder textQueryBuilder = new Builder();
2371

    
2372
        if(!StringUtils.isEmpty(queryString)){
2373

    
2374
            textQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("titleCache", queryString), Occur.SHOULD);
2375

    
2376
            // common name
2377
            Builder nameQueryBuilder = new Builder();
2378
            if(languages == null || languages.size() == 0){
2379
                nameQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("name", queryString), Occur.MUST);
2380
            } else {
2381
                Builder languageSubQueryBuilder = new Builder();
2382
                for(Language lang : languages){
2383
                    languageSubQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("language.uuid",  lang.getUuid().toString(), false), Occur.SHOULD);
2384
                }
2385
                nameQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("name", queryString), Occur.MUST);
2386
                nameQueryBuilder.add(languageSubQueryBuilder.build(), Occur.MUST);
2387
            }
2388
            textQueryBuilder.add(nameQueryBuilder.build(), Occur.SHOULD);
2389

    
2390

    
2391
            // text field from TextData
2392
            textQueryBuilder.add(descriptionElementQueryFactory.newMultilanguageTextQuery("text", queryString, languages), Occur.SHOULD);
2393

    
2394
            // --- TermBase fields - by representation ----
2395
            // state field from CategoricalData
2396
            textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("stateData.state", queryString, languages), Occur.SHOULD);
2397

    
2398
            // state field from CategoricalData
2399
            textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("stateData.modifyingText", queryString, languages), Occur.SHOULD);
2400

    
2401
            // area field from Distribution
2402
            textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("area", queryString, languages), Occur.SHOULD);
2403

    
2404
            // status field from Distribution
2405
            textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("status", queryString, languages), Occur.SHOULD);
2406

    
2407
            finalQueryBuilder.add(textQueryBuilder.build(), Occur.MUST);
2408

    
2409
        }
2410
        // --- classification ----
2411

    
2412

    
2413
        if(classification != null){
2414
            finalQueryBuilder.add(descriptionElementQueryFactory.newEntityIdQuery("inDescription.taxon.taxonNodes.classification.id", classification), Occur.MUST);
2415
        }
2416
        if(subtree != null){
2417
            finalQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("inDescription.taxon.taxonNodes.treeIndex", subtree.treeIndexWc(), true), Occur.MUST);
2418
        }
2419

    
2420
        // --- IdentifieableEntity fields - by uuid
2421
        if(features != null && features.size() > 0 ){
2422
            finalQueryBuilder.add(descriptionElementQueryFactory.newEntityUuidsQuery("feature.uuid", features), Occur.MUST);
2423
        }
2424

    
2425
        // the description must be associated with a taxon
2426
        finalQueryBuilder.add(descriptionElementQueryFactory.newIsNotNullQuery("inDescription.taxon.id"), Occur.MUST);
2427

    
2428
        BooleanQuery finalQuery = finalQueryBuilder.build();
2429
        logger.info("prepareByDescriptionElementFullTextSearch() query: " + finalQuery.toString());
2430
        return finalQuery;
2431
    }
2432

    
2433
    @Override
2434
    public List<Synonym> createInferredSynonyms(Taxon taxon, Classification classification, SynonymType type, boolean doWithMisappliedNames){
2435

    
2436
        List <Synonym> inferredSynonyms = new ArrayList<>();
2437
        List<Synonym> inferredSynonymsToBeRemoved = new ArrayList<>();
2438

    
2439
        Map <UUID, IZoologicalName> zooHashMap = new HashMap<>();
2440
        boolean includeUnpublished = INCLUDE_UNPUBLISHED;
2441

    
2442
        UUID nameUuid= taxon.getName().getUuid();
2443
        IZoologicalName taxonName = getZoologicalName(nameUuid, zooHashMap);
2444
        String epithetOfTaxon = null;
2445
        String infragenericEpithetOfTaxon = null;
2446
        String infraspecificEpithetOfTaxon = null;
2447
        if (taxonName.isSpecies()){
2448
             epithetOfTaxon= taxonName.getSpecificEpithet();
2449
        } else if (taxonName.isInfraGeneric()){
2450
            infragenericEpithetOfTaxon = taxonName.getInfraGenericEpithet();
2451
        } else if (taxonName.isInfraSpecific()){
2452
            infraspecificEpithetOfTaxon = taxonName.getInfraSpecificEpithet();
2453
        }
2454
        String genusOfTaxon = taxonName.getGenusOrUninomial();
2455
        Set<TaxonNode> nodes = taxon.getTaxonNodes();
2456
        List<String> taxonNames = new ArrayList<>();
2457

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

    
2462
            if (node.getClassification().equals(classification)){
2463
                if (!node.isTopmostNode()){
2464
                    TaxonNode parent = node.getParent();
2465
                    parent = CdmBase.deproxy(parent);
2466
                    TaxonName parentName =  parent.getTaxon().getName();
2467
                    IZoologicalName zooParentName = CdmBase.deproxy(parentName);
2468
                    Taxon parentTaxon = CdmBase.deproxy(parent.getTaxon());
2469

    
2470
                    //create inferred synonyms for species, subspecies
2471
                    if ((parentName.isGenus() || parentName.isSpecies() || parentName.getRank().equals(Rank.SUBGENUS())) ){
2472

    
2473
                        Synonym inferredEpithet = null;
2474
                        Synonym inferredGenus = null;
2475
                        Synonym potentialCombination = null;
2476

    
2477
                        List<String> propertyPaths = new ArrayList<>();
2478
                        propertyPaths.add("synonym");
2479
                        propertyPaths.add("synonym.name");
2480
                        List<OrderHint> orderHintsSynonyms = new ArrayList<>();
2481
                        orderHintsSynonyms.add(new OrderHint("titleCache", SortOrder.ASCENDING));
2482

    
2483
                        List<Synonym> synonyMsOfParent = dao.getSynonyms(parentTaxon, SynonymType.HETEROTYPIC_SYNONYM_OF(), null, null,orderHintsSynonyms,propertyPaths);
2484
                        List<Synonym> synonymsOfTaxon= dao.getSynonyms(taxon, SynonymType.HETEROTYPIC_SYNONYM_OF(),
2485
                                null, null,orderHintsSynonyms,propertyPaths);
2486

    
2487
                        List<TaxonRelationship> taxonRelListParent = new ArrayList<>();
2488
                        List<TaxonRelationship> taxonRelListTaxon = new ArrayList<>();
2489
                        if (doWithMisappliedNames){
2490
                            List<OrderHint> orderHintsMisapplied = new ArrayList<>();
2491
                            orderHintsMisapplied.add(new OrderHint("relatedFrom.titleCache", SortOrder.ASCENDING));
2492
                            taxonRelListParent = dao.getTaxonRelationships(parentTaxon, TaxonRelationshipType.MISAPPLIED_NAME_FOR(),
2493
                                    includeUnpublished, null, null, orderHintsMisapplied, propertyPaths, Direction.relatedTo);
2494
                            taxonRelListTaxon = dao.getTaxonRelationships(taxon, TaxonRelationshipType.MISAPPLIED_NAME_FOR(),
2495
                                    includeUnpublished, null, null, orderHintsMisapplied, propertyPaths, Direction.relatedTo);
2496
                        }
2497

    
2498
                        if (type.equals(SynonymType.INFERRED_EPITHET_OF())){
2499
                            for (Synonym synonymRelationOfParent:synonyMsOfParent){
2500

    
2501
                                inferredEpithet = createInferredEpithets(taxon,
2502
                                        zooHashMap, taxonName, epithetOfTaxon,
2503
                                        infragenericEpithetOfTaxon,
2504
                                        infraspecificEpithetOfTaxon,
2505
                                        taxonNames, parentName,
2506
                                        synonymRelationOfParent);
2507

    
2508
                                inferredSynonyms.add(inferredEpithet);
2509
                                zooHashMap.put(inferredEpithet.getName().getUuid(), inferredEpithet.getName());
2510
                                taxonNames.add(inferredEpithet.getName().getNameCache());
2511
                            }
2512

    
2513
                            if (doWithMisappliedNames){
2514

    
2515
                                for (TaxonRelationship taxonRelationship: taxonRelListParent){
2516
                                     Taxon misappliedName = taxonRelationship.getFromTaxon();
2517

    
2518
                                     inferredEpithet = createInferredEpithets(taxon,
2519
                                             zooHashMap, taxonName, epithetOfTaxon,
2520
                                             infragenericEpithetOfTaxon,
2521
                                             infraspecificEpithetOfTaxon,
2522
                                             taxonNames, parentName,
2523
                                             misappliedName);
2524

    
2525
                                    inferredSynonyms.add(inferredEpithet);
2526
                                    zooHashMap.put(inferredEpithet.getName().getUuid(), inferredEpithet.getName());
2527
                                    taxonNames.add(inferredEpithet.getName().getNameCache());
2528
                                }
2529
                            }
2530

    
2531
                            if (!taxonNames.isEmpty()){
2532
                                List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2533
                                if (!synNotInCDM.isEmpty()){
2534
                                    inferredSynonymsToBeRemoved.clear();
2535

    
2536
                                    for (Synonym syn :inferredSynonyms){
2537
                                        IZoologicalName name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2538
                                        if (!synNotInCDM.contains(name.getNameCache())){
2539
                                            inferredSynonymsToBeRemoved.add(syn);
2540
                                        }
2541
                                    }
2542

    
2543
                                    // Remove identified Synonyms from inferredSynonyms
2544
                                    for (Synonym synonym : inferredSynonymsToBeRemoved) {
2545
                                        inferredSynonyms.remove(synonym);
2546
                                    }
2547
                                }
2548
                            }
2549

    
2550
                        }else if (type.equals(SynonymType.INFERRED_GENUS_OF())){
2551

    
2552
                            for (Synonym synonymRelationOfTaxon:synonymsOfTaxon){
2553

    
2554
                                inferredGenus = createInferredGenus(taxon,
2555
                                        zooHashMap, taxonName, epithetOfTaxon,
2556
                                        genusOfTaxon, taxonNames, zooParentName, synonymRelationOfTaxon);
2557

    
2558
                                inferredSynonyms.add(inferredGenus);
2559
                                zooHashMap.put(inferredGenus.getName().getUuid(), inferredGenus.getName());
2560
                                taxonNames.add(inferredGenus.getName().getNameCache());
2561
                            }
2562

    
2563
                            if (doWithMisappliedNames){
2564

    
2565
                                for (TaxonRelationship taxonRelationship: taxonRelListTaxon){
2566
                                    Taxon misappliedName = taxonRelationship.getFromTaxon();
2567
                                    inferredGenus = createInferredGenus(taxon, zooHashMap, taxonName, infraspecificEpithetOfTaxon, genusOfTaxon, taxonNames, zooParentName,  misappliedName);
2568

    
2569
                                    inferredSynonyms.add(inferredGenus);
2570
                                    zooHashMap.put(inferredGenus.getName().getUuid(), inferredGenus.getName());
2571
                                     taxonNames.add(inferredGenus.getName().getNameCache());
2572
                                }
2573
                            }
2574

    
2575

    
2576
                            if (!taxonNames.isEmpty()){
2577
                                List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2578
                                IZoologicalName name;
2579
                                if (!synNotInCDM.isEmpty()){
2580
                                    inferredSynonymsToBeRemoved.clear();
2581

    
2582
                                    for (Synonym syn :inferredSynonyms){
2583
                                        name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2584
                                        if (!synNotInCDM.contains(name.getNameCache())){
2585
                                            inferredSynonymsToBeRemoved.add(syn);
2586
                                        }
2587
                                    }
2588

    
2589
                                    // Remove identified Synonyms from inferredSynonyms
2590
                                    for (Synonym synonym : inferredSynonymsToBeRemoved) {
2591
                                        inferredSynonyms.remove(synonym);
2592
                                    }
2593
                                }
2594
                            }
2595

    
2596
                        }else if (type.equals(SynonymType.POTENTIAL_COMBINATION_OF())){
2597

    
2598
                            Reference sourceReference = null; // TODO: Determination of sourceReference is redundant
2599
                            //for all synonyms of the parent...
2600
                            for (Synonym synonymRelationOfParent:synonyMsOfParent){
2601
                                TaxonName synName;
2602
                                HibernateProxyHelper.deproxy(synonymRelationOfParent);
2603

    
2604
                                synName = synonymRelationOfParent.getName();
2605

    
2606
                                // Set the sourceReference
2607
                                sourceReference = synonymRelationOfParent.getSec();
2608

    
2609
                                // Determine the idInSource
2610
                                String idInSourceParent = getIdInSource(synonymRelationOfParent);
2611

    
2612
                                IZoologicalName parentSynZooName = getZoologicalName(synName.getUuid(), zooHashMap);
2613
                                String synParentGenus = parentSynZooName.getGenusOrUninomial();
2614
                                String synParentInfragenericName = null;
2615
                                String synParentSpecificEpithet = null;
2616

    
2617
                                if (parentSynZooName.isInfraGeneric()){
2618
                                    synParentInfragenericName = parentSynZooName.getInfraGenericEpithet();
2619
                                }
2620
                                if (parentSynZooName.isSpecies()){
2621
                                    synParentSpecificEpithet = parentSynZooName.getSpecificEpithet();
2622
                                }
2623

    
2624
                               /* if (synGenusName != null && !synonymsGenus.containsKey(synGenusName)){
2625
                                    synonymsGenus.put(synGenusName, idInSource);
2626
                                }*/
2627

    
2628
                                //for all synonyms of the taxon
2629

    
2630
                                for (Synonym synonymRelationOfTaxon:synonymsOfTaxon){
2631

    
2632
                                    IZoologicalName zooSynName = getZoologicalName(synonymRelationOfTaxon.getName().getUuid(), zooHashMap);
2633
                                    potentialCombination = createPotentialCombination(idInSourceParent, parentSynZooName, zooSynName,
2634
                                            synParentGenus,
2635
                                            synParentInfragenericName,
2636
                                            synParentSpecificEpithet, synonymRelationOfTaxon, zooHashMap);
2637

    
2638
                                    taxon.addSynonym(potentialCombination, SynonymType.POTENTIAL_COMBINATION_OF());
2639
                                    inferredSynonyms.add(potentialCombination);
2640
                                    zooHashMap.put(potentialCombination.getName().getUuid(), potentialCombination.getName());
2641
                                     taxonNames.add(potentialCombination.getName().getNameCache());
2642
                                }
2643
                            }
2644

    
2645
                            if (doWithMisappliedNames){
2646

    
2647
                                for (TaxonRelationship parentRelationship: taxonRelListParent){
2648

    
2649
                                    TaxonName misappliedParentName;
2650

    
2651
                                    Taxon misappliedParent = parentRelationship.getFromTaxon();
2652
                                    misappliedParentName = misappliedParent.getName();
2653

    
2654
                                    HibernateProxyHelper.deproxy(misappliedParent);
2655

    
2656
                                    // Set the sourceReference
2657
                                    sourceReference = misappliedParent.getSec();
2658

    
2659
                                    // Determine the idInSource
2660
                                    String idInSourceParent = getIdInSource(misappliedParent);
2661

    
2662
                                    IZoologicalName parentSynZooName = getZoologicalName(misappliedParentName.getUuid(), zooHashMap);
2663
                                    String synParentGenus = parentSynZooName.getGenusOrUninomial();
2664
                                    String synParentInfragenericName = null;
2665
                                    String synParentSpecificEpithet = null;
2666

    
2667
                                    if (parentSynZooName.isInfraGeneric()){
2668
                                        synParentInfragenericName = parentSynZooName.getInfraGenericEpithet();
2669
                                    }
2670
                                    if (parentSynZooName.isSpecies()){
2671
                                        synParentSpecificEpithet = parentSynZooName.getSpecificEpithet();
2672
                                    }
2673

    
2674
                                    for (TaxonRelationship taxonRelationship: taxonRelListTaxon){
2675
                                        Taxon misappliedName = taxonRelationship.getFromTaxon();
2676
                                        IZoologicalName zooMisappliedName = getZoologicalName(misappliedName.getName().getUuid(), zooHashMap);
2677
                                        potentialCombination = createPotentialCombination(
2678
                                                idInSourceParent, parentSynZooName, zooMisappliedName,
2679
                                                synParentGenus,
2680
                                                synParentInfragenericName,
2681
                                                synParentSpecificEpithet, misappliedName, zooHashMap);
2682

    
2683
                                        taxon.addSynonym(potentialCombination, SynonymType.POTENTIAL_COMBINATION_OF());
2684
                                        inferredSynonyms.add(potentialCombination);
2685
                                        zooHashMap.put(potentialCombination.getName().getUuid(), potentialCombination.getName());
2686
                                         taxonNames.add(potentialCombination.getName().getNameCache());
2687
                                    }
2688
                                }
2689
                            }
2690

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

    
2721
        return inferredSynonyms;
2722
    }
2723

    
2724
    private Synonym createPotentialCombination(String idInSourceParent,
2725
            IZoologicalName parentSynZooName, 	IZoologicalName zooSynName, String synParentGenus,
2726
            String synParentInfragenericName, String synParentSpecificEpithet,
2727
            TaxonBase<?> syn, Map<UUID, IZoologicalName> zooHashMap) {
2728
        Synonym potentialCombination;
2729
        Reference sourceReference;
2730
        IZoologicalName inferredSynName;
2731
        HibernateProxyHelper.deproxy(syn);
2732

    
2733
        // Set sourceReference
2734
        sourceReference = syn.getSec();
2735
        if (sourceReference == null){
2736
            logger.warn("The synonym has no sec reference because it is a misapplied name! Take the sec reference of taxon");
2737
            //TODO:Remove
2738
            if (!parentSynZooName.getTaxa().isEmpty()){
2739
                TaxonBase<?> taxon = parentSynZooName.getTaxa().iterator().next();
2740

    
2741
                sourceReference = taxon.getSec();
2742
            }
2743
        }
2744
        String synTaxonSpecificEpithet = zooSynName.getSpecificEpithet();
2745

    
2746
        String synTaxonInfraSpecificName= null;
2747

    
2748
        if (parentSynZooName.isSpecies()){
2749
            synTaxonInfraSpecificName = zooSynName.getInfraSpecificEpithet();
2750
        }
2751

    
2752
        /*if (epithetName != null && !synonymsEpithet.contains(epithetName)){
2753
            synonymsEpithet.add(epithetName);
2754
        }*/
2755

    
2756
        //create potential combinations...
2757
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(syn.getName().getRank());
2758

    
2759
        inferredSynName.setGenusOrUninomial(synParentGenus);
2760
        if (zooSynName.isSpecies()){
2761
              inferredSynName.setSpecificEpithet(synTaxonSpecificEpithet);
2762
              if (parentSynZooName.isInfraGeneric()){
2763
                  inferredSynName.setInfraGenericEpithet(synParentInfragenericName);
2764
              }
2765
        }
2766
        if (zooSynName.isInfraSpecific()){
2767
            inferredSynName.setSpecificEpithet(synParentSpecificEpithet);
2768
            inferredSynName.setInfraSpecificEpithet(synTaxonInfraSpecificName);
2769
        }
2770
        if (parentSynZooName.isInfraGeneric()){
2771
            inferredSynName.setInfraGenericEpithet(synParentInfragenericName);
2772
        }
2773

    
2774
        potentialCombination = Synonym.NewInstance(inferredSynName, null);
2775

    
2776
        // Set the sourceReference
2777
        potentialCombination.setSec(sourceReference);
2778

    
2779

    
2780
        // Determine the idInSource
2781
        String idInSourceSyn= getIdInSource(syn);
2782

    
2783
        if (idInSourceParent != null && idInSourceSyn != null) {
2784
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation, idInSourceSyn + "; " + idInSourceParent, POTENTIAL_COMBINATION_NAMESPACE, sourceReference, null);
2785
            inferredSynName.addSource(originalSource);
2786
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation, idInSourceSyn + "; " + idInSourceParent, POTENTIAL_COMBINATION_NAMESPACE, sourceReference, null);
2787
            potentialCombination.addSource(originalSource);
2788
        }
2789

    
2790
        return potentialCombination;
2791
    }
2792

    
2793
    private Synonym createInferredGenus(Taxon taxon,
2794
            Map<UUID, IZoologicalName> zooHashMap, IZoologicalName taxonName,
2795
            String epithetOfTaxon, String genusOfTaxon,
2796
            List<String> taxonNames, IZoologicalName zooParentName,
2797
            TaxonBase syn) {
2798

    
2799
        Synonym inferredGenus;
2800
        TaxonName synName;
2801
        IZoologicalName inferredSynName;
2802
        synName =syn.getName();
2803
        HibernateProxyHelper.deproxy(syn);
2804

    
2805
        // Determine the idInSource
2806
        String idInSourceSyn = getIdInSource(syn);
2807
        String idInSourceTaxon = getIdInSource(taxon);
2808
        // Determine the sourceReference
2809
        Reference sourceReference = syn.getSec();
2810

    
2811
        //logger.warn(sourceReference.getTitleCache());
2812

    
2813
        synName = syn.getName();
2814
        IZoologicalName synZooName = getZoologicalName(synName.getUuid(), zooHashMap);
2815
        String synSpeciesEpithetName = synZooName.getSpecificEpithet();
2816
         /* if (synonymsEpithet != null && !synonymsEpithet.contains(synSpeciesEpithetName)){
2817
            synonymsEpithet.add(synSpeciesEpithetName);
2818
        }*/
2819

    
2820
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(taxon.getName().getRank());
2821
        //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...
2822

    
2823
        inferredSynName.setGenusOrUninomial(genusOfTaxon);
2824
        if (zooParentName.isInfraGeneric()){
2825
            inferredSynName.setInfraGenericEpithet(zooParentName.getInfraGenericEpithet());
2826
        }
2827

    
2828
        if (taxonName.isSpecies()){
2829
            inferredSynName.setSpecificEpithet(synSpeciesEpithetName);
2830
        }
2831
        if (taxonName.isInfraSpecific()){
2832
            inferredSynName.setSpecificEpithet(epithetOfTaxon);
2833
            inferredSynName.setInfraSpecificEpithet(synZooName.getInfraGenericEpithet());
2834
        }
2835

    
2836
        inferredGenus = Synonym.NewInstance(inferredSynName, null);
2837

    
2838
        // Set the sourceReference
2839
        inferredGenus.setSec(sourceReference);
2840

    
2841
        // Add the original source
2842
        if (idInSourceSyn != null && idInSourceTaxon != null) {
2843
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2844
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2845
            inferredGenus.addSource(originalSource);
2846

    
2847
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2848
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2849
            inferredSynName.addSource(originalSource);
2850
            originalSource = null;
2851

    
2852
        }else{
2853
            logger.error("There is an idInSource missing: " + idInSourceSyn + " of Synonym or " + idInSourceTaxon + " of Taxon");
2854
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2855
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2856
            inferredGenus.addSource(originalSource);
2857

    
2858
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2859
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2860
            inferredSynName.addSource(originalSource);
2861
            originalSource = null;
2862
        }
2863

    
2864
        taxon.addSynonym(inferredGenus, SynonymType.INFERRED_GENUS_OF());
2865

    
2866
        return inferredGenus;
2867
    }
2868

    
2869
    private Synonym createInferredEpithets(Taxon taxon,
2870
            Map<UUID, IZoologicalName> zooHashMap, IZoologicalName taxonName,
2871
            String epithetOfTaxon, String infragenericEpithetOfTaxon,
2872
            String infraspecificEpithetOfTaxon, List<String> taxonNames,
2873
            TaxonName parentName, TaxonBase<?> syn) {
2874

    
2875
        Synonym inferredEpithet;
2876
        TaxonName synName;
2877
        IZoologicalName inferredSynName;
2878
        HibernateProxyHelper.deproxy(syn);
2879

    
2880
        // Determine the idInSource
2881
        String idInSourceSyn = getIdInSource(syn);
2882
        String idInSourceTaxon =  getIdInSource(taxon);
2883
        // Determine the sourceReference
2884
        Reference sourceReference = syn.getSec();
2885

    
2886
        if (sourceReference == null){
2887
             logger.warn("The synonym has no sec reference because it is a misapplied name! Take the sec reference of taxon" + taxon.getSec());
2888
             sourceReference = taxon.getSec();
2889
        }
2890

    
2891
        synName = syn.getName();
2892
        IZoologicalName zooSynName = getZoologicalName(synName.getUuid(), zooHashMap);
2893
        String synGenusName = zooSynName.getGenusOrUninomial();
2894
        String synInfraGenericEpithet = null;
2895
        String synSpecificEpithet = null;
2896

    
2897
        if (zooSynName.getInfraGenericEpithet() != null){
2898
            synInfraGenericEpithet = zooSynName.getInfraGenericEpithet();
2899
        }
2900

    
2901
        if (zooSynName.isInfraSpecific()){
2902
            synSpecificEpithet = zooSynName.getSpecificEpithet();
2903
        }
2904

    
2905
           /* if (synGenusName != null && !synonymsGenus.containsKey(synGenusName)){
2906
            synonymsGenus.put(synGenusName, idInSource);
2907
        }*/
2908

    
2909
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(taxon.getName().getRank());
2910

    
2911
        // DEBUG TODO: for subgenus or subspecies the infrageneric or infraspecific epithet should be used!!!
2912
        if (epithetOfTaxon == null && infragenericEpithetOfTaxon == null && infraspecificEpithetOfTaxon == null) {
2913
            logger.error("This specificEpithet is NULL" + taxon.getTitleCache());
2914
        }
2915
        inferredSynName.setGenusOrUninomial(synGenusName);
2916

    
2917
        if (parentName.isInfraGeneric()){
2918
            inferredSynName.setInfraGenericEpithet(synInfraGenericEpithet);
2919
        }
2920
        if (taxonName.isSpecies()){
2921
            inferredSynName.setSpecificEpithet(epithetOfTaxon);
2922
        }else if (taxonName.isInfraSpecific()){
2923
            inferredSynName.setSpecificEpithet(synSpecificEpithet);
2924
            inferredSynName.setInfraSpecificEpithet(infraspecificEpithetOfTaxon);
2925
        }
2926

    
2927
        inferredEpithet = Synonym.NewInstance(inferredSynName, null);
2928

    
2929
        // Set the sourceReference
2930
        inferredEpithet.setSec(sourceReference);
2931

    
2932
        /* Add the original source
2933
        if (idInSource != null) {
2934
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(idInSource, "InferredEpithetOf", syn.getSec(), null);
2935

    
2936
            // Add the citation
2937
            Reference citation = getCitation(syn);
2938
            if (citation != null) {
2939
                originalSource.setCitation(citation);
2940
                inferredEpithet.addSource(originalSource);
2941
            }
2942
        }*/
2943
        String taxonId = idInSourceTaxon+ "; " + idInSourceSyn;
2944

    
2945

    
2946
        IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2947
                taxonId, INFERRED_EPITHET_NAMESPACE, sourceReference, null);
2948

    
2949
        inferredEpithet.addSource(originalSource);
2950

    
2951
        originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2952
                taxonId, INFERRED_EPITHET_NAMESPACE, sourceReference, null);
2953

    
2954
        inferredSynName.addSource(originalSource);
2955

    
2956
        taxon.addSynonym(inferredEpithet, SynonymType.INFERRED_EPITHET_OF());
2957

    
2958
        return inferredEpithet;
2959
    }
2960

    
2961
    /**
2962
     * Returns an existing IZoologicalName or extends an internal hashmap if it does not exist.
2963
     * Very likely only useful for createInferredSynonyms().
2964
     * @param uuid
2965
     * @param zooHashMap
2966
     * @return
2967
     */
2968
    private IZoologicalName getZoologicalName(UUID uuid, Map <UUID, IZoologicalName> zooHashMap) {
2969
        IZoologicalName taxonName =nameDao.findZoologicalNameByUUID(uuid);
2970
        if (taxonName == null) {
2971
            taxonName = zooHashMap.get(uuid);
2972
        }
2973
        return taxonName;
2974
    }
2975

    
2976
    /**
2977
     * Returns the idInSource for a given Synonym.
2978
     * @param syn
2979
     */
2980
    private String getIdInSource(TaxonBase<?> taxonBase) {
2981
        String idInSource = null;
2982
        Set<IdentifiableSource> sources = taxonBase.getSources();
2983
        if (sources.size() == 1) {
2984
            IdentifiableSource source = sources.iterator().next();
2985
            if (source != null) {
2986
                idInSource  = source.getIdInSource();
2987
            }
2988
        } else if (sources.size() > 1) {
2989
            int count = 1;
2990
            idInSource = "";
2991
            for (IdentifiableSource source : sources) {
2992
                idInSource += source.getIdInSource();
2993
                if (count < sources.size()) {
2994
                    idInSource += "; ";
2995
                }
2996
                count++;
2997
            }
2998
        } else if (sources.size() == 0){
2999
            logger.warn("No idInSource for TaxonBase " + taxonBase.getUuid() + " - " + taxonBase.getTitleCache());
3000
        }
3001

    
3002
        return idInSource;
3003
    }
3004

    
3005
    /**
3006
     * Returns the citation for a given Synonym.
3007
     * @param syn
3008
     */
3009
    private Reference getCitation(Synonym syn) {
3010
        Reference citation = null;
3011
        Set<IdentifiableSource> sources = syn.getSources();
3012
        if (sources.size() == 1) {
3013
            IdentifiableSource source = sources.iterator().next();
3014
            if (source != null) {
3015
                citation = source.getCitation();
3016
            }
3017
        } else if (sources.size() > 1) {
3018
            logger.warn("This Synonym has more than one source: " + syn.getUuid() + " (" + syn.getTitleCache() +")");
3019
        }
3020

    
3021
        return citation;
3022
    }
3023

    
3024
    @Override
3025
    public List<Synonym>  createAllInferredSynonyms(Taxon taxon, Classification tree, boolean doWithMisappliedNames){
3026
        List <Synonym> inferredSynonyms = new ArrayList<>();
3027

    
3028
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.INFERRED_EPITHET_OF(), doWithMisappliedNames));
3029
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.INFERRED_GENUS_OF(), doWithMisappliedNames));
3030
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.POTENTIAL_COMBINATION_OF(), doWithMisappliedNames));
3031

    
3032
        return inferredSynonyms;
3033
    }
3034

    
3035
    @Override
3036
    public List<Classification> listClassifications(TaxonBase taxonBase, Integer limit, Integer start, List<String> propertyPaths) {
3037

    
3038
        // TODO quickly implemented, create according dao !!!!
3039
        Set<TaxonNode> nodes = new HashSet<>();
3040
        Set<Classification> classifications = new HashSet<>();
3041
        List<Classification> list = new ArrayList<>();
3042

    
3043
        if (taxonBase == null) {
3044
            return list;
3045
        }
3046

    
3047
        taxonBase = load(taxonBase.getUuid());
3048

    
3049
        if (taxonBase instanceof Taxon) {
3050
            nodes.addAll(((Taxon)taxonBase).getTaxonNodes());
3051
        } else {
3052
            Taxon taxon = ((Synonym)taxonBase).getAcceptedTaxon();
3053
            if (taxon != null){
3054
                nodes.addAll(taxon.getTaxonNodes());
3055
            }
3056
        }
3057
        for (TaxonNode node : nodes) {
3058
            classifications.add(node.getClassification());
3059
        }
3060
        list.addAll(classifications);
3061
        return list;
3062
    }
3063

    
3064
    @Override
3065
    @Transactional(readOnly = false)
3066
    public UpdateResult changeRelatedTaxonToSynonym(UUID fromTaxonUuid,
3067
            UUID toTaxonUuid,
3068
            TaxonRelationshipType oldRelationshipType,
3069
            SynonymType synonymType) throws DataChangeNoRollbackException {
3070
        UpdateResult result = new UpdateResult();
3071
        Taxon fromTaxon = (Taxon) dao.load(fromTaxonUuid);
3072
        Taxon toTaxon = (Taxon) dao.load(toTaxonUuid);
3073
        result = changeRelatedTaxonToSynonym(fromTaxon, toTaxon, oldRelationshipType, synonymType);
3074

    
3075
        result.addUpdatedObject(toTaxon);
3076
        result.addUpdatedObject(result.getCdmEntity());
3077

    
3078
        return result;
3079
    }
3080

    
3081
    @Override
3082
    @Transactional(readOnly = false)
3083
    public UpdateResult changeRelatedTaxonToSynonym(Taxon fromTaxon, Taxon toTaxon, TaxonRelationshipType oldRelationshipType,
3084
            SynonymType synonymType) throws DataChangeNoRollbackException {
3085

    
3086
        UpdateResult result = new UpdateResult();
3087
        // Create new synonym using concept name
3088
        TaxonName synonymName = fromTaxon.getName();
3089

    
3090
        // Remove concept relation from taxon
3091
        toTaxon.removeTaxon(fromTaxon, oldRelationshipType);
3092

    
3093
        // Create a new synonym for the taxon
3094
        Synonym synonym;
3095
        if (synonymType != null
3096
                && synonymType.equals(SynonymType.HOMOTYPIC_SYNONYM_OF())){
3097
            synonym = Synonym.NewInstance(synonymName, fromTaxon.getSec());
3098
            toTaxon.addHomotypicSynonym(synonym);
3099
        } else{
3100
            synonym = toTaxon.addHeterotypicSynonymName(synonymName);
3101
        }
3102
        //keep the publish flag
3103
        synonym.setPublish(fromTaxon.isPublish());
3104
        this.saveOrUpdate(toTaxon);
3105
        //TODO: configurator and classification
3106
        TaxonDeletionConfigurator config = new TaxonDeletionConfigurator();
3107
        config.setDeleteNameIfPossible(false);
3108
        result.includeResult(this.deleteTaxon(fromTaxon.getUuid(), config, null));
3109
        result.setCdmEntity(synonym);
3110
        result.addUpdatedObject(toTaxon);
3111
        result.addUpdatedObject(synonym);
3112
        return result;
3113
    }
3114

    
3115
    @Override
3116
    public DeleteResult isDeletable(UUID taxonBaseUuid, DeleteConfiguratorBase config){
3117
        DeleteResult result = new DeleteResult();
3118
        TaxonBase<?> taxonBase = load(taxonBaseUuid);
3119
        Set<CdmBase> references = commonService.getReferencingObjectsForDeletion(taxonBase);
3120
        if (taxonBase instanceof Taxon){
3121
            TaxonDeletionConfigurator taxonConfig = (TaxonDeletionConfigurator) config;
3122
            List<String> propertyPaths = new ArrayList<>();
3123
            propertyPaths.add("taxonNodes");
3124
            Taxon taxon = (Taxon)load(taxonBaseUuid, propertyPaths);
3125

    
3126
            result = isDeletableForTaxon(references, taxonConfig );
3127

    
3128
            if (taxonConfig.isDeleteNameIfPossible()){
3129
                if (taxonBase.getName() != null){
3130
                    DeleteResult nameResult = nameService.isDeletable(taxonBase.getName().getUuid(), taxonConfig.getNameDeletionConfig(), taxon.getUuid());
3131
                    if (!nameResult.isOk()){
3132
                        result.addExceptions(nameResult.getExceptions());
3133
                    }
3134
                }
3135

    
3136
            }
3137
        }else{
3138
            SynonymDeletionConfigurator synonymConfig = (SynonymDeletionConfigurator) config;
3139
            result = isDeletableForSynonym(references, synonymConfig);
3140
            if (synonymConfig.isDeleteNameIfPossible() && taxonBase.getName() != null){
3141
                DeleteResult nameResult = nameService.isDeletable(taxonBase.getName().getUuid(), synonymConfig.getNameDeletionConfig(), taxonBase.getUuid());
3142
                if (!nameResult.isOk()){
3143
                    result.addExceptions(nameResult.getExceptions());
3144

    
3145
                }
3146
            }
3147
        }
3148

    
3149
        return result;
3150
    }
3151

    
3152
    private DeleteResult isDeletableForSynonym(Set<CdmBase> references, SynonymDeletionConfigurator config){
3153

    
3154
        DeleteResult result = new DeleteResult();
3155
        for (CdmBase ref: references){
3156
            if (!(ref instanceof Taxon || ref instanceof TaxonName || ref instanceof SecundumSource)){
3157
                String message = "The Synonym can't be deleted as long as it is referenced by " + ref.getClass().getSimpleName() + " with id "+ ref.getId();
3158
                result.addException(new ReferencedObjectUndeletableException(message));
3159
                result.addRelatedObject(ref);
3160
                result.setAbort();
3161
            }
3162
        }
3163

    
3164
        return result;
3165
    }
3166

    
3167
    private DeleteResult isDeletableForTaxon(Set<CdmBase> references, TaxonDeletionConfigurator config){
3168
        String message = null;
3169
        DeleteResult result = new DeleteResult();
3170
        for (CdmBase ref: references){
3171
            if (!(ref instanceof TaxonName || ref instanceof SecundumSource)){
3172
            	message = null;
3173
                if (!config.isDeleteSynonymRelations() && (ref instanceof Synonym)){
3174
                    message = "The taxon can't be deleted as long as it has synonyms.";
3175
                }
3176
                if (!config.isDeleteDescriptions() && (ref instanceof DescriptionBase)){
3177
                    message = "The taxon can't be deleted as long as it has factual data.";
3178
                }
3179

    
3180
                if (!config.isDeleteTaxonNodes() && (ref instanceof TaxonNode)){
3181
                    message = "The taxon can't be deleted as long as it belongs to a taxon node.";
3182
                }
3183
                if (ref instanceof TaxonNode && config.getClassificationUuid() != null && !config.isDeleteInAllClassifications() && !((TaxonNode)ref).getClassification().getUuid().equals(config.getClassificationUuid())){
3184
                    message = "The taxon can't be deleted as long as it is used in more than one classification";
3185

    
3186
                }
3187
                if (!config.isDeleteTaxonRelationships() && (ref instanceof TaxonRelationship)){
3188
                    if (!config.isDeleteMisappliedNames() &&
3189
                            (((TaxonRelationship)ref).getType().isMisappliedName())){
3190
                        message = "The taxon can't be deleted as long as it has misapplied names or invalid designations.";
3191
                    } else{
3192
                        message = "The taxon can't be deleted as long as it belongs to taxon relationship.";
3193
                    }
3194
                }
3195
                if (ref instanceof PolytomousKeyNode){
3196
                    message = "The taxon can't be deleted as long as it is referenced by a polytomous key node.";
3197
                }
3198

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

    
3203
               /* //PolytomousKeyNode
3204
                if (referencingObject.isInstanceOf(PolytomousKeyNode.class)){
3205
                    String message = "Taxon" + taxon.getTitleCache() + " can't be deleted as it is used in polytomous key node";
3206
                    return message;
3207
                }*/
3208

    
3209
                //TaxonInteraction
3210
                if (ref.isInstanceOf(TaxonInteraction.class)){
3211
                    message = "Taxon can't be deleted as it is used in taxonInteraction#taxon2";
3212
                }
3213

    
3214
              //TaxonInteraction
3215
                if (ref.isInstanceOf(DeterminationEvent.class)){
3216
                    message = "Taxon can't be deleted as it is used in a determination event";
3217
                }
3218
            }
3219
            if (message != null){
3220
	            result.addException(new ReferencedObjectUndeletableException(message));
3221
	            result.addRelatedObject(ref);
3222
	            result.setAbort();
3223
            }
3224
        }
3225

    
3226
        return result;
3227
    }
3228

    
3229
    @Override
3230
    public IncludedTaxaDTO listIncludedTaxa(UUID taxonUuid, IncludedTaxonConfiguration config) {
3231
        IncludedTaxaDTO result = new IncludedTaxaDTO(taxonUuid);
3232

    
3233
        //preliminary implementation
3234

    
3235
        Set<Taxon> taxa = new HashSet<>();
3236
        TaxonBase<?> taxonBase = find(taxonUuid);
3237
        if (taxonBase == null){
3238
            return new IncludedTaxaDTO();
3239
        }else if (taxonBase.isInstanceOf(Taxon.class)){
3240
            Taxon taxon = CdmBase.deproxy(taxonBase, Taxon.class);
3241
            taxa.add(taxon);
3242
        }else if (taxonBase.isInstanceOf(Synonym.class)){
3243
            //TODO partial synonyms ??
3244
            //TODO synonyms in general
3245
            Synonym syn = CdmBase.deproxy(taxonBase, Synonym.class);
3246
            taxa.add(syn.getAcceptedTaxon());
3247
        }else{
3248
            throw new IllegalArgumentException("Unhandled class " + taxonBase.getClass().getSimpleName());
3249
        }
3250

    
3251
        Set<Taxon> related = makeRelatedIncluded(taxa, result, config);
3252
        int i = 0;
3253
        while((! related.isEmpty()) && i++ < 100){  //to avoid
3254
             related = makeRelatedIncluded(related, result, config);
3255
        }
3256

    
3257
        return result;
3258
    }
3259

    
3260
    /**
3261
     * @param uncheckedTaxa
3262
     * @param existingTaxa
3263
     * @param config
3264
     *
3265
     * Computes all children and conceptually congruent and included taxa and adds them to the existingTaxa
3266
     * data structure.
3267
     * @return the set of conceptually related taxa for further use
3268
     */
3269
    private Set<Taxon> makeRelatedIncluded(Set<Taxon> uncheckedTaxa, IncludedTaxaDTO existingTaxa, IncludedTaxonConfiguration config) {
3270

    
3271
        //children
3272
        Set<TaxonNode> taxonNodes = new HashSet<>();
3273
        for (Taxon taxon: uncheckedTaxa){
3274
            taxonNodes.addAll(taxon.getTaxonNodes());
3275
        }
3276

    
3277
        Set<Taxon> children = new HashSet<>();
3278
        if (! config.onlyCongruent){
3279
            for (TaxonNode node: taxonNodes){
3280
                List<TaxonNode> childNodes = nodeService.loadChildNodesOfTaxonNode(node, null, true, config.includeUnpublished, null);
3281
                for (TaxonNode child : childNodes){
3282
                    children.add(child.getTaxon());
3283
                }
3284
            }
3285
            children.remove(null);  // just to be on the save side
3286
        }
3287

    
3288
        Iterator<Taxon> it = children.iterator();
3289
        while(it.hasNext()){
3290
            UUID uuid = it.next().getUuid();
3291
            if (existingTaxa.contains(uuid)){
3292
                it.remove();
3293
            }else{
3294
                existingTaxa.addIncludedTaxon(uuid, new ArrayList<>(), false);
3295
            }
3296
        }
3297

    
3298
        //concept relations
3299
        Set<Taxon> uncheckedAndChildren = new HashSet<>(uncheckedTaxa);
3300
        uncheckedAndChildren.addAll(children);
3301

    
3302
        Set<Taxon> relatedTaxa = makeConceptIncludedTaxa(uncheckedAndChildren, existingTaxa, config);
3303

    
3304

    
3305
        Set<Taxon> result = new HashSet<>(relatedTaxa);
3306
        return result;
3307
    }
3308

    
3309
    /**
3310
     * Computes all conceptually congruent or included taxa and adds them to the existingTaxa data structure.
3311
     * @return the set of these computed taxa
3312
     */
3313
    private Set<Taxon> makeConceptIncludedTaxa(Set<Taxon> unchecked, IncludedTaxaDTO existingTaxa, IncludedTaxonConfiguration config) {
3314
        Set<Taxon> result = new HashSet<>();
3315

    
3316
        for (Taxon taxon : unchecked){
3317
            Set<TaxonRelationship> fromRelations = taxon.getRelationsFromThisTaxon();
3318
            Set<TaxonRelationship> toRelations = taxon.getRelationsToThisTaxon();
3319

    
3320
            for (TaxonRelationship fromRel : fromRelations){
3321
                if (config.includeDoubtful == false && fromRel.isDoubtful()){
3322
                    continue;
3323
                }
3324
                TaxonRelationshipType fromRelType = fromRel.getType();
3325
                if (fromRelType.equals(TaxonRelationshipType.CONGRUENT_TO()) ||
3326
                        !config.onlyCongruent && (
3327
                            fromRelType.equals(TaxonRelationshipType.INCLUDES()) ||
3328
                            fromRelType.equals(TaxonRelationshipType.CONGRUENT_OR_INCLUDES())
3329
                        )
3330
                    ){
3331
                    result.add(fromRel.getToTaxon());
3332
                }
3333
            }
3334

    
3335
            for (TaxonRelationship toRel : toRelations){
3336
                if (config.includeDoubtful == false && toRel.isDoubtful()){
3337
                    continue;
3338
                }
3339
                TaxonRelationshipType fromRelType = toRel.getType();
3340
                if (fromRelType.equals(TaxonRelationshipType.CONGRUENT_TO()) ||
3341
                        !config.includeDoubtful && fromRelType.equals(TaxonRelationshipType.TAXONOMICALLY_INCLUDED_IN())){
3342
                    result.add(toRel.getFromTaxon());
3343
                }
3344
            }
3345
        }
3346

    
3347
        Iterator<Taxon> it = result.iterator();
3348
        while(it.hasNext()){
3349
            UUID uuid = it.next().getUuid();
3350
            if (existingTaxa.contains(uuid)){
3351
                it.remove();
3352
            }else{
3353
                existingTaxa.addIncludedTaxon(uuid, new ArrayList<>(), false);
3354
            }
3355
        }
3356
        return result;
3357
    }
3358

    
3359
    @Override
3360
    public List<TaxonBase> findTaxaByName(MatchingTaxonConfigurator config){
3361
        @SuppressWarnings("rawtypes")
3362
        List<TaxonBase> taxonList = dao.getTaxaByName(true, config.isIncludeSynonyms(), false, false, false,
3363
                config.getTaxonNameTitle(), null, null, MatchMode.EXACT, null, config.isIncludeSynonyms(), null, 0, 0, config.getPropertyPath());
3364
        return taxonList;
3365
    }
3366

    
3367
	@Override
3368
	@Transactional(readOnly = true)
3369
	public <S extends TaxonBase> Pager<IdentifiedEntityDTO<S>> findByIdentifier(
3370
			Class<S> clazz, String identifier, DefinedTerm identifierType, TaxonNode subtreeFilter,
3371
			MatchMode matchmode, boolean includeEntity, Integer pageSize,
3372
			Integer pageNumber,	List<String> propertyPaths) {
3373
		if (subtreeFilter == null){
3374
			return findByIdentifier(clazz, identifier, identifierType, matchmode, includeEntity, pageSize, pageNumber, propertyPaths);
3375
		}
3376

    
3377
		long numberOfResults = dao.countByIdentifier(clazz, identifier, identifierType, subtreeFilter, matchmode);
3378
        List<Object[]> daoResults = new ArrayList<>();
3379
        if(numberOfResults > 0) { // no point checking again
3380
        	daoResults = dao.findByIdentifier(clazz, identifier, identifierType, subtreeFilter,
3381
    				matchmode, includeEntity, pageSize, pageNumber, propertyPaths);
3382
        }
3383

    
3384
        List<IdentifiedEntityDTO<S>> result = new ArrayList<>();
3385
        for (Object[] daoObj : daoResults){
3386
        	if (includeEntity){
3387
        		result.add(new IdentifiedEntityDTO<>((DefinedTerm)daoObj[0], (String)daoObj[1], (S)daoObj[2]));
3388
        	}else{
3389
        		result.add(new IdentifiedEntityDTO<>((DefinedTerm)daoObj[0], (String)daoObj[1], (UUID)daoObj[2], (String)daoObj[3], null));
3390
        	}
3391
        }
3392
		return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, result);
3393
	}
3394

    
3395
	@Override
3396
    @Transactional(readOnly = true)
3397
    public <S extends TaxonBase> Pager<MarkedEntityDTO<S>> findByMarker(
3398
            Class<S> clazz, MarkerType markerType, Boolean markerValue,
3399
            TaxonNode subtreeFilter, boolean includeEntity, TaxonTitleType titleType,
3400
            Integer pageSize, Integer pageNumber, List<String> propertyPaths) {
3401
        if (subtreeFilter == null){
3402
            return super.findByMarker (clazz, markerType, markerValue, includeEntity, pageSize, pageNumber, propertyPaths);
3403
        }
3404

    
3405
        Long numberOfResults = dao.countByMarker(clazz, markerType, markerValue, subtreeFilter);
3406
        List<Object[]> daoResults = new ArrayList<>();
3407
        if(numberOfResults > 0) { // no point checking again
3408
            daoResults = dao.findByMarker(clazz, markerType, markerValue, subtreeFilter,
3409
                    includeEntity, titleType, pageSize, pageNumber, propertyPaths);
3410
        }
3411

    
3412
        List<MarkedEntityDTO<S>> result = new ArrayList<>();
3413
        for (Object[] daoObj : daoResults){
3414
            if (includeEntity){
3415
                result.add(new MarkedEntityDTO<S>((MarkerType)daoObj[0], (Boolean)daoObj[1], (S)daoObj[2]));
3416
            }else{
3417
                result.add(new MarkedEntityDTO<S>((MarkerType)daoObj[0], (Boolean)daoObj[1], (UUID)daoObj[2], (String)daoObj[3]));
3418
            }
3419
        }
3420
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, result);
3421
    }
3422

    
3423
    @Override
3424
	public UpdateResult moveFactualDateToAnotherTaxon(UUID fromTaxonUuid, UUID toTaxonUuid){
3425
		UpdateResult result = new UpdateResult();
3426

    
3427
		Taxon fromTaxon = (Taxon)dao.load(fromTaxonUuid);
3428
		Taxon toTaxon = (Taxon) dao.load(toTaxonUuid);
3429
    	for(TaxonDescription description : fromTaxon.getDescriptions()){
3430
              //reload to avoid session conflicts
3431
              description = HibernateProxyHelper.deproxy(description, TaxonDescription.class);
3432

    
3433
              String moveMessage = String.format("Description moved from %s", fromTaxon);
3434
              if(description.isProtectedTitleCache()){
3435
                  String separator = "";
3436
                  if(!StringUtils.isBlank(description.getTitleCache())){
3437
                      separator = " - ";
3438
                  }
3439
                  description.setTitleCache(description.getTitleCache() + separator + moveMessage, true);
3440
              }
3441
              Annotation annotation = Annotation.NewInstance(moveMessage, Language.getDefaultLanguage());
3442
              annotation.setAnnotationType(AnnotationType.TECHNICAL());
3443
              description.addAnnotation(annotation);
3444
              toTaxon.addDescription(description);
3445
              dao.saveOrUpdate(toTaxon);
3446
              dao.saveOrUpdate(fromTaxon);
3447
              result.addUpdatedObject(toTaxon);
3448
              result.addUpdatedObject(fromTaxon);
3449

    
3450
        }
3451

    
3452
    	return result;
3453
	}
3454

    
3455
	@Override
3456
	@Transactional(readOnly = false)
3457
	public UpdateResult swapSynonymAndAcceptedTaxon(UUID synonymUUid,
3458
			UUID acceptedTaxonUuid, boolean setNameInSource, boolean newUuidForAcceptedTaxon, SecReferenceHandlingSwapEnum secHandling, UUID newSecAcc, UUID newSecSyn) {
3459
		TaxonBase<?> base = this.load(synonymUUid);
3460
		Synonym syn = HibernateProxyHelper.deproxy(base, Synonym.class);
3461
		base = this.load(acceptedTaxonUuid);
3462
		Taxon taxon = HibernateProxyHelper.deproxy(base, Taxon.class);
3463

    
3464
		Reference refAcc = referenceService.load(newSecAcc);
3465
		Reference refSyn = referenceService.load(newSecSyn);
3466

    
3467
		return this.swapSynonymAndAcceptedTaxon(syn, taxon, setNameInSource, newUuidForAcceptedTaxon, secHandling, refAcc, refSyn);
3468
	}
3469

    
3470
    @Override
3471
    public TaxonRelationshipsDTO listTaxonRelationships(UUID taxonUuid, Set<TaxonRelationshipType> directTypes,
3472
            Set<TaxonRelationshipType> inversTypes,
3473
            Direction direction, boolean groupMisapplications,
3474
            boolean includeUnpublished,
3475
            Integer pageSize, Integer pageNumber) {
3476
        TaxonBase<?> taxonBase = dao.load(taxonUuid);
3477
        if (taxonBase == null || !taxonBase.isInstanceOf(TaxonBase.class)){
3478
            //TODO handle
3479
            throw new RuntimeException("Taxon for uuid " + taxonUuid + " not found");
3480
        }else{
3481
            Taxon taxon = CdmBase.deproxy(taxonBase, Taxon.class);
3482
            boolean doDirect = (direction == null || direction == Direction.relatedTo);
3483
            boolean doInvers = (direction == null || direction == Direction.relatedFrom);
3484

    
3485
            TaxonRelationshipsDTO dto = new TaxonRelationshipsDTO();
3486

    
3487
            //TODO paging is difficult because misapplication string is an attribute
3488
            //of toplevel dto
3489
//        long numberOfResults = dao.countTaxonRelationships(taxon, type, includeUnpublished, TaxonRelationship.Direction.relatedFrom);
3490
//        List<TaxonRelationshipsDTO> results = new ArrayList<>();
3491
//        if(numberOfResults > 0) { // no point checking again
3492
//            results = dao.getTaxonRelationships(taxon, type, includeUnpublished, pageSize, pageNumber, orderHints, propertyPaths, TaxonRelationship.Direction.relatedFrom);
3493
//        }
3494
//
3495
//        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);;
3496

    
3497
            //TODO languages
3498
            List<Language> languages = null;
3499
            if (doDirect){
3500
                direction = Direction.relatedTo;
3501
                //TODO order hints, property path
3502
                List<TaxonRelationship> relations = dao.getTaxonRelationships(taxon, directTypes, includeUnpublished, pageSize, pageNumber, null, null, direction.invers());
3503
                for (TaxonRelationship relation : relations){
3504
                    dto.addRelation(relation, direction, languages);
3505
                }
3506
            }
3507
            if (doInvers){
3508
                direction = Direction.relatedFrom;
3509
                //TODO order hints, property path
3510
                List<TaxonRelationship> relations = dao.getTaxonRelationships(taxon, inversTypes, includeUnpublished, pageSize, pageNumber, null, null, direction.invers());
3511
                for (TaxonRelationship relation : relations){
3512
                    dto.addRelation(relation, direction, languages);
3513
                }
3514
            }
3515
            if (groupMisapplications){
3516
                //TODO
3517
                dto.createMisapplicationString();
3518
            }
3519
            return dto;
3520
        }
3521
    }
3522
}
(89-89/97)