Project

General

Profile

Download (167 KB) Statistics
| Branch: | Tag: | Revision:
1
/**
2
* Copyright (C) 2007 EDIT
3
* European Distributed Institute of Taxonomy
4
* http://www.e-taxonomy.eu
5
*
6
* The contents of this file are subject to the Mozilla Public License Version 1.1
7
* See LICENSE.TXT at the top of this package for the full license terms.
8
*/
9

    
10
package eu.etaxonomy.cdm.api.service;
11

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

    
24
import javax.persistence.EntityNotFoundException;
25

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

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

    
141

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

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

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

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

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

    
160
    @Autowired
161
    private ITaxonNodeDao taxonNodeDao;
162

    
163
    @Autowired
164
    private ITaxonNameDao nameDao;
165

    
166
    @Autowired
167
    private INameService nameService;
168

    
169
    @Autowired
170
    private IOccurrenceService occurrenceService;
171

    
172
    @Autowired
173
    private ITaxonNodeService nodeService;
174

    
175
    @Autowired
176
    private IDescriptionService descriptionService;
177

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

    
184
    @Autowired
185
    private IOccurrenceDao occurrenceDao;
186

    
187
    @Autowired
188
    private IClassificationDao classificationDao;
189

    
190
    @Autowired
191
    private AbstractBeanInitializer beanInitializer;
192

    
193
    @Autowired
194
    private ILuceneIndexToolProvider luceneIndexToolProvider;
195

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

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

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

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

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

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

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

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

    
241

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

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

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

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

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

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

    
280
		return result;
281
    }
282

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

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

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

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

    
312
        newTaxon.setName(synonymName);
313

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

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

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

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

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

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

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

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

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

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

    
387
        return result;
388
    }
389

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

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

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

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

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

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

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

    
457
        return result;
458
    }
459

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

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

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

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

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

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

    
535
        UpdateResult result = new UpdateResult();
536

    
537
        TaxonName synonymName = synonym.getName();
538

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

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

    
556
        return result;
557
    }
558

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
728
        Synonym synonym = null;
729

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
861
        return result;
862
    }
863

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

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

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

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

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

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

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

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

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

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

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

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

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

    
936
        numberOfResults += numberTaxaResults;
937

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1024

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

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

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

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

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

    
1083
        taxonMedia = deduplicateMedia(taxonMedia);
1084

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

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

    
1090
        return taxonMedia;
1091
    }
1092

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1297
       return result;
1298
    }
1299

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

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

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

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

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

    
1329
        if (result.isOk()){
1330

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1456
        return bestCandidate;
1457
    }
1458

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

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

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

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

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

    
1509
        return result;
1510
    }
1511

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

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

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

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

    
1550
        if (! isSingleInGroup){
1551
            boolean isHomotypicToAccepted = synonymName.isHomotypic(fromTaxonName);
1552
            boolean hasHomotypicSynonymRelatives = isHomotypicToAccepted ? hgSize > 2 : hgSize > 1;
1553
            if (isHomotypicToAccepted){
1554
                String message = "Synonym is in homotypic group with accepted taxon%s. First remove synonym from homotypic group of accepted taxon before moving to other taxon.";
1555
                String homotypicRelatives = hasHomotypicSynonymRelatives ? " and other synonym(s)":"";
1556
                message = String.format(message, homotypicRelatives);
1557
                throw new HomotypicalGroupChangeException(message);
1558
            }
1559
            if (! moveHomotypicGroup){
1560
                String message = "Synonym is in homotypic group with other synonym(s). Either move complete homotypic group or remove synonym from homotypic group prior to moving to other taxon.";
1561
                throw new HomotypicalGroupChangeException(message);
1562
            }
1563
        }else{
1564
            moveHomotypicGroup = true;  //single synonym always allows to moveCompleteGroup
1565
        }
1566
//        Assert.assertTrue("Synonym can only be moved with complete homotypic group", moveHomotypicGroup);
1567

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

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

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

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

    
1592
        result.addUpdatedObject(oldTaxon);
1593
        result.addUpdatedObject(newTaxon);
1594
        saveOrUpdate(oldTaxon);
1595
        saveOrUpdate(newTaxon);
1596

    
1597
        return result;
1598
    }
1599

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

    
1603
        return dao.getUuidAndTitleCache(clazz, limit, pattern);
1604
    }
1605

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

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

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

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

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

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

    
1639
    @Transactional(readOnly = true)
1640
    @Override
1641
    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) {
1642
         long numberOfResults = dao.countByTitleWithRestrictions(clazz, queryString, matchmode, restrictions);
1643

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

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

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

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

    
1682
        LuceneSearch luceneSearch = prepareByDistributionSearch(areaFilter, statusFilter, classification, subtree);
1683

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

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

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

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

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

    
1721
        Builder finalQueryBuilder = new Builder();
1722
        Builder textQueryBuilder = new Builder();
1723

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

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

    
1732
        // ---- search criteria
1733
        luceneSearch.setCdmTypRestriction(clazz);
1734

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

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

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

    
1760
        luceneSearch.setQuery(finalQueryBuilder.build());
1761

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

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

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

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

    
1815
        Builder finalQueryBuilder = new Builder();
1816

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

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

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

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

    
1837
        finalQueryBuilder.add(joinQuery, Occur.MUST);
1838

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

    
1846
        luceneSearch.setQuery(finalQueryBuilder.build());
1847

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

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

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

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

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

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

    
1902
        boolean addDistributionFilter = namedAreas != null && namedAreas.size() > 0;
1903

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

    
1907
        /*
1908
          ======== filtering by distribution , HOWTO ========
1909

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

    
1915

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

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

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

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

    
1948
        Builder multiIndexByAreaFilterBuilder = new Builder();
1949
        boolean includeUnpublished = searchModes.contains(TaxaAndNamesSearchMode.includeUnpublished);
1950

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

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

    
1988
            }
1989
        }
1990

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

    
2016
            idFieldMap.put(CdmBaseType.TAXON_BASE, "id");
2017

    
2018
            luceneSearches.add(byCommonNameSearch);
2019

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

    
2038

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

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

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

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

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

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

    
2101
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
2102
            }
2103
        }
2104

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

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

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

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

    
2129
        if(addDistributionFilter){
2130

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

    
2146
        if (addDistributionFilter){
2147
            multiSearch.setFilter(multiIndexByAreaFilterBuilder.build());
2148
        }
2149

    
2150

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

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

    
2164

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

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

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

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

    
2190
        BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, queryFactory);
2191

    
2192
        ScoreMode scoreMode = asFilter ?  ScoreMode.None : ScoreMode.Max;
2193

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

    
2196
        return taxonAreaJoinQuery;
2197
    }
2198

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

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

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

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

    
2236
        Builder finalQueryBuilder = new Builder();
2237

    
2238
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, Taxon.class);
2239

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

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

    
2246

    
2247
        Query byAreaQuery = createByDistributionJoinQuery(namedAreaList, distributionStatusList, taxonQueryFactory, null, false);
2248

    
2249
        finalQueryBuilder.add(byAreaQuery, Occur.MUST);
2250

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

    
2261
        return luceneSearch;
2262
    }
2263

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

    
2270
        LuceneSearch luceneSearch = prepareByDescriptionElementFullTextSearch(clazz, queryString, classification, subtree, features, languages, highlightFragments);
2271

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

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

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

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

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

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

    
2306
        LuceneMultiSearch multiSearch = new LuceneMultiSearch(luceneIndexToolProvider, luceneSearchByDescriptionElement, luceneSearchByTaxonBase);
2307

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

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

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

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

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

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

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

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

    
2351
        BooleanQuery finalQuery = createByDescriptionElementFullTextQuery(queryString, classification, subtree, features,
2352
                languages, descriptionElementQueryFactory);
2353

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

    
2361
        return luceneSearch;
2362
    }
2363

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

    
2376
        Builder finalQueryBuilder = new Builder();
2377
        Builder textQueryBuilder = new Builder();
2378

    
2379
        if(!StringUtils.isEmpty(queryString)){
2380

    
2381
            textQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("titleCache", queryString), Occur.SHOULD);
2382

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

    
2397

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

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

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

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

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

    
2414
            finalQueryBuilder.add(textQueryBuilder.build(), Occur.MUST);
2415

    
2416
        }
2417
        // --- classification ----
2418

    
2419

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

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

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

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

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

    
2443
        List <Synonym> inferredSynonyms = new ArrayList<>();
2444
        List<Synonym> inferredSynonymsToBeRemoved = new ArrayList<>();
2445

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

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

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

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

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

    
2480
                        Synonym inferredEpithet = null;
2481
                        Synonym inferredGenus = null;
2482
                        Synonym potentialCombination = null;
2483

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

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

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

    
2505
                        if (type.equals(SynonymType.INFERRED_EPITHET_OF())){
2506
                            for (Synonym synonymRelationOfParent:synonyMsOfParent){
2507

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

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

    
2520
                            if (doWithMisappliedNames){
2521

    
2522
                                for (TaxonRelationship taxonRelationship: taxonRelListParent){
2523
                                     Taxon misappliedName = taxonRelationship.getFromTaxon();
2524

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

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

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

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

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

    
2557
                        }else if (type.equals(SynonymType.INFERRED_GENUS_OF())){
2558

    
2559
                            for (Synonym synonymRelationOfTaxon:synonymsOfTaxon){
2560

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

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

    
2570
                            if (doWithMisappliedNames){
2571

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

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

    
2582

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

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

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

    
2603
                        }else if (type.equals(SynonymType.POTENTIAL_COMBINATION_OF())){
2604

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

    
2611
                                synName = synonymRelationOfParent.getName();
2612

    
2613
                                // Set the sourceReference
2614
                                sourceReference = synonymRelationOfParent.getSec();
2615

    
2616
                                // Determine the idInSource
2617
                                String idInSourceParent = getIdInSource(synonymRelationOfParent);
2618

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

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

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

    
2635
                                //for all synonyms of the taxon
2636

    
2637
                                for (Synonym synonymRelationOfTaxon:synonymsOfTaxon){
2638

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

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

    
2652
                            if (doWithMisappliedNames){
2653

    
2654
                                for (TaxonRelationship parentRelationship: taxonRelListParent){
2655

    
2656
                                    TaxonName misappliedParentName;
2657

    
2658
                                    Taxon misappliedParent = parentRelationship.getFromTaxon();
2659
                                    misappliedParentName = misappliedParent.getName();
2660

    
2661
                                    HibernateProxyHelper.deproxy(misappliedParent);
2662

    
2663
                                    // Set the sourceReference
2664
                                    sourceReference = misappliedParent.getSec();
2665

    
2666
                                    // Determine the idInSource
2667
                                    String idInSourceParent = getIdInSource(misappliedParent);
2668

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

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

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

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

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

    
2728
        return inferredSynonyms;
2729
    }
2730

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

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

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

    
2753
        String synTaxonInfraSpecificName= null;
2754

    
2755
        if (parentSynZooName.isSpecies()){
2756
            synTaxonInfraSpecificName = zooSynName.getInfraSpecificEpithet();
2757
        }
2758

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

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

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

    
2781
        potentialCombination = Synonym.NewInstance(inferredSynName, null);
2782

    
2783
        // Set the sourceReference
2784
        potentialCombination.setSec(sourceReference);
2785

    
2786

    
2787
        // Determine the idInSource
2788
        String idInSourceSyn= getIdInSource(syn);
2789

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

    
2797
        return potentialCombination;
2798
    }
2799

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

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

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

    
2818
        //logger.warn(sourceReference.getTitleCache());
2819

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

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

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

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

    
2843
        inferredGenus = Synonym.NewInstance(inferredSynName, null);
2844

    
2845
        // Set the sourceReference
2846
        inferredGenus.setSec(sourceReference);
2847

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

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

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

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

    
2871
        taxon.addSynonym(inferredGenus, SynonymType.INFERRED_GENUS_OF());
2872

    
2873
        return inferredGenus;
2874
    }
2875

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

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

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

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

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

    
2904
        if (zooSynName.getInfraGenericEpithet() != null){
2905
            synInfraGenericEpithet = zooSynName.getInfraGenericEpithet();
2906
        }
2907

    
2908
        if (zooSynName.isInfraSpecific()){
2909
            synSpecificEpithet = zooSynName.getSpecificEpithet();
2910
        }
2911

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

    
2916
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(taxon.getName().getRank());
2917

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

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

    
2934
        inferredEpithet = Synonym.NewInstance(inferredSynName, null);
2935

    
2936
        // Set the sourceReference
2937
        inferredEpithet.setSec(sourceReference);
2938

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

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

    
2952

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

    
2956
        inferredEpithet.addSource(originalSource);
2957

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

    
2961
        inferredSynName.addSource(originalSource);
2962

    
2963
        taxon.addSynonym(inferredEpithet, SynonymType.INFERRED_EPITHET_OF());
2964

    
2965
        return inferredEpithet;
2966
    }
2967

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

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

    
3009
        return idInSource;
3010
    }
3011

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

    
3028
        return citation;
3029
    }
3030

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

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

    
3039
        return inferredSynonyms;
3040
    }
3041

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

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

    
3050
        if (taxonBase == null) {
3051
            return list;
3052
        }
3053

    
3054
        taxonBase = load(taxonBase.getUuid());
3055

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

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

    
3082
        result.addUpdatedObject(toTaxon);
3083
        result.addUpdatedObject(result.getCdmEntity());
3084

    
3085
        return result;
3086
    }
3087

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

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

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

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

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

    
3133
            result = isDeletableForTaxon(references, taxonConfig );
3134

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

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

    
3152
                }
3153
            }
3154
        }
3155

    
3156
        return result;
3157
    }
3158

    
3159
    private DeleteResult isDeletableForSynonym(Set<CdmBase> references, SynonymDeletionConfigurator config){
3160

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

    
3171
        return result;
3172
    }
3173

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

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

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

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

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

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

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

    
3233
        return result;
3234
    }
3235

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

    
3240
        //preliminary implementation
3241

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

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

    
3264
        return result;
3265
    }
3266

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

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

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

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

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

    
3309
        Set<Taxon> relatedTaxa = makeConceptIncludedTaxa(uncheckedAndChildren, existingTaxa, config);
3310

    
3311

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
3457
        }
3458

    
3459
    	return result;
3460
	}
3461

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

    
3471
		Reference refAcc = referenceService.load(newSecAcc);
3472
		Reference refSyn = referenceService.load(newSecSyn);
3473

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

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

    
3492
            TaxonRelationshipsDTO dto = new TaxonRelationshipsDTO();
3493

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

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