Project

General

Profile

Download (169 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
import java.util.stream.Collectors;
24

    
25
import javax.persistence.EntityNotFoundException;
26

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

    
43
import eu.etaxonomy.cdm.api.service.config.DeleteConfiguratorBase;
44
import eu.etaxonomy.cdm.api.service.config.IFindTaxaAndNamesConfigurator;
45
import eu.etaxonomy.cdm.api.service.config.IncludedTaxonConfiguration;
46
import eu.etaxonomy.cdm.api.service.config.MatchingTaxonConfigurator;
47
import eu.etaxonomy.cdm.api.service.config.NodeDeletionConfigurator.ChildHandling;
48
import eu.etaxonomy.cdm.api.service.config.SynonymDeletionConfigurator;
49
import eu.etaxonomy.cdm.api.service.config.TaxonDeletionConfigurator;
50
import eu.etaxonomy.cdm.api.service.dto.IdentifiedEntityDTO;
51
import eu.etaxonomy.cdm.api.service.dto.IncludedTaxaDTO;
52
import eu.etaxonomy.cdm.api.service.dto.MarkedEntityDTO;
53
import eu.etaxonomy.cdm.api.service.dto.TaxonRelationshipsDTO;
54
import eu.etaxonomy.cdm.api.service.exception.DataChangeNoRollbackException;
55
import eu.etaxonomy.cdm.api.service.exception.HomotypicalGroupChangeException;
56
import eu.etaxonomy.cdm.api.service.exception.ReferencedObjectUndeletableException;
57
import eu.etaxonomy.cdm.api.service.pager.Pager;
58
import eu.etaxonomy.cdm.api.service.pager.impl.AbstractPagerImpl;
59
import eu.etaxonomy.cdm.api.service.pager.impl.DefaultPagerImpl;
60
import eu.etaxonomy.cdm.api.service.search.ILuceneIndexToolProvider;
61
import eu.etaxonomy.cdm.api.service.search.ISearchResultBuilder;
62
import eu.etaxonomy.cdm.api.service.search.LuceneMultiSearch;
63
import eu.etaxonomy.cdm.api.service.search.LuceneMultiSearchException;
64
import eu.etaxonomy.cdm.api.service.search.LuceneParseException;
65
import eu.etaxonomy.cdm.api.service.search.LuceneSearch;
66
import eu.etaxonomy.cdm.api.service.search.QueryFactory;
67
import eu.etaxonomy.cdm.api.service.search.SearchResult;
68
import eu.etaxonomy.cdm.api.service.search.SearchResultBuilder;
69
import eu.etaxonomy.cdm.api.util.TaxonRelationshipEdge;
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 = LogManager.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 oldSynonym, Taxon oldAcceptedTaxon, boolean setNameInSource, SecReferenceHandlingSwapEnum secHandling, Reference newSecAcc, Reference newSecSyn){
284
        UpdateResult result = new UpdateResult();
285
        oldAcceptedTaxon.removeSynonym(oldSynonym);
286
        TaxonName synonymName = oldSynonym.getName();
287
        TaxonName taxonName = HibernateProxyHelper.deproxy(oldAcceptedTaxon.getName());
288
        String oldTaxonTitleCache = oldAcceptedTaxon.getTitleCache();
289

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

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

    
304
        //move descriptions
305
        Set<TaxonDescription> descriptionsToCopy = new HashSet<>(oldAcceptedTaxon.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(oldSynonym.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 : oldAcceptedTaxon.getTaxonRelations()){
328
            Taxon fromTaxon = HibernateProxyHelper.deproxy(taxonRelationship.getFromTaxon());
329
            Taxon toTaxon = HibernateProxyHelper.deproxy(taxonRelationship.getToTaxon());
330
            if (fromTaxon == oldAcceptedTaxon){
331
                newTaxon.addTaxonRelation(taxonRelationship.getToTaxon(), taxonRelationship.getType(),
332
                        taxonRelationship.getCitation(), taxonRelationship.getCitationMicroReference());
333

    
334
            }else if(toTaxon == oldAcceptedTaxon){
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<>(oldAcceptedTaxon.getTaxonNodes());
352
        for (TaxonNode node: nodes){
353
            node = HibernateProxyHelper.deproxy(node);
354
            TaxonNode parent = node.getParent();
355
            oldAcceptedTaxon.removeTaxonNode(node);
356
            node.setTaxon(newTaxon);
357
            if (parent != null){
358
                parent.addChildNode(node, null, null);
359
            }
360
        }
361

    
362
        //synonym
363
        Synonym newSynonym = oldSynonym.clone();
364
        newSynonym.setName(taxonName);
365
        newSynonym.setPublish(oldAcceptedTaxon.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(oldAcceptedTaxon.getUuid(), conf, null);
381
        if (oldSynonym.isPersited()){
382
            oldSynonym.setSecSource(null);
383
            deleteResult.includeResult(deleteSynonym(oldSynonym.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 taxonMedia.stream().distinct().collect(Collectors.toList());
1095
    }
1096

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1297
       return result;
1298
    }
1299

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

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

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

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

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

    
1329
        if (result.isOk()){
1330

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1456
        return bestCandidate;
1457
    }
1458

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

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

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

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

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

    
1509
        return result;
1510
    }
1511

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

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

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

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

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

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

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

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

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

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

    
1599
        return result;
1600
    }
1601

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1817
        Builder finalQueryBuilder = new Builder();
1818

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

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

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

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

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

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

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

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

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

    
1865
        // FIXME: allow taxonomic ordering
1866
        //  hql equivalent:  order by t.name.genusOrUninomial, case when t.name.specificEpithet
1867
        // like '\"%\"' then 1 else 0 end, t.name.specificEpithet, t.name.rank desc, t.name.nameCache";
1868
        // this requires building a special sort column by a special classBridge
1869
        if(highlightFragments){
1870
            logger.warn("findTaxaAndNamesByFullText() : fragment highlighting is " +
1871
                    "currently not fully supported by this method and thus " +
1872
                    "may not work with common names and misapplied names.");
1873
        }
1874

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

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

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

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

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

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

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

    
1918

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

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

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

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

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

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

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

    
1991
            }
1992
        }
1993

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

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

    
2021
            luceneSearches.add(byCommonNameSearch);
2022

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

    
2041

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

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

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

    
2067
                //TODO replace by createByDistributionJoinQuery
2068
                BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, distributionFilterQueryFactory);
2069

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

    
2097

    
2098
                Query taxonAreaJoinQuery = distributionFilterQueryFactory.newJoinQuery(Distribution.class,
2099
                        fromField, true, byDistributionQuery, toField, null, ScoreMode.None);
2100
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
2101

    
2102
                String toFieldProParte = "relation." + TaxonRelationshipType.uuidProParteMisappliedNameFor +".to.id";
2103
                Query taxonAreaJoinQueryProParte = distributionFilterQueryFactory.newJoinQuery(Distribution.class,
2104
                        fromField, true, byDistributionQuery, toFieldProParte, null, ScoreMode.None);
2105
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQueryProParte, Occur.SHOULD);
2106

    
2107
                String toFieldPartial = "relation." + TaxonRelationshipType.uuidPartialMisappliedNameFor +".to.id";
2108
                Query taxonAreaJoinQueryPartial = distributionFilterQueryFactory.newJoinQuery(Distribution.class,
2109
                        fromField, true, byDistributionQuery, toFieldPartial, null, ScoreMode.None);
2110
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQueryPartial, Occur.SHOULD);
2111
            }
2112
        }
2113

    
2114
        // search by pro parte synonyms
2115
        if(searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)) {
2116
            //TODO merge with misapplied name search once #7487 is fixed
2117
            Set<TaxonRelationshipType> relTypes = new HashSet<>();
2118
            relTypes.addAll(TaxonRelationshipType.allSynonymTypes());
2119

    
2120
            luceneSearches.add(prepareFindByTaxonRelationFullTextSearch(
2121
                    new TaxonRelationshipEdge(relTypes, Direction.relatedTo),
2122
                    queryString, classification, subtree, includeUnpublished, languages, highlightFragments, sortFields));
2123
            idFieldMap.put(CdmBaseType.TAXON_BASE, "id");
2124

    
2125
            if(addDistributionFilter){
2126
                String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
2127

    
2128
                //proparte synonyms
2129
                String toField = "relation."+TaxonRelationshipType.uuidProParteSynonymFor+".to.id";
2130
                BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, distributionFilterQueryFactory);
2131
                Query taxonAreaJoinQuery = distributionFilterQueryFactory.newJoinQuery(Distribution.class,
2132
                        fromField, true, byDistributionQuery, toField, null, ScoreMode.None);
2133
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
2134

    
2135
                //partial synonyms
2136
                toField = "relation."+TaxonRelationshipType.uuidPartialSynonymFor+".to.id";
2137
//                BooleanQuery byDistributionQuery2 = createByDistributionQuery(namedAreaList, distributionStatusList, distributionFilterQueryFactory);
2138
                Query taxonAreaJoinQuery2 = distributionFilterQueryFactory.newJoinQuery(Distribution.class,
2139
                        fromField, true, byDistributionQuery, toField, null, ScoreMode.None);
2140
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery2, Occur.SHOULD);
2141
            }
2142
        }//end pro parte synonyms
2143

    
2144
        LuceneMultiSearch multiSearch = new LuceneMultiSearch(luceneIndexToolProvider,
2145
                luceneSearches.toArray(new LuceneSearch[luceneSearches.size()]));
2146

    
2147
        if(addDistributionFilter){
2148

    
2149
            // B)
2150
            // in this case we need a filter which uses a join query
2151
            // to get the TaxonBase documents for the DescriptionElementBase documents
2152
            // which are matching the areas in question
2153
            //
2154
            // for doTaxa, doByCommonName
2155
            Query taxonAreaJoinQuery = createByDistributionJoinQuery(
2156
                    namedAreaList,
2157
                    distributionStatusList,
2158
                    distributionFilterQueryFactory,
2159
                    Taxon.class, true
2160
                    );
2161
            multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
2162
        }
2163

    
2164
        if (addDistributionFilter){
2165
            multiSearch.setFilter(multiIndexByAreaFilterBuilder.build());
2166
        }
2167

    
2168

    
2169
        // --- execute search
2170
        TopGroups<BytesRef> topDocsResultSet;
2171
        try {
2172
            topDocsResultSet = multiSearch.executeSearch(pageSize, pageNumber);
2173
        } catch (ParseException e) {
2174
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
2175
            luceneParseException.setStackTrace(e.getStackTrace());
2176
            throw luceneParseException;
2177
        }
2178

    
2179
        // --- initialize taxa, highlight matches ....
2180
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(multiSearch, multiSearch.getQuery());
2181

    
2182

    
2183
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
2184
                topDocsResultSet, multiSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
2185

    
2186
        long totalHits = (topDocsResultSet != null) ? Long.valueOf(topDocsResultSet.totalGroupCount) : 0;
2187
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
2188
    }
2189

    
2190
    /**
2191
     * @param namedAreaList at least one area must be in the list
2192
     * @param distributionStatusList optional
2193
     * @param toType toType
2194
     *      Optional parameter. Only used for debugging to print the toType documents
2195
     * @param asFilter TODO
2196
     * @return
2197
     * @throws IOException
2198
     */
2199
    protected Query createByDistributionJoinQuery(
2200
            List<NamedArea> namedAreaList,
2201
            List<PresenceAbsenceTerm> distributionStatusList,
2202
            QueryFactory queryFactory, Class<? extends CdmBase> toType, boolean asFilter
2203
            ) throws IOException {
2204

    
2205
        String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
2206
        String toField = "id"; // id in toType usually this is the TaxonBase index
2207

    
2208
        BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, queryFactory);
2209

    
2210
        ScoreMode scoreMode = asFilter ?  ScoreMode.None : ScoreMode.Max;
2211

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

    
2214
        return taxonAreaJoinQuery;
2215
    }
2216

    
2217
    /**
2218
     * @param namedAreaList
2219
     * @param distributionStatusList
2220
     * @param queryFactory
2221
     * @return
2222
     */
2223
    private BooleanQuery createByDistributionQuery(List<NamedArea> namedAreaList,
2224
            List<PresenceAbsenceTerm> distributionStatusList, QueryFactory queryFactory) {
2225
        Builder areaQueryBuilder = new Builder();
2226
        // area field from Distribution
2227
        areaQueryBuilder.add(queryFactory.newEntityIdsQuery("area.id", namedAreaList), Occur.MUST);
2228

    
2229
        // status field from Distribution
2230
        if(distributionStatusList != null && distributionStatusList.size() > 0){
2231
            areaQueryBuilder.add(queryFactory.newEntityIdsQuery("status.id", distributionStatusList), Occur.MUST);
2232
        }
2233

    
2234
        BooleanQuery areaQuery = areaQueryBuilder.build();
2235
        logger.debug("createByDistributionQuery() query: " + areaQuery.toString());
2236
        return areaQuery;
2237
    }
2238

    
2239
    /**
2240
     * This method has been primarily created for testing the area join query but might
2241
     * also be useful in other situations
2242
     *
2243
     * @param namedAreaList
2244
     * @param distributionStatusList
2245
     * @param classification
2246
     * @param highlightFragments
2247
     * @return
2248
     * @throws IOException
2249
     */
2250
    protected LuceneSearch prepareByDistributionSearch(
2251
            List<NamedArea> namedAreaList, List<PresenceAbsenceTerm> distributionStatusList,
2252
            Classification classification, TaxonNode subtree) throws IOException {
2253

    
2254
        Builder finalQueryBuilder = new Builder();
2255

    
2256
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, Taxon.class);
2257

    
2258
        // FIXME is this query factory using the wrong type?
2259
        QueryFactory taxonQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(Taxon.class);
2260

    
2261
        SortField[] sortFields = new  SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING, false)};
2262
        luceneSearch.setSortFields(sortFields);
2263

    
2264

    
2265
        Query byAreaQuery = createByDistributionJoinQuery(namedAreaList, distributionStatusList, taxonQueryFactory, null, false);
2266

    
2267
        finalQueryBuilder.add(byAreaQuery, Occur.MUST);
2268

    
2269
        if(classification != null){
2270
            finalQueryBuilder.add(taxonQueryFactory.newEntityIdQuery(AcceptedTaxonBridge.DOC_KEY_CLASSIFICATION_ID, classification), Occur.MUST);
2271
        }
2272
        if(subtree != null){
2273
            finalQueryBuilder.add(taxonQueryFactory.newTermQuery(AcceptedTaxonBridge.DOC_KEY_TREEINDEX, subtree.treeIndexWc(), true), Occur.MUST);
2274
        }
2275
        BooleanQuery finalQuery = finalQueryBuilder.build();
2276
        logger.info("prepareByAreaSearch() query: " + finalQuery.toString());
2277
        luceneSearch.setQuery(finalQuery);
2278

    
2279
        return luceneSearch;
2280
    }
2281

    
2282
    @Override
2283
    public Pager<SearchResult<TaxonBase>> findByDescriptionElementFullText(
2284
            Class<? extends DescriptionElementBase> clazz, String queryString,
2285
            Classification classification, TaxonNode subtree, List<Feature> features, List<Language> languages,
2286
            boolean highlightFragments, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException {
2287

    
2288
        LuceneSearch luceneSearch = prepareByDescriptionElementFullTextSearch(clazz, queryString, classification, subtree, features, languages, highlightFragments);
2289

    
2290
        // --- execute search
2291
        TopGroups<BytesRef> topDocsResultSet;
2292
        try {
2293
            topDocsResultSet = luceneSearch.executeSearch(pageSize, pageNumber);
2294
        } catch (ParseException e) {
2295
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
2296
            luceneParseException.setStackTrace(e.getStackTrace());
2297
            throw luceneParseException;
2298
        }
2299

    
2300
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
2301
        idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
2302

    
2303
        // --- initialize taxa, highlight matches ....
2304
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
2305
        @SuppressWarnings("rawtypes")
2306
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
2307
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
2308

    
2309
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
2310
        return new DefaultPagerImpl<>(pageNumber, Long.valueOf(totalHits), pageSize, searchResults);
2311
    }
2312

    
2313
    @Override
2314
    public Pager<SearchResult<TaxonBase>> findByEverythingFullText(String queryString,
2315
            Classification classification, TaxonNode subtree, boolean includeUnpublished, List<Language> languages, boolean highlightFragments,
2316
            Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException, LuceneMultiSearchException {
2317

    
2318
        LuceneSearch luceneSearchByDescriptionElement = prepareByDescriptionElementFullTextSearch(null, queryString,
2319
                classification, subtree,
2320
                null, languages, highlightFragments);
2321
        LuceneSearch luceneSearchByTaxonBase = prepareFindByFullTextSearch(null, queryString, classification, subtree, null,
2322
                includeUnpublished, languages, highlightFragments, null);
2323

    
2324
        LuceneMultiSearch multiSearch = new LuceneMultiSearch(luceneIndexToolProvider, luceneSearchByDescriptionElement, luceneSearchByTaxonBase);
2325

    
2326
        // --- execute search
2327
        TopGroups<BytesRef> topDocsResultSet;
2328
        try {
2329
            topDocsResultSet = multiSearch.executeSearch(pageSize, pageNumber);
2330
        } catch (ParseException e) {
2331
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
2332
            luceneParseException.setStackTrace(e.getStackTrace());
2333
            throw luceneParseException;
2334
        }
2335

    
2336
        // --- initialize taxa, highlight matches ....
2337
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(multiSearch, multiSearch.getQuery());
2338

    
2339
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
2340
        idFieldMap.put(CdmBaseType.TAXON_BASE, "id");
2341
        idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
2342

    
2343
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
2344
                topDocsResultSet, multiSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
2345

    
2346
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
2347
        return new DefaultPagerImpl<>(pageNumber, Long.valueOf(totalHits), pageSize, searchResults);
2348
    }
2349

    
2350
    /**
2351
     * @param clazz
2352
     * @param queryString
2353
     * @param classification
2354
     * @param features
2355
     * @param languages
2356
     * @param highlightFragments
2357
     * @param directorySelectClass
2358
     * @return
2359
     */
2360
    protected LuceneSearch prepareByDescriptionElementFullTextSearch(Class<? extends CdmBase> clazz,
2361
            String queryString, Classification classification, TaxonNode subtree, List<Feature> features,
2362
            List<Language> languages, boolean highlightFragments) {
2363

    
2364
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, DescriptionElementBase.class);
2365
        QueryFactory descriptionElementQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(DescriptionElementBase.class);
2366

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

    
2369
        BooleanQuery finalQuery = createByDescriptionElementFullTextQuery(queryString, classification, subtree, features,
2370
                languages, descriptionElementQueryFactory);
2371

    
2372
        luceneSearch.setSortFields(sortFields);
2373
        luceneSearch.setCdmTypRestriction(clazz);
2374
        luceneSearch.setQuery(finalQuery);
2375
        if(highlightFragments){
2376
            luceneSearch.setHighlightFields(descriptionElementQueryFactory.getTextFieldNamesAsArray());
2377
        }
2378

    
2379
        return luceneSearch;
2380
    }
2381

    
2382
    /**
2383
     * @param queryString
2384
     * @param classification
2385
     * @param features
2386
     * @param languages
2387
     * @param descriptionElementQueryFactory
2388
     * @return
2389
     */
2390
    private BooleanQuery createByDescriptionElementFullTextQuery(String queryString,
2391
            Classification classification, TaxonNode subtree, List<Feature> features,
2392
            List<Language> languages, QueryFactory descriptionElementQueryFactory) {
2393

    
2394
        Builder finalQueryBuilder = new Builder();
2395
        Builder textQueryBuilder = new Builder();
2396

    
2397
        if(!StringUtils.isEmpty(queryString)){
2398

    
2399
            textQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("titleCache", queryString), Occur.SHOULD);
2400

    
2401
            // common name
2402
            Builder nameQueryBuilder = new Builder();
2403
            if(languages == null || languages.size() == 0){
2404
                nameQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("name", queryString), Occur.MUST);
2405
            } else {
2406
                Builder languageSubQueryBuilder = new Builder();
2407
                for(Language lang : languages){
2408
                    languageSubQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("language.uuid",  lang.getUuid().toString(), false), Occur.SHOULD);
2409
                }
2410
                nameQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("name", queryString), Occur.MUST);
2411
                nameQueryBuilder.add(languageSubQueryBuilder.build(), Occur.MUST);
2412
            }
2413
            textQueryBuilder.add(nameQueryBuilder.build(), Occur.SHOULD);
2414

    
2415

    
2416
            // text field from TextData
2417
            textQueryBuilder.add(descriptionElementQueryFactory.newMultilanguageTextQuery("text", queryString, languages), Occur.SHOULD);
2418

    
2419
            // --- TermBase fields - by representation ----
2420
            // state field from CategoricalData
2421
            textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("stateData.state", queryString, languages), Occur.SHOULD);
2422

    
2423
            // state field from CategoricalData
2424
            textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("stateData.modifyingText", queryString, languages), Occur.SHOULD);
2425

    
2426
            // area field from Distribution
2427
            textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("area", queryString, languages), Occur.SHOULD);
2428

    
2429
            // status field from Distribution
2430
            textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("status", queryString, languages), Occur.SHOULD);
2431

    
2432
            finalQueryBuilder.add(textQueryBuilder.build(), Occur.MUST);
2433

    
2434
        }
2435
        // --- classification ----
2436

    
2437

    
2438
        if(classification != null){
2439
            finalQueryBuilder.add(descriptionElementQueryFactory.newEntityIdQuery("inDescription.taxon.taxonNodes.classification.id", classification), Occur.MUST);
2440
        }
2441
        if(subtree != null){
2442
            finalQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("inDescription.taxon.taxonNodes.treeIndex", subtree.treeIndexWc(), true), Occur.MUST);
2443
        }
2444

    
2445
        // --- IdentifieableEntity fields - by uuid
2446
        if(features != null && features.size() > 0 ){
2447
            finalQueryBuilder.add(descriptionElementQueryFactory.newEntityUuidsQuery("feature.uuid", features), Occur.MUST);
2448
        }
2449

    
2450
        // the description must be associated with a taxon
2451
        finalQueryBuilder.add(descriptionElementQueryFactory.newIsNotNullQuery("inDescription.taxon.id"), Occur.MUST);
2452

    
2453
        BooleanQuery finalQuery = finalQueryBuilder.build();
2454
        logger.info("prepareByDescriptionElementFullTextSearch() query: " + finalQuery.toString());
2455
        return finalQuery;
2456
    }
2457

    
2458
    @Override
2459
    public List<Synonym> createInferredSynonyms(Taxon taxon, Classification classification, SynonymType type, boolean doWithMisappliedNames){
2460

    
2461
        List <Synonym> inferredSynonyms = new ArrayList<>();
2462
        List<Synonym> inferredSynonymsToBeRemoved = new ArrayList<>();
2463

    
2464
        Map <UUID, IZoologicalName> zooHashMap = new HashMap<>();
2465
        boolean includeUnpublished = INCLUDE_UNPUBLISHED;
2466

    
2467
        UUID nameUuid= taxon.getName().getUuid();
2468
        IZoologicalName taxonName = getZoologicalName(nameUuid, zooHashMap);
2469
        String epithetOfTaxon = null;
2470
        String infragenericEpithetOfTaxon = null;
2471
        String infraspecificEpithetOfTaxon = null;
2472
        if (taxonName.isSpecies()){
2473
             epithetOfTaxon= taxonName.getSpecificEpithet();
2474
        } else if (taxonName.isInfraGeneric()){
2475
            infragenericEpithetOfTaxon = taxonName.getInfraGenericEpithet();
2476
        } else if (taxonName.isInfraSpecific()){
2477
            infraspecificEpithetOfTaxon = taxonName.getInfraSpecificEpithet();
2478
        }
2479
        String genusOfTaxon = taxonName.getGenusOrUninomial();
2480
        Set<TaxonNode> nodes = taxon.getTaxonNodes();
2481
        List<String> taxonNames = new ArrayList<>();
2482

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

    
2487
            if (node.getClassification().equals(classification)){
2488
                if (!node.isTopmostNode()){
2489
                    TaxonNode parent = node.getParent();
2490
                    parent = CdmBase.deproxy(parent);
2491
                    TaxonName parentName =  parent.getTaxon().getName();
2492
                    IZoologicalName zooParentName = CdmBase.deproxy(parentName);
2493
                    Taxon parentTaxon = CdmBase.deproxy(parent.getTaxon());
2494

    
2495
                    //create inferred synonyms for species, subspecies
2496
                    if ((parentName.isGenus() || parentName.isSpecies() || parentName.getRank().equals(Rank.SUBGENUS())) ){
2497

    
2498
                        Synonym inferredEpithet = null;
2499
                        Synonym inferredGenus = null;
2500
                        Synonym potentialCombination = null;
2501

    
2502
                        List<String> propertyPaths = new ArrayList<>();
2503
                        propertyPaths.add("synonym");
2504
                        propertyPaths.add("synonym.name");
2505
                        List<OrderHint> orderHintsSynonyms = new ArrayList<>();
2506
                        orderHintsSynonyms.add(new OrderHint("titleCache", SortOrder.ASCENDING));
2507

    
2508
                        List<Synonym> synonyMsOfParent = dao.getSynonyms(parentTaxon, SynonymType.HETEROTYPIC_SYNONYM_OF(), null, null,orderHintsSynonyms,propertyPaths);
2509
                        List<Synonym> synonymsOfTaxon= dao.getSynonyms(taxon, SynonymType.HETEROTYPIC_SYNONYM_OF(),
2510
                                null, null,orderHintsSynonyms,propertyPaths);
2511

    
2512
                        List<TaxonRelationship> taxonRelListParent = new ArrayList<>();
2513
                        List<TaxonRelationship> taxonRelListTaxon = new ArrayList<>();
2514
                        if (doWithMisappliedNames){
2515
                            List<OrderHint> orderHintsMisapplied = new ArrayList<>();
2516
                            orderHintsMisapplied.add(new OrderHint("relatedFrom.titleCache", SortOrder.ASCENDING));
2517
                            taxonRelListParent = dao.getTaxonRelationships(parentTaxon, TaxonRelationshipType.MISAPPLIED_NAME_FOR(),
2518
                                    includeUnpublished, null, null, orderHintsMisapplied, propertyPaths, Direction.relatedTo);
2519
                            taxonRelListTaxon = dao.getTaxonRelationships(taxon, TaxonRelationshipType.MISAPPLIED_NAME_FOR(),
2520
                                    includeUnpublished, null, null, orderHintsMisapplied, propertyPaths, Direction.relatedTo);
2521
                        }
2522

    
2523
                        if (type.equals(SynonymType.INFERRED_EPITHET_OF())){
2524
                            for (Synonym synonymRelationOfParent:synonyMsOfParent){
2525

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

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

    
2538
                            if (doWithMisappliedNames){
2539

    
2540
                                for (TaxonRelationship taxonRelationship: taxonRelListParent){
2541
                                     Taxon misappliedName = taxonRelationship.getFromTaxon();
2542

    
2543
                                     inferredEpithet = createInferredEpithets(taxon,
2544
                                             zooHashMap, taxonName, epithetOfTaxon,
2545
                                             infragenericEpithetOfTaxon,
2546
                                             infraspecificEpithetOfTaxon,
2547
                                             taxonNames, parentName,
2548
                                             misappliedName);
2549

    
2550
                                    inferredSynonyms.add(inferredEpithet);
2551
                                    zooHashMap.put(inferredEpithet.getName().getUuid(), inferredEpithet.getName());
2552
                                    taxonNames.add(inferredEpithet.getName().getNameCache());
2553
                                }
2554
                            }
2555

    
2556
                            if (!taxonNames.isEmpty()){
2557
                                List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2558
                                if (!synNotInCDM.isEmpty()){
2559
                                    inferredSynonymsToBeRemoved.clear();
2560

    
2561
                                    for (Synonym syn :inferredSynonyms){
2562
                                        IZoologicalName name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2563
                                        if (!synNotInCDM.contains(name.getNameCache())){
2564
                                            inferredSynonymsToBeRemoved.add(syn);
2565
                                        }
2566
                                    }
2567

    
2568
                                    // Remove identified Synonyms from inferredSynonyms
2569
                                    for (Synonym synonym : inferredSynonymsToBeRemoved) {
2570
                                        inferredSynonyms.remove(synonym);
2571
                                    }
2572
                                }
2573
                            }
2574

    
2575
                        }else if (type.equals(SynonymType.INFERRED_GENUS_OF())){
2576

    
2577
                            for (Synonym synonymRelationOfTaxon:synonymsOfTaxon){
2578

    
2579
                                inferredGenus = createInferredGenus(taxon,
2580
                                        zooHashMap, taxonName, epithetOfTaxon,
2581
                                        genusOfTaxon, taxonNames, zooParentName, synonymRelationOfTaxon);
2582

    
2583
                                inferredSynonyms.add(inferredGenus);
2584
                                zooHashMap.put(inferredGenus.getName().getUuid(), inferredGenus.getName());
2585
                                taxonNames.add(inferredGenus.getName().getNameCache());
2586
                            }
2587

    
2588
                            if (doWithMisappliedNames){
2589

    
2590
                                for (TaxonRelationship taxonRelationship: taxonRelListTaxon){
2591
                                    Taxon misappliedName = taxonRelationship.getFromTaxon();
2592
                                    inferredGenus = createInferredGenus(taxon, zooHashMap, taxonName, infraspecificEpithetOfTaxon, genusOfTaxon, taxonNames, zooParentName,  misappliedName);
2593

    
2594
                                    inferredSynonyms.add(inferredGenus);
2595
                                    zooHashMap.put(inferredGenus.getName().getUuid(), inferredGenus.getName());
2596
                                     taxonNames.add(inferredGenus.getName().getNameCache());
2597
                                }
2598
                            }
2599

    
2600

    
2601
                            if (!taxonNames.isEmpty()){
2602
                                List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2603
                                IZoologicalName name;
2604
                                if (!synNotInCDM.isEmpty()){
2605
                                    inferredSynonymsToBeRemoved.clear();
2606

    
2607
                                    for (Synonym syn :inferredSynonyms){
2608
                                        name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2609
                                        if (!synNotInCDM.contains(name.getNameCache())){
2610
                                            inferredSynonymsToBeRemoved.add(syn);
2611
                                        }
2612
                                    }
2613

    
2614
                                    // Remove identified Synonyms from inferredSynonyms
2615
                                    for (Synonym synonym : inferredSynonymsToBeRemoved) {
2616
                                        inferredSynonyms.remove(synonym);
2617
                                    }
2618
                                }
2619
                            }
2620

    
2621
                        }else if (type.equals(SynonymType.POTENTIAL_COMBINATION_OF())){
2622

    
2623
                            Reference sourceReference = null; // TODO: Determination of sourceReference is redundant
2624
                            //for all synonyms of the parent...
2625
                            for (Synonym synonymRelationOfParent:synonyMsOfParent){
2626
                                TaxonName synName;
2627
                                HibernateProxyHelper.deproxy(synonymRelationOfParent);
2628

    
2629
                                synName = synonymRelationOfParent.getName();
2630

    
2631
                                // Set the sourceReference
2632
                                sourceReference = synonymRelationOfParent.getSec();
2633

    
2634
                                // Determine the idInSource
2635
                                String idInSourceParent = getIdInSource(synonymRelationOfParent);
2636

    
2637
                                IZoologicalName parentSynZooName = getZoologicalName(synName.getUuid(), zooHashMap);
2638
                                String synParentGenus = parentSynZooName.getGenusOrUninomial();
2639
                                String synParentInfragenericName = null;
2640
                                String synParentSpecificEpithet = null;
2641

    
2642
                                if (parentSynZooName.isInfraGeneric()){
2643
                                    synParentInfragenericName = parentSynZooName.getInfraGenericEpithet();
2644
                                }
2645
                                if (parentSynZooName.isSpecies()){
2646
                                    synParentSpecificEpithet = parentSynZooName.getSpecificEpithet();
2647
                                }
2648

    
2649
                               /* if (synGenusName != null && !synonymsGenus.containsKey(synGenusName)){
2650
                                    synonymsGenus.put(synGenusName, idInSource);
2651
                                }*/
2652

    
2653
                                //for all synonyms of the taxon
2654

    
2655
                                for (Synonym synonymRelationOfTaxon:synonymsOfTaxon){
2656

    
2657
                                    IZoologicalName zooSynName = getZoologicalName(synonymRelationOfTaxon.getName().getUuid(), zooHashMap);
2658
                                    potentialCombination = createPotentialCombination(idInSourceParent, parentSynZooName, zooSynName,
2659
                                            synParentGenus,
2660
                                            synParentInfragenericName,
2661
                                            synParentSpecificEpithet, synonymRelationOfTaxon, zooHashMap);
2662

    
2663
                                    taxon.addSynonym(potentialCombination, SynonymType.POTENTIAL_COMBINATION_OF());
2664
                                    inferredSynonyms.add(potentialCombination);
2665
                                    zooHashMap.put(potentialCombination.getName().getUuid(), potentialCombination.getName());
2666
                                     taxonNames.add(potentialCombination.getName().getNameCache());
2667
                                }
2668
                            }
2669

    
2670
                            if (doWithMisappliedNames){
2671

    
2672
                                for (TaxonRelationship parentRelationship: taxonRelListParent){
2673

    
2674
                                    TaxonName misappliedParentName;
2675

    
2676
                                    Taxon misappliedParent = parentRelationship.getFromTaxon();
2677
                                    misappliedParentName = misappliedParent.getName();
2678

    
2679
                                    HibernateProxyHelper.deproxy(misappliedParent);
2680

    
2681
                                    // Set the sourceReference
2682
                                    sourceReference = misappliedParent.getSec();
2683

    
2684
                                    // Determine the idInSource
2685
                                    String idInSourceParent = getIdInSource(misappliedParent);
2686

    
2687
                                    IZoologicalName parentSynZooName = getZoologicalName(misappliedParentName.getUuid(), zooHashMap);
2688
                                    String synParentGenus = parentSynZooName.getGenusOrUninomial();
2689
                                    String synParentInfragenericName = null;
2690
                                    String synParentSpecificEpithet = null;
2691

    
2692
                                    if (parentSynZooName.isInfraGeneric()){
2693
                                        synParentInfragenericName = parentSynZooName.getInfraGenericEpithet();
2694
                                    }
2695
                                    if (parentSynZooName.isSpecies()){
2696
                                        synParentSpecificEpithet = parentSynZooName.getSpecificEpithet();
2697
                                    }
2698

    
2699
                                    for (TaxonRelationship taxonRelationship: taxonRelListTaxon){
2700
                                        Taxon misappliedName = taxonRelationship.getFromTaxon();
2701
                                        IZoologicalName zooMisappliedName = getZoologicalName(misappliedName.getName().getUuid(), zooHashMap);
2702
                                        potentialCombination = createPotentialCombination(
2703
                                                idInSourceParent, parentSynZooName, zooMisappliedName,
2704
                                                synParentGenus,
2705
                                                synParentInfragenericName,
2706
                                                synParentSpecificEpithet, misappliedName, zooHashMap);
2707

    
2708
                                        taxon.addSynonym(potentialCombination, SynonymType.POTENTIAL_COMBINATION_OF());
2709
                                        inferredSynonyms.add(potentialCombination);
2710
                                        zooHashMap.put(potentialCombination.getName().getUuid(), potentialCombination.getName());
2711
                                         taxonNames.add(potentialCombination.getName().getNameCache());
2712
                                    }
2713
                                }
2714
                            }
2715

    
2716
                            if (!taxonNames.isEmpty()){
2717
                                List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2718
                                IZoologicalName name;
2719
                                if (!synNotInCDM.isEmpty()){
2720
                                    inferredSynonymsToBeRemoved.clear();
2721
                                    for (Synonym syn :inferredSynonyms){
2722
                                        try{
2723
                                            name = syn.getName();
2724
                                        }catch (ClassCastException e){
2725
                                            name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2726
                                        }
2727
                                        if (!synNotInCDM.contains(name.getNameCache())){
2728
                                            inferredSynonymsToBeRemoved.add(syn);
2729
                                        }
2730
                                     }
2731
                                    // Remove identified Synonyms from inferredSynonyms
2732
                                    for (Synonym synonym : inferredSynonymsToBeRemoved) {
2733
                                        inferredSynonyms.remove(synonym);
2734
                                    }
2735
                                }
2736
                            }
2737
                        }
2738
                    }else {
2739
                        logger.info("The synonym type is not defined.");
2740
                        return inferredSynonyms;
2741
                    }
2742
                }
2743
            }
2744
        }
2745

    
2746
        return inferredSynonyms;
2747
    }
2748

    
2749
    private Synonym createPotentialCombination(String idInSourceParent,
2750
            IZoologicalName parentSynZooName, 	IZoologicalName zooSynName, String synParentGenus,
2751
            String synParentInfragenericName, String synParentSpecificEpithet,
2752
            TaxonBase<?> syn, Map<UUID, IZoologicalName> zooHashMap) {
2753
        Synonym potentialCombination;
2754
        Reference sourceReference;
2755
        IZoologicalName inferredSynName;
2756
        HibernateProxyHelper.deproxy(syn);
2757

    
2758
        // Set sourceReference
2759
        sourceReference = syn.getSec();
2760
        if (sourceReference == null){
2761
            logger.warn("The synonym has no sec reference because it is a misapplied name! Take the sec reference of taxon");
2762
            //TODO:Remove
2763
            if (!parentSynZooName.getTaxa().isEmpty()){
2764
                TaxonBase<?> taxon = parentSynZooName.getTaxa().iterator().next();
2765

    
2766
                sourceReference = taxon.getSec();
2767
            }
2768
        }
2769
        String synTaxonSpecificEpithet = zooSynName.getSpecificEpithet();
2770

    
2771
        String synTaxonInfraSpecificName= null;
2772

    
2773
        if (parentSynZooName.isSpecies()){
2774
            synTaxonInfraSpecificName = zooSynName.getInfraSpecificEpithet();
2775
        }
2776

    
2777
        /*if (epithetName != null && !synonymsEpithet.contains(epithetName)){
2778
            synonymsEpithet.add(epithetName);
2779
        }*/
2780

    
2781
        //create potential combinations...
2782
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(syn.getName().getRank());
2783

    
2784
        inferredSynName.setGenusOrUninomial(synParentGenus);
2785
        if (zooSynName.isSpecies()){
2786
              inferredSynName.setSpecificEpithet(synTaxonSpecificEpithet);
2787
              if (parentSynZooName.isInfraGeneric()){
2788
                  inferredSynName.setInfraGenericEpithet(synParentInfragenericName);
2789
              }
2790
        }
2791
        if (zooSynName.isInfraSpecific()){
2792
            inferredSynName.setSpecificEpithet(synParentSpecificEpithet);
2793
            inferredSynName.setInfraSpecificEpithet(synTaxonInfraSpecificName);
2794
        }
2795
        if (parentSynZooName.isInfraGeneric()){
2796
            inferredSynName.setInfraGenericEpithet(synParentInfragenericName);
2797
        }
2798

    
2799
        potentialCombination = Synonym.NewInstance(inferredSynName, null);
2800

    
2801
        // Set the sourceReference
2802
        potentialCombination.setSec(sourceReference);
2803

    
2804

    
2805
        // Determine the idInSource
2806
        String idInSourceSyn= getIdInSource(syn);
2807

    
2808
        if (idInSourceParent != null && idInSourceSyn != null) {
2809
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation, idInSourceSyn + "; " + idInSourceParent, POTENTIAL_COMBINATION_NAMESPACE, sourceReference, null);
2810
            inferredSynName.addSource(originalSource);
2811
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation, idInSourceSyn + "; " + idInSourceParent, POTENTIAL_COMBINATION_NAMESPACE, sourceReference, null);
2812
            potentialCombination.addSource(originalSource);
2813
        }
2814

    
2815
        return potentialCombination;
2816
    }
2817

    
2818
    private Synonym createInferredGenus(Taxon taxon,
2819
            Map<UUID, IZoologicalName> zooHashMap, IZoologicalName taxonName,
2820
            String epithetOfTaxon, String genusOfTaxon,
2821
            List<String> taxonNames, IZoologicalName zooParentName,
2822
            TaxonBase syn) {
2823

    
2824
        Synonym inferredGenus;
2825
        TaxonName synName;
2826
        IZoologicalName inferredSynName;
2827
        synName =syn.getName();
2828
        HibernateProxyHelper.deproxy(syn);
2829

    
2830
        // Determine the idInSource
2831
        String idInSourceSyn = getIdInSource(syn);
2832
        String idInSourceTaxon = getIdInSource(taxon);
2833
        // Determine the sourceReference
2834
        Reference sourceReference = syn.getSec();
2835

    
2836
        //logger.warn(sourceReference.getTitleCache());
2837

    
2838
        synName = syn.getName();
2839
        IZoologicalName synZooName = getZoologicalName(synName.getUuid(), zooHashMap);
2840
        String synSpeciesEpithetName = synZooName.getSpecificEpithet();
2841
         /* if (synonymsEpithet != null && !synonymsEpithet.contains(synSpeciesEpithetName)){
2842
            synonymsEpithet.add(synSpeciesEpithetName);
2843
        }*/
2844

    
2845
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(taxon.getName().getRank());
2846
        //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...
2847

    
2848
        inferredSynName.setGenusOrUninomial(genusOfTaxon);
2849
        if (zooParentName.isInfraGeneric()){
2850
            inferredSynName.setInfraGenericEpithet(zooParentName.getInfraGenericEpithet());
2851
        }
2852

    
2853
        if (taxonName.isSpecies()){
2854
            inferredSynName.setSpecificEpithet(synSpeciesEpithetName);
2855
        }
2856
        if (taxonName.isInfraSpecific()){
2857
            inferredSynName.setSpecificEpithet(epithetOfTaxon);
2858
            inferredSynName.setInfraSpecificEpithet(synZooName.getInfraGenericEpithet());
2859
        }
2860

    
2861
        inferredGenus = Synonym.NewInstance(inferredSynName, null);
2862

    
2863
        // Set the sourceReference
2864
        inferredGenus.setSec(sourceReference);
2865

    
2866
        // Add the original source
2867
        if (idInSourceSyn != null && idInSourceTaxon != null) {
2868
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2869
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2870
            inferredGenus.addSource(originalSource);
2871

    
2872
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2873
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2874
            inferredSynName.addSource(originalSource);
2875
            originalSource = null;
2876

    
2877
        }else{
2878
            logger.error("There is an idInSource missing: " + idInSourceSyn + " of Synonym or " + idInSourceTaxon + " of Taxon");
2879
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2880
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2881
            inferredGenus.addSource(originalSource);
2882

    
2883
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2884
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2885
            inferredSynName.addSource(originalSource);
2886
            originalSource = null;
2887
        }
2888

    
2889
        taxon.addSynonym(inferredGenus, SynonymType.INFERRED_GENUS_OF());
2890

    
2891
        return inferredGenus;
2892
    }
2893

    
2894
    private Synonym createInferredEpithets(Taxon taxon,
2895
            Map<UUID, IZoologicalName> zooHashMap, IZoologicalName taxonName,
2896
            String epithetOfTaxon, String infragenericEpithetOfTaxon,
2897
            String infraspecificEpithetOfTaxon, List<String> taxonNames,
2898
            TaxonName parentName, TaxonBase<?> syn) {
2899

    
2900
        Synonym inferredEpithet;
2901
        TaxonName synName;
2902
        IZoologicalName inferredSynName;
2903
        HibernateProxyHelper.deproxy(syn);
2904

    
2905
        // Determine the idInSource
2906
        String idInSourceSyn = getIdInSource(syn);
2907
        String idInSourceTaxon =  getIdInSource(taxon);
2908
        // Determine the sourceReference
2909
        Reference sourceReference = syn.getSec();
2910

    
2911
        if (sourceReference == null){
2912
             logger.warn("The synonym has no sec reference because it is a misapplied name! Take the sec reference of taxon" + taxon.getSec());
2913
             sourceReference = taxon.getSec();
2914
        }
2915

    
2916
        synName = syn.getName();
2917
        IZoologicalName zooSynName = getZoologicalName(synName.getUuid(), zooHashMap);
2918
        String synGenusName = zooSynName.getGenusOrUninomial();
2919
        String synInfraGenericEpithet = null;
2920
        String synSpecificEpithet = null;
2921

    
2922
        if (zooSynName.getInfraGenericEpithet() != null){
2923
            synInfraGenericEpithet = zooSynName.getInfraGenericEpithet();
2924
        }
2925

    
2926
        if (zooSynName.isInfraSpecific()){
2927
            synSpecificEpithet = zooSynName.getSpecificEpithet();
2928
        }
2929

    
2930
           /* if (synGenusName != null && !synonymsGenus.containsKey(synGenusName)){
2931
            synonymsGenus.put(synGenusName, idInSource);
2932
        }*/
2933

    
2934
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(taxon.getName().getRank());
2935

    
2936
        // DEBUG TODO: for subgenus or subspecies the infrageneric or infraspecific epithet should be used!!!
2937
        if (epithetOfTaxon == null && infragenericEpithetOfTaxon == null && infraspecificEpithetOfTaxon == null) {
2938
            logger.error("This specificEpithet is NULL" + taxon.getTitleCache());
2939
        }
2940
        inferredSynName.setGenusOrUninomial(synGenusName);
2941

    
2942
        if (parentName.isInfraGeneric()){
2943
            inferredSynName.setInfraGenericEpithet(synInfraGenericEpithet);
2944
        }
2945
        if (taxonName.isSpecies()){
2946
            inferredSynName.setSpecificEpithet(epithetOfTaxon);
2947
        }else if (taxonName.isInfraSpecific()){
2948
            inferredSynName.setSpecificEpithet(synSpecificEpithet);
2949
            inferredSynName.setInfraSpecificEpithet(infraspecificEpithetOfTaxon);
2950
        }
2951

    
2952
        inferredEpithet = Synonym.NewInstance(inferredSynName, null);
2953

    
2954
        // Set the sourceReference
2955
        inferredEpithet.setSec(sourceReference);
2956

    
2957
        /* Add the original source
2958
        if (idInSource != null) {
2959
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(idInSource, "InferredEpithetOf", syn.getSec(), null);
2960

    
2961
            // Add the citation
2962
            Reference citation = getCitation(syn);
2963
            if (citation != null) {
2964
                originalSource.setCitation(citation);
2965
                inferredEpithet.addSource(originalSource);
2966
            }
2967
        }*/
2968
        String taxonId = idInSourceTaxon+ "; " + idInSourceSyn;
2969

    
2970

    
2971
        IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2972
                taxonId, INFERRED_EPITHET_NAMESPACE, sourceReference, null);
2973

    
2974
        inferredEpithet.addSource(originalSource);
2975

    
2976
        originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2977
                taxonId, INFERRED_EPITHET_NAMESPACE, sourceReference, null);
2978

    
2979
        inferredSynName.addSource(originalSource);
2980

    
2981
        taxon.addSynonym(inferredEpithet, SynonymType.INFERRED_EPITHET_OF());
2982

    
2983
        return inferredEpithet;
2984
    }
2985

    
2986
    /**
2987
     * Returns an existing IZoologicalName or extends an internal hashmap if it does not exist.
2988
     * Very likely only useful for createInferredSynonyms().
2989
     * @param uuid
2990
     * @param zooHashMap
2991
     * @return
2992
     */
2993
    private IZoologicalName getZoologicalName(UUID uuid, Map <UUID, IZoologicalName> zooHashMap) {
2994
        IZoologicalName taxonName =nameDao.findZoologicalNameByUUID(uuid);
2995
        if (taxonName == null) {
2996
            taxonName = zooHashMap.get(uuid);
2997
        }
2998
        return taxonName;
2999
    }
3000

    
3001
    /**
3002
     * Returns the idInSource for a given Synonym.
3003
     * @param syn
3004
     */
3005
    private String getIdInSource(TaxonBase<?> taxonBase) {
3006
        String idInSource = null;
3007
        Set<IdentifiableSource> sources = taxonBase.getSources();
3008
        if (sources.size() == 1) {
3009
            IdentifiableSource source = sources.iterator().next();
3010
            if (source != null) {
3011
                idInSource  = source.getIdInSource();
3012
            }
3013
        } else if (sources.size() > 1) {
3014
            int count = 1;
3015
            idInSource = "";
3016
            for (IdentifiableSource source : sources) {
3017
                idInSource += source.getIdInSource();
3018
                if (count < sources.size()) {
3019
                    idInSource += "; ";
3020
                }
3021
                count++;
3022
            }
3023
        } else if (sources.size() == 0){
3024
            logger.warn("No idInSource for TaxonBase " + taxonBase.getUuid() + " - " + taxonBase.getTitleCache());
3025
        }
3026

    
3027
        return idInSource;
3028
    }
3029

    
3030
    /**
3031
     * Returns the citation for a given Synonym.
3032
     * @param syn
3033
     */
3034
    private Reference getCitation(Synonym syn) {
3035
        Reference citation = null;
3036
        Set<IdentifiableSource> sources = syn.getSources();
3037
        if (sources.size() == 1) {
3038
            IdentifiableSource source = sources.iterator().next();
3039
            if (source != null) {
3040
                citation = source.getCitation();
3041
            }
3042
        } else if (sources.size() > 1) {
3043
            logger.warn("This Synonym has more than one source: " + syn.getUuid() + " (" + syn.getTitleCache() +")");
3044
        }
3045

    
3046
        return citation;
3047
    }
3048

    
3049
    @Override
3050
    public List<Synonym>  createAllInferredSynonyms(Taxon taxon, Classification tree, boolean doWithMisappliedNames){
3051
        List <Synonym> inferredSynonyms = new ArrayList<>();
3052

    
3053
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.INFERRED_EPITHET_OF(), doWithMisappliedNames));
3054
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.INFERRED_GENUS_OF(), doWithMisappliedNames));
3055
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.POTENTIAL_COMBINATION_OF(), doWithMisappliedNames));
3056

    
3057
        return inferredSynonyms;
3058
    }
3059

    
3060
    @Override
3061
    public List<Classification> listClassifications(TaxonBase taxonBase, Integer limit, Integer start, List<String> propertyPaths) {
3062

    
3063
        // TODO quickly implemented, create according dao !!!!
3064
        Set<TaxonNode> nodes = new HashSet<>();
3065
        Set<Classification> classifications = new HashSet<>();
3066
        List<Classification> list = new ArrayList<>();
3067

    
3068
        if (taxonBase == null) {
3069
            return list;
3070
        }
3071

    
3072
        taxonBase = load(taxonBase.getUuid());
3073

    
3074
        if (taxonBase instanceof Taxon) {
3075
            nodes.addAll(((Taxon)taxonBase).getTaxonNodes());
3076
        } else {
3077
            Taxon taxon = ((Synonym)taxonBase).getAcceptedTaxon();
3078
            if (taxon != null){
3079
                nodes.addAll(taxon.getTaxonNodes());
3080
            }
3081
        }
3082
        for (TaxonNode node : nodes) {
3083
            classifications.add(node.getClassification());
3084
        }
3085
        list.addAll(classifications);
3086
        return list;
3087
    }
3088

    
3089
    @Override
3090
    @Transactional(readOnly = false)
3091
    public UpdateResult changeRelatedTaxonToSynonym(UUID fromTaxonUuid,
3092
            UUID toTaxonUuid,
3093
            TaxonRelationshipType oldRelationshipType,
3094
            SynonymType synonymType) throws DataChangeNoRollbackException {
3095
        UpdateResult result = new UpdateResult();
3096
        Taxon fromTaxon = (Taxon) dao.load(fromTaxonUuid);
3097
        Taxon toTaxon = (Taxon) dao.load(toTaxonUuid);
3098
        result = changeRelatedTaxonToSynonym(fromTaxon, toTaxon, oldRelationshipType, synonymType);
3099

    
3100
        result.addUpdatedObject(toTaxon);
3101
        result.addUpdatedObject(result.getCdmEntity());
3102

    
3103
        return result;
3104
    }
3105

    
3106
    @Override
3107
    @Transactional(readOnly = false)
3108
    public UpdateResult changeRelatedTaxonToSynonym(Taxon fromTaxon, Taxon toTaxon, TaxonRelationshipType oldRelationshipType,
3109
            SynonymType synonymType) throws DataChangeNoRollbackException {
3110

    
3111
        UpdateResult result = new UpdateResult();
3112
        // Create new synonym using concept name
3113
        TaxonName synonymName = fromTaxon.getName();
3114

    
3115
        // Remove concept relation from taxon
3116
        toTaxon.removeTaxon(fromTaxon, oldRelationshipType);
3117

    
3118
        // Create a new synonym for the taxon
3119
        Synonym synonym;
3120
        if (synonymType != null
3121
                && synonymType.equals(SynonymType.HOMOTYPIC_SYNONYM_OF())){
3122
            synonym = Synonym.NewInstance(synonymName, fromTaxon.getSec());
3123
            toTaxon.addHomotypicSynonym(synonym);
3124
        } else{
3125
            synonym = toTaxon.addHeterotypicSynonymName(synonymName);
3126
        }
3127
        //keep the publish flag
3128
        synonym.setPublish(fromTaxon.isPublish());
3129
        this.saveOrUpdate(toTaxon);
3130
        //TODO: configurator and classification
3131
        TaxonDeletionConfigurator config = new TaxonDeletionConfigurator();
3132
        config.setDeleteNameIfPossible(false);
3133
        result.includeResult(this.deleteTaxon(fromTaxon.getUuid(), config, null));
3134
        result.setCdmEntity(synonym);
3135
        result.addUpdatedObject(toTaxon);
3136
        result.addUpdatedObject(synonym);
3137
        return result;
3138
    }
3139

    
3140
    @Override
3141
    public DeleteResult isDeletable(UUID taxonBaseUuid, DeleteConfiguratorBase config){
3142
        DeleteResult result = new DeleteResult();
3143
        TaxonBase<?> taxonBase = load(taxonBaseUuid);
3144
        Set<CdmBase> references = commonService.getReferencingObjectsForDeletion(taxonBase);
3145
        if (taxonBase instanceof Taxon){
3146
            TaxonDeletionConfigurator taxonConfig = (TaxonDeletionConfigurator) config;
3147
            List<String> propertyPaths = new ArrayList<>();
3148
            propertyPaths.add("taxonNodes");
3149
            Taxon taxon = (Taxon)load(taxonBaseUuid, propertyPaths);
3150

    
3151
            result = isDeletableForTaxon(references, taxonConfig );
3152

    
3153
            if (taxonConfig.isDeleteNameIfPossible()){
3154
                if (taxonBase.getName() != null){
3155
                    DeleteResult nameResult = nameService.isDeletable(taxonBase.getName().getUuid(), taxonConfig.getNameDeletionConfig(), taxon.getUuid());
3156
                    if (!nameResult.isOk()){
3157
                        result.addExceptions(nameResult.getExceptions());
3158
                    }
3159
                }
3160

    
3161
            }
3162
        }else{
3163
            SynonymDeletionConfigurator synonymConfig = (SynonymDeletionConfigurator) config;
3164
            result = isDeletableForSynonym(references, synonymConfig);
3165
            if (synonymConfig.isDeleteNameIfPossible() && taxonBase.getName() != null){
3166
                DeleteResult nameResult = nameService.isDeletable(taxonBase.getName().getUuid(), synonymConfig.getNameDeletionConfig(), taxonBase.getUuid());
3167
                if (!nameResult.isOk()){
3168
                    result.addExceptions(nameResult.getExceptions());
3169

    
3170
                }
3171
            }
3172
        }
3173

    
3174
        return result;
3175
    }
3176

    
3177
    private DeleteResult isDeletableForSynonym(Set<CdmBase> references, SynonymDeletionConfigurator config){
3178

    
3179
        DeleteResult result = new DeleteResult();
3180
        for (CdmBase ref: references){
3181
            if (!(ref instanceof Taxon || ref instanceof TaxonName || ref instanceof SecundumSource)){
3182
                String message = "The Synonym can't be deleted as long as it is referenced by " + ref.getClass().getSimpleName() + " with id "+ ref.getId();
3183
                result.addException(new ReferencedObjectUndeletableException(message));
3184
                result.addRelatedObject(ref);
3185
                result.setAbort();
3186
            }
3187
        }
3188

    
3189
        return result;
3190
    }
3191

    
3192
    private DeleteResult isDeletableForTaxon(Set<CdmBase> references, TaxonDeletionConfigurator config){
3193
        String message = null;
3194
        DeleteResult result = new DeleteResult();
3195
        for (CdmBase ref: references){
3196
            if (!(ref instanceof TaxonName || ref instanceof SecundumSource)){
3197
            	message = null;
3198
                if (!config.isDeleteSynonymRelations() && (ref instanceof Synonym)){
3199
                    message = "The taxon can't be deleted as long as it has synonyms.";
3200
                }
3201
                if (!config.isDeleteDescriptions() && (ref instanceof DescriptionBase)){
3202
                    message = "The taxon can't be deleted as long as it has factual data.";
3203
                }
3204

    
3205
                if (!config.isDeleteTaxonNodes() && (ref instanceof TaxonNode)){
3206
                    message = "The taxon can't be deleted as long as it belongs to a taxon node.";
3207
                }
3208
                if (ref instanceof TaxonNode && config.getClassificationUuid() != null && !config.isDeleteInAllClassifications() && !((TaxonNode)ref).getClassification().getUuid().equals(config.getClassificationUuid())){
3209
                    message = "The taxon can't be deleted as long as it is used in more than one classification";
3210

    
3211
                }
3212
                if (!config.isDeleteTaxonRelationships() && (ref instanceof TaxonRelationship)){
3213
                    if (!config.isDeleteMisappliedNames() &&
3214
                            (((TaxonRelationship)ref).getType().isMisappliedName())){
3215
                        message = "The taxon can't be deleted as long as it has misapplied names or invalid designations.";
3216
                    } else{
3217
                        message = "The taxon can't be deleted as long as it belongs to taxon relationship.";
3218
                    }
3219
                }
3220
                if (ref instanceof PolytomousKeyNode){
3221
                    message = "The taxon can't be deleted as long as it is referenced by a polytomous key node.";
3222
                }
3223

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

    
3228
               /* //PolytomousKeyNode
3229
                if (referencingObject.isInstanceOf(PolytomousKeyNode.class)){
3230
                    String message = "Taxon" + taxon.getTitleCache() + " can't be deleted as it is used in polytomous key node";
3231
                    return message;
3232
                }*/
3233

    
3234
                //TaxonInteraction
3235
                if (ref.isInstanceOf(TaxonInteraction.class)){
3236
                    message = "Taxon can't be deleted as it is used in taxonInteraction#taxon2";
3237
                }
3238

    
3239
              //TaxonInteraction
3240
                if (ref.isInstanceOf(DeterminationEvent.class)){
3241
                    message = "Taxon can't be deleted as it is used in a determination event";
3242
                }
3243
            }
3244
            if (message != null){
3245
	            result.addException(new ReferencedObjectUndeletableException(message));
3246
	            result.addRelatedObject(ref);
3247
	            result.setAbort();
3248
            }
3249
        }
3250

    
3251
        return result;
3252
    }
3253

    
3254
    @Override
3255
    public IncludedTaxaDTO listIncludedTaxa(UUID taxonUuid, IncludedTaxonConfiguration config) {
3256
        IncludedTaxaDTO result = new IncludedTaxaDTO(taxonUuid);
3257

    
3258
        //preliminary implementation
3259

    
3260
        Set<Taxon> taxa = new HashSet<>();
3261
        TaxonBase<?> taxonBase = find(taxonUuid);
3262
        if (taxonBase == null){
3263
            return new IncludedTaxaDTO();
3264
        }else if (taxonBase.isInstanceOf(Taxon.class)){
3265
            Taxon taxon = CdmBase.deproxy(taxonBase, Taxon.class);
3266
            taxa.add(taxon);
3267
        }else if (taxonBase.isInstanceOf(Synonym.class)){
3268
            //TODO partial synonyms ??
3269
            //TODO synonyms in general
3270
            Synonym syn = CdmBase.deproxy(taxonBase, Synonym.class);
3271
            taxa.add(syn.getAcceptedTaxon());
3272
        }else{
3273
            throw new IllegalArgumentException("Unhandled class " + taxonBase.getClass().getSimpleName());
3274
        }
3275

    
3276
        Set<Taxon> related = makeRelatedIncluded(taxa, result, config);
3277
        int i = 0;
3278
        while((! related.isEmpty()) && i++ < 100){  //to avoid
3279
             related = makeRelatedIncluded(related, result, config);
3280
        }
3281

    
3282
        return result;
3283
    }
3284

    
3285
    /**
3286
     * @param uncheckedTaxa
3287
     * @param existingTaxa
3288
     * @param config
3289
     *
3290
     * Computes all children and conceptually congruent and included taxa and adds them to the existingTaxa
3291
     * data structure.
3292
     * @return the set of conceptually related taxa for further use
3293
     */
3294
    private Set<Taxon> makeRelatedIncluded(Set<Taxon> uncheckedTaxa, IncludedTaxaDTO existingTaxa, IncludedTaxonConfiguration config) {
3295

    
3296
        //children
3297
        Set<TaxonNode> taxonNodes = new HashSet<>();
3298
        for (Taxon taxon: uncheckedTaxa){
3299
            taxonNodes.addAll(taxon.getTaxonNodes());
3300
        }
3301

    
3302
        Set<Taxon> children = new HashSet<>();
3303
        if (! config.onlyCongruent){
3304
            for (TaxonNode node: taxonNodes){
3305
                List<TaxonNode> childNodes = nodeService.loadChildNodesOfTaxonNode(node, null, true, config.includeUnpublished, null);
3306
                for (TaxonNode child : childNodes){
3307
                    children.add(child.getTaxon());
3308
                }
3309
            }
3310
            children.remove(null);  // just to be on the save side
3311
        }
3312

    
3313
        Iterator<Taxon> it = children.iterator();
3314
        while(it.hasNext()){
3315
            UUID uuid = it.next().getUuid();
3316
            if (existingTaxa.contains(uuid)){
3317
                it.remove();
3318
            }else{
3319
                existingTaxa.addIncludedTaxon(uuid, new ArrayList<>(), false);
3320
            }
3321
        }
3322

    
3323
        //concept relations
3324
        Set<Taxon> uncheckedAndChildren = new HashSet<>(uncheckedTaxa);
3325
        uncheckedAndChildren.addAll(children);
3326

    
3327
        Set<Taxon> relatedTaxa = makeConceptIncludedTaxa(uncheckedAndChildren, existingTaxa, config);
3328

    
3329

    
3330
        Set<Taxon> result = new HashSet<>(relatedTaxa);
3331
        return result;
3332
    }
3333

    
3334
    /**
3335
     * Computes all conceptually congruent or included taxa and adds them to the existingTaxa data structure.
3336
     * @return the set of these computed taxa
3337
     */
3338
    private Set<Taxon> makeConceptIncludedTaxa(Set<Taxon> unchecked, IncludedTaxaDTO existingTaxa, IncludedTaxonConfiguration config) {
3339
        Set<Taxon> result = new HashSet<>();
3340

    
3341
        for (Taxon taxon : unchecked){
3342
            Set<TaxonRelationship> fromRelations = taxon.getRelationsFromThisTaxon();
3343
            Set<TaxonRelationship> toRelations = taxon.getRelationsToThisTaxon();
3344

    
3345
            for (TaxonRelationship fromRel : fromRelations){
3346
                if (config.includeDoubtful == false && fromRel.isDoubtful()){
3347
                    continue;
3348
                }
3349
                TaxonRelationshipType fromRelType = fromRel.getType();
3350
                if (fromRelType.equals(TaxonRelationshipType.CONGRUENT_TO()) ||
3351
                        !config.onlyCongruent && (
3352
                            fromRelType.equals(TaxonRelationshipType.INCLUDES()) ||
3353
                            fromRelType.equals(TaxonRelationshipType.CONGRUENT_OR_INCLUDES())
3354
                        )
3355
                    ){
3356
                    result.add(fromRel.getToTaxon());
3357
                }
3358
            }
3359

    
3360
            for (TaxonRelationship toRel : toRelations){
3361
                if (config.includeDoubtful == false && toRel.isDoubtful()){
3362
                    continue;
3363
                }
3364
                TaxonRelationshipType fromRelType = toRel.getType();
3365
                if (fromRelType.equals(TaxonRelationshipType.CONGRUENT_TO()) ||
3366
                        !config.includeDoubtful && fromRelType.equals(TaxonRelationshipType.TAXONOMICALLY_INCLUDED_IN())){
3367
                    result.add(toRel.getFromTaxon());
3368
                }
3369
            }
3370
        }
3371

    
3372
        Iterator<Taxon> it = result.iterator();
3373
        while(it.hasNext()){
3374
            UUID uuid = it.next().getUuid();
3375
            if (existingTaxa.contains(uuid)){
3376
                it.remove();
3377
            }else{
3378
                existingTaxa.addIncludedTaxon(uuid, new ArrayList<>(), false);
3379
            }
3380
        }
3381
        return result;
3382
    }
3383

    
3384
    @Override
3385
    public List<TaxonBase> findTaxaByName(MatchingTaxonConfigurator config){
3386
        @SuppressWarnings("rawtypes")
3387
        List<TaxonBase> taxonList = dao.getTaxaByName(true, config.isIncludeSynonyms(), false, false, false,
3388
                config.getTaxonNameTitle(), null, null, MatchMode.EXACT, null, config.isIncludeSynonyms(), null, null, null, config.getPropertyPath());
3389
        return taxonList;
3390
    }
3391

    
3392
	@Override
3393
	@Transactional(readOnly = true)
3394
	public <S extends TaxonBase> Pager<IdentifiedEntityDTO<S>> findByIdentifier(
3395
			Class<S> clazz, String identifier, DefinedTerm identifierType, TaxonNode subtreeFilter,
3396
			MatchMode matchmode, boolean includeEntity, Integer pageSize,
3397
			Integer pageNumber,	List<String> propertyPaths) {
3398
		if (subtreeFilter == null){
3399
			return findByIdentifier(clazz, identifier, identifierType, matchmode, includeEntity, pageSize, pageNumber, propertyPaths);
3400
		}
3401

    
3402
		long numberOfResults = dao.countByIdentifier(clazz, identifier, identifierType, subtreeFilter, matchmode);
3403
        List<Object[]> daoResults = new ArrayList<>();
3404
        if(numberOfResults > 0) { // no point checking again
3405
        	daoResults = dao.findByIdentifier(clazz, identifier, identifierType, subtreeFilter,
3406
    				matchmode, includeEntity, pageSize, pageNumber, propertyPaths);
3407
        }
3408

    
3409
        List<IdentifiedEntityDTO<S>> result = new ArrayList<>();
3410
        for (Object[] daoObj : daoResults){
3411
        	if (includeEntity){
3412
        		result.add(new IdentifiedEntityDTO<>((DefinedTerm)daoObj[0], (String)daoObj[1], (S)daoObj[2]));
3413
        	}else{
3414
        		result.add(new IdentifiedEntityDTO<>((DefinedTerm)daoObj[0], (String)daoObj[1], (UUID)daoObj[2], (String)daoObj[3], null));
3415
        	}
3416
        }
3417
		return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, result);
3418
	}
3419

    
3420
	@Override
3421
    @Transactional(readOnly = true)
3422
    public <S extends TaxonBase> Pager<MarkedEntityDTO<S>> findByMarker(
3423
            Class<S> clazz, MarkerType markerType, Boolean markerValue,
3424
            TaxonNode subtreeFilter, boolean includeEntity, TaxonTitleType titleType,
3425
            Integer pageSize, Integer pageNumber, List<String> propertyPaths) {
3426
        if (subtreeFilter == null){
3427
            return super.findByMarker (clazz, markerType, markerValue, includeEntity, pageSize, pageNumber, propertyPaths);
3428
        }
3429

    
3430
        Long numberOfResults = dao.countByMarker(clazz, markerType, markerValue, subtreeFilter);
3431
        List<Object[]> daoResults = new ArrayList<>();
3432
        if(numberOfResults > 0) { // no point checking again
3433
            daoResults = dao.findByMarker(clazz, markerType, markerValue, subtreeFilter,
3434
                    includeEntity, titleType, pageSize, pageNumber, propertyPaths);
3435
        }
3436

    
3437
        List<MarkedEntityDTO<S>> result = new ArrayList<>();
3438
        for (Object[] daoObj : daoResults){
3439
            if (includeEntity){
3440
                result.add(new MarkedEntityDTO<S>((MarkerType)daoObj[0], (Boolean)daoObj[1], (S)daoObj[2]));
3441
            }else{
3442
                result.add(new MarkedEntityDTO<S>((MarkerType)daoObj[0], (Boolean)daoObj[1], (UUID)daoObj[2], (String)daoObj[3]));
3443
            }
3444
        }
3445
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, result);
3446
    }
3447

    
3448
    @Override
3449
	public UpdateResult moveFactualDateToAnotherTaxon(UUID fromTaxonUuid, UUID toTaxonUuid){
3450
		UpdateResult result = new UpdateResult();
3451

    
3452
		Taxon fromTaxon = (Taxon)dao.load(fromTaxonUuid);
3453
		Taxon toTaxon = (Taxon) dao.load(toTaxonUuid);
3454
    	for(TaxonDescription description : fromTaxon.getDescriptions()){
3455
              //reload to avoid session conflicts
3456
              description = HibernateProxyHelper.deproxy(description, TaxonDescription.class);
3457

    
3458
              String moveMessage = String.format("Description moved from %s", fromTaxon);
3459
              if(description.isProtectedTitleCache()){
3460
                  String separator = "";
3461
                  if(!StringUtils.isBlank(description.getTitleCache())){
3462
                      separator = " - ";
3463
                  }
3464
                  description.setTitleCache(description.getTitleCache() + separator + moveMessage, true);
3465
              }
3466
              Annotation annotation = Annotation.NewInstance(moveMessage, Language.getDefaultLanguage());
3467
              annotation.setAnnotationType(AnnotationType.TECHNICAL());
3468
              description.addAnnotation(annotation);
3469
              toTaxon.addDescription(description);
3470
              dao.saveOrUpdate(toTaxon);
3471
              dao.saveOrUpdate(fromTaxon);
3472
              result.addUpdatedObject(toTaxon);
3473
              result.addUpdatedObject(fromTaxon);
3474

    
3475
        }
3476

    
3477
    	return result;
3478
	}
3479

    
3480
	@Override
3481
	@Transactional(readOnly = false)
3482
	public UpdateResult swapSynonymAndAcceptedTaxon(UUID synonymUUid,
3483
			UUID acceptedTaxonUuid, boolean setNameInSource, boolean newUuidForAcceptedTaxon, SecReferenceHandlingSwapEnum secHandling, UUID newSecAcc, UUID newSecSyn) {
3484
		TaxonBase<?> base = this.load(synonymUUid);
3485
		Synonym syn = HibernateProxyHelper.deproxy(base, Synonym.class);
3486
		base = this.load(acceptedTaxonUuid);
3487
		Taxon taxon = HibernateProxyHelper.deproxy(base, Taxon.class);
3488

    
3489
		Reference refAcc = referenceService.load(newSecAcc);
3490
		Reference refSyn = referenceService.load(newSecSyn);
3491

    
3492
		return this.swapSynonymAndAcceptedTaxon(syn, taxon, setNameInSource, newUuidForAcceptedTaxon, secHandling, refAcc, refSyn);
3493
	}
3494

    
3495
    @Override
3496
    public TaxonRelationshipsDTO listTaxonRelationships(UUID taxonUuid, Set<TaxonRelationshipType> directTypes,
3497
            Set<TaxonRelationshipType> inversTypes,
3498
            Direction direction, boolean groupMisapplications,
3499
            boolean includeUnpublished,
3500
            Integer pageSize, Integer pageNumber) {
3501
        TaxonBase<?> taxonBase = dao.load(taxonUuid);
3502
        if (taxonBase == null || !taxonBase.isInstanceOf(TaxonBase.class)){
3503
            //TODO handle
3504
            throw new RuntimeException("Taxon for uuid " + taxonUuid + " not found");
3505
        }else{
3506
            Taxon taxon = CdmBase.deproxy(taxonBase, Taxon.class);
3507
            boolean doDirect = (direction == null || direction == Direction.relatedTo);
3508
            boolean doInvers = (direction == null || direction == Direction.relatedFrom);
3509

    
3510
            TaxonRelationshipsDTO dto = new TaxonRelationshipsDTO();
3511

    
3512
            //TODO paging is difficult because misapplication string is an attribute
3513
            //of toplevel dto
3514
//        long numberOfResults = dao.countTaxonRelationships(taxon, type, includeUnpublished, TaxonRelationship.Direction.relatedFrom);
3515
//        List<TaxonRelationshipsDTO> results = new ArrayList<>();
3516
//        if(numberOfResults > 0) { // no point checking again
3517
//            results = dao.getTaxonRelationships(taxon, type, includeUnpublished, pageSize, pageNumber, orderHints, propertyPaths, TaxonRelationship.Direction.relatedFrom);
3518
//        }
3519
//
3520
//        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);;
3521

    
3522
            //TODO languages
3523
            List<Language> languages = null;
3524
            if (doDirect){
3525
                direction = Direction.relatedTo;
3526
                //TODO order hints, property path
3527
                List<TaxonRelationship> relations = dao.getTaxonRelationships(taxon, directTypes, includeUnpublished, pageSize, pageNumber, null, null, direction.invers());
3528
                for (TaxonRelationship relation : relations){
3529
                    dto.addRelation(relation, direction, languages);
3530
                }
3531
            }
3532
            if (doInvers){
3533
                direction = Direction.relatedFrom;
3534
                //TODO order hints, property path
3535
                List<TaxonRelationship> relations = dao.getTaxonRelationships(taxon, inversTypes, includeUnpublished, pageSize, pageNumber, null, null, direction.invers());
3536
                for (TaxonRelationship relation : relations){
3537
                    dto.addRelation(relation, direction, languages);
3538
                }
3539
            }
3540
            if (groupMisapplications){
3541
                //TODO
3542
                dto.createMisapplicationString();
3543
            }
3544
            return dto;
3545
        }
3546
    }
3547
}
(87-87/95)